diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7b553d57d7..3bd5b1a460 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -187,6 +187,34 @@ jobs: TS_NODE_SKIP_IGNORE: true MAINNET_RPC_URL: https://eth-mainnet.alchemyapi.io/v2/${{ secrets.ALCHEMY_MAINNET_KEY }} FORK_NETWORK: mainnet + + monitor-tests: + name: 'Monitor Tests (Mainnet)' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16.x + cache: 'yarn' + - run: yarn install --immutable + - name: 'Cache hardhat network fork' + uses: actions/cache@v3 + with: + path: cache/hardhat-network-fork + key: hardhat-network-fork-${{ runner.os }}-${{ hashFiles('test/integration/fork-block-numbers.ts') }} + restore-keys: | + hardhat-network-fork-${{ runner.os }}- + hardhat-network-fork- + - run: npx hardhat test ./test/monitor/*.test.ts + env: + NODE_OPTIONS: '--max-old-space-size=8192' + TS_NODE_SKIP_IGNORE: true + MAINNET_RPC_URL: https://eth-mainnet.alchemyapi.io/v2/${{ secrets.ALCHEMY_MAINNET_KEY }} + FORK_NETWORK: mainnet + FORK: 1 + PROTO_IMPL: 1 + slither: name: 'Slither' runs-on: ubuntu-latest diff --git a/.openzeppelin/base_8453.json b/.openzeppelin/base_8453.json index 6c4c4a3671..0d90ea97cb 100644 --- a/.openzeppelin/base_8453.json +++ b/.openzeppelin/base_8453.json @@ -3144,6 +3144,190 @@ } } } + }, + "83264eb95f2f9ab0055f3cdf3d195b52003b35099a624ee29920f6a83be6b884": { + "address": "0xD45a441F334f6f27CDDA3728414FD26Cc5798E66", + "txHash": "0xcce3cfb75dad5e947efeab8a30cd981ca578d96f7a8bee1512a86b2849a0fa24", + "layout": { + "solcVersion": "0.8.19", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "07b40b651527d3b3c3f0d1fb77a991853411f5b7fd564a45478bb03e177adcae": { + "address": "0x69c20aD99eb1054cd7Da2809572205186975dA17", + "txHash": "0x05c19fbc6774d5e85aadba888cc56e0764a104c1da7e3fa9f0774dfba8a46215", + "layout": { + "solcVersion": "0.8.19", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/.openzeppelin/mainnet.json b/.openzeppelin/mainnet.json index 8ae5721830..d0dae943ca 100644 --- a/.openzeppelin/mainnet.json +++ b/.openzeppelin/mainnet.json @@ -3747,10 +3747,7 @@ }, "t_enum(TradeKind)25002": { "label": "enum TradeKind", - "members": [ - "DUTCH_AUCTION", - "BATCH_AUCTION" - ], + "members": ["DUTCH_AUCTION", "BATCH_AUCTION"], "numberOfBytes": "1" }, "t_mapping(t_contract(IERC20)15191,t_contract(ITrade)27151)": { @@ -4043,11 +4040,7 @@ }, "t_enum(CollateralStatus)24460": { "label": "enum CollateralStatus", - "members": [ - "SOUND", - "IFFY", - "DISABLED" - ], + "members": ["SOUND", "IFFY", "DISABLED"], "numberOfBytes": "1" }, "t_mapping(t_bytes32,t_bytes32)": { @@ -6340,10 +6333,7 @@ }, "t_enum(TradeKind)17751": { "label": "enum TradeKind", - "members": [ - "DUTCH_AUCTION", - "BATCH_AUCTION" - ], + "members": ["DUTCH_AUCTION", "BATCH_AUCTION"], "numberOfBytes": "1" }, "t_mapping(t_contract(IERC20)11113,t_contract(ITrade)19704)": { @@ -6652,6 +6642,190 @@ } } } + }, + "f0632c54f5763a16d6d87d14d0e7a80a079e8b998507fa1d081ee3b631c3961c": { + "address": "0xA42850A760151bb3ACF17E7f8643EB4d864bF7a6", + "txHash": "0xfa37e2544175813e2b4308c62f14f05f336a62ea25c94dd9346f710449498d0c", + "layout": { + "solcVersion": "0.8.19", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "ebc9c3f1c253e562c3d21649a4c7d904b40ed64689bc3d3bc57bbe09fcd1d120": { + "address": "0x35fDc5537c32588bfc97b393A8ed522Df737af5A", + "txHash": "0xc1d9400b9492c969e5a156fa8e419ccd8a1138160f6eb4079192455e3af357e6", + "layout": { + "solcVersion": "0.8.19", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 693d32bd91..a5330e5235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,93 @@ # Changelog +# 3.1.0 + +### Upgrade Steps -- Required + +Upgrade all core contracts and _all_ assets. Most ERC20s do not need to be upgraded. Use `Deployer.deployRTokenAsset()` to create a new `RTokenAsset` instance. This asset should be swapped too. + +ERC20s that _do_ need to be upgraded: + +- Morpho +- Convex +- CompoundV3 + +Then, call `Broker.cacheComponents()`. + +Finally, call `Broker.setBatchTradeImplementation(newGnosisTrade)`. + +### Core Protocol Contracts + +- `BackingManager` [+2 slots] + - Replace use of `lotPrice()` with `price()` everywhere + - Track `tokensOut` on trades and account for during collateralization math + - Call `StRSR.payoutRewards()` after forwarding RSR + - Make `backingBuffer` math precise + - Add caching in `RecollateralizationLibP1` + - Use `price().low` instead of `price().high` to compute maximum sell amounts +- `BasketHandler` + - Replace use of `lotPrice()` with `price()` everywhere + - Minor gas optimizations to status tracking and custom redemption math +- `Broker` [+1 slot] + - Cache `rToken` address and add `cacheComponents()` helper + - Allow `reportViolation()` to be called when paused or frozen + - Disallow starting dutch trades with non-RTokenAsset assets when `lastSave() != block.timestamp` +- `Distributor` + - Call `RevenueTrader.distributeTokenToBuy()` before distribution table changes + - Call `StRSR.payoutRewards()` or `Furnace.melt()` after distributions + - Minor gas optimizations +- `Furnace` + - Allow melting while frozen +- `Main` + - Remove `furnace.melt()` from `poke()` +- `RevenueTrader` + - Replace use of `lotPrice()` with `price()` everywhere + - Ensure `settleTrade` cannot be reverted due to `tokenToBuy` distribution + - Ensure during `manageTokens()` that the Distributor is configured for the `tokenToBuy` +- `StRSR` + - Use correct era in `UnstakingStarted` event + - Expose `draftEra` via `getDraftEra()` view + +### Facades + +- `FacadeMonitor` + - Add `batchAuctionsDisabled()` view + - Add `dutchAuctionsDisabled()` view + - Add `issuanceAvailable()` view + - Add `redemptionAvailable()` view + - Add `backingRedeemable()` view +- `FacadeRead` + - Add `draftEra` argument to `pendingUnstakings()` + - Remove `.melt()` calls during pokes + +## Plugins + +### Assets + +- ALL + - Deprecate `lotPrice()` + - Alter `price().low` to decay downwards to 0 over the price timeout + - Alter `price().high` to decay upwards to 3x over the price timeout + - Move `ORACLE_TIMEOUT_BUFFER` into code, as opposed to incorporating at the deployment script level + - Make`refPerTok()` smoother during event of hard default + - Check for `defaultThreshold > 0` in constructors + - Add 9 more decimals of precision to reward accounting (some wrappers excluded) +- compoundv2: make wrapper much more gas efficient during COMP claim +- compoundv3 bugfix: check permission correctly on underlying comet +- curve: Also `refresh()` the RToken's AssetRegistry during `refresh()` +- convex: Update to latest approved wrapper from Convex team +- morpho-aave: Add ability to track and handout MORPHO rewards +- yearnv2: Use pricePerShare helper for more precision + +### Governance + +- Add a minimum voting delay of 1 day + +### Trading + +- `GnosisTrade` + - Add `sellAmount() returns (uint192)` view + # 3.0.1 ### Upgrade steps @@ -8,6 +96,8 @@ Update `BackingManager`, both `RevenueTraders` (rTokenTrader/rsrTrader), and cal # 3.0.0 +Bump solidity version to 0.8.19 + ### Upgrade Steps #### Required Steps @@ -38,9 +128,7 @@ It is acceptable to leave these function calls out of the initial upgrade tx and ### Core Protocol Contracts - `AssetRegistry` [+1 slot] - Summary: StRSR contract need to know when refresh() was last called - - # Add last refresh timestamp tracking and expose via `lastRefresh()` getter - Summary: Other component contracts need to know when refresh() was last called + Summary: Other component contracts need to know when refresh() was last called - Add `lastRefresh()` timestamp getter - Add `size()` getter for number of registered assets - Require asset is SOUND on registration @@ -180,10 +268,10 @@ Remove `FacadeMonitor` - now redundant with `nextRecollateralizationAuction()` a - `FacadeRead` Summary: Add new data summary views frontends may be interested in - - Remove `basketNonce` from `redeem(.., uint48 basketNonce)` - - Add `redeemCustom(.., uint48[] memory basketNonces, uint192[] memory portions)` callstatic to simulate multi-basket redemptions - - Remove `traderBalances(..)` - - Add `balancesAcrossAllTraders(IBackingManager) returns (IERC20[] memory erc20s, uint256[] memory balances, uint256[] memory balancesNeededByBackingManager)` +- Remove `basketNonce` from `redeem(.., uint48 basketNonce)` +- Add `redeemCustom(.., uint48[] memory basketNonces, uint192[] memory portions)` callstatic to simulate multi-basket redemptions +- Remove `traderBalances(..)` +- Add `balancesAcrossAllTraders(IBackingManager) returns (IERC20[] memory erc20s, uint256[] memory balances, uint256[] memory balancesNeededByBackingManager)` - `FacadeWrite` Summary: More expressive and fine-grained control over the set of pausers and freezers diff --git a/README.md b/README.md index 1c569eb492..e8ab78faec 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ For a much more detailed explanation of the economic design, including an hour-l - [Testing with Echidna](https://github.com/reserve-protocol/protocol/blob/master/docs/using-echidna.md): Notes so far on setup and usage of Echidna (which is decidedly an integration-in-progress!) - [Deployment](https://github.com/reserve-protocol/protocol/blob/master/docs/deployment.md): How to do test deployments in our environment. - [System Design](https://github.com/reserve-protocol/protocol/blob/master/docs/system-design.md): The overall architecture of our system, and some detailed descriptions about what our protocol is _intended_ to do. +- [Pause and Freeze States](https://github.com/reserve-protocol/protocol/blob/master/docs/pause-freeze-states.md): An overview of which protocol functions are halted in the paused and frozen states. - [Deployment Variables](https://github.com/reserve-protocol/protocol/blob/master/docs/deployment-variables.md) A detailed description of the governance variables of the protocol. - [Our Solidity Style](https://github.com/reserve-protocol/protocol/blob/master/docs/solidity-style.md): Common practices, details, and conventions relevant to reading and writing our Solidity source code, estpecially where those go beyond standard practice. - [Writing Collateral Plugins](https://github.com/reserve-protocol/protocol/blob/master/docs/collateral.md): An overview of how to develop collateral plugins and the concepts / questions involved. diff --git a/common/configuration.ts b/common/configuration.ts index 308bfc671e..3692d8068f 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -109,6 +109,7 @@ interface INetworkConfig { AAVE_INCENTIVES?: string AAVE_EMISSIONS_MGR?: string AAVE_RESERVE_TREASURY?: string + AAVE_DATA_PROVIDER?: string COMPTROLLER?: string FLUX_FINANCE_COMPTROLLER?: string GNOSIS_EASY_AUCTION?: string @@ -224,6 +225,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { AAVE_INCENTIVES: '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5', AAVE_EMISSIONS_MGR: '0xEE56e2B3D491590B5b31738cC34d5232F378a8D5', AAVE_RESERVE_TREASURY: '0x464C71f6c2F760DdA6093dCB91C24c39e5d6e18c', + AAVE_DATA_PROVIDER: '0x057835Ad21a177dbdd3090bB1CAE03EaCF78Fc6d', FLUX_FINANCE_COMPTROLLER: '0x95Af143a021DF745bc78e845b54591C53a8B3A51', COMPTROLLER: '0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B', GNOSIS_EASY_AUCTION: '0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101', @@ -329,6 +331,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { }, AAVE_LENDING_POOL: '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9', AAVE_RESERVE_TREASURY: '0x464C71f6c2F760DdA6093dCB91C24c39e5d6e18c', + AAVE_DATA_PROVIDER: '0x057835Ad21a177dbdd3090bB1CAE03EaCF78Fc6d', FLUX_FINANCE_COMPTROLLER: '0x95Af143a021DF745bc78e845b54591C53a8B3A51', COMPTROLLER: '0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B', GNOSIS_EASY_AUCTION: '0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101', @@ -428,6 +431,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { }, AAVE_LENDING_POOL: '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9', AAVE_RESERVE_TREASURY: '0x464C71f6c2F760DdA6093dCB91C24c39e5d6e18c', + AAVE_DATA_PROVIDER: '0x057835Ad21a177dbdd3090bB1CAE03EaCF78Fc6d', FLUX_FINANCE_COMPTROLLER: '0x95Af143a021DF745bc78e845b54591C53a8B3A51', COMPTROLLER: '0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B', GNOSIS_EASY_AUCTION: '0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101', @@ -642,6 +646,10 @@ export interface IRTokenConfig { params: IConfig } +export interface IMonitorParams { + AAVE_V2_DATA_PROVIDER_ADDR: string +} + export interface IBackupInfo { backupUnit: string diversityFactor: BigNumber @@ -690,7 +698,7 @@ export const MAX_THROTTLE_PCT_RATE = BigNumber.from(10).pow(18) export const GNOSIS_MAX_TOKENS = BigNumber.from(7).mul(BigNumber.from(10).pow(28)) // Timestamps -export const MAX_ORACLE_TIMEOUT = BigNumber.from(2).pow(48).sub(1) +export const MAX_ORACLE_TIMEOUT = BigNumber.from(2).pow(48).sub(1).sub(300) export const MAX_TRADING_DELAY = 31536000 // 1 year export const MIN_WARMUP_PERIOD = 60 // 1 minute export const MAX_WARMUP_PERIOD = 31536000 // 1 year diff --git a/common/numbers.ts b/common/numbers.ts index 6d53f464d1..d49a2a6606 100644 --- a/common/numbers.ts +++ b/common/numbers.ts @@ -16,7 +16,9 @@ export const pow10 = (exponent: BigNumberish): BigNumber => { // Convert `x` to a new BigNumber with decimals = `decimals`. // Input should have SCALE_DECIMALS (18) decimal places, and `decimals` should be less than 18. export const toBNDecimals = (x: BigNumberish, decimals: number): BigNumber => { - return BigNumber.from(x).div(pow10(SCALE_DECIMALS - decimals)) + return decimals < SCALE_DECIMALS + ? BigNumber.from(x).div(pow10(SCALE_DECIMALS - decimals)) + : BigNumber.from(x).mul(pow10(decimals - SCALE_DECIMALS)) } // Convert to the BigNumber representing a Fix from a BigNumberish. diff --git a/contracts/facade/FacadeAct.sol b/contracts/facade/FacadeAct.sol index 3534a1fa62..45f32b4f7c 100644 --- a/contracts/facade/FacadeAct.sol +++ b/contracts/facade/FacadeAct.sol @@ -117,11 +117,11 @@ contract FacadeAct is IFacadeAct, Multicall { } surpluses[i] = erc20s[i].balanceOf(address(revenueTrader)); - (uint192 lotLow, ) = reg.assets[i].lotPrice(); // {UoA/tok} - if (lotLow == 0) continue; + (uint192 low, ) = reg.assets[i].price(); // {UoA/tok} + if (low == 0) continue; // {qTok} = {UoA} / {UoA/tok} - minTradeAmounts[i] = minTradeVolume.safeDiv(lotLow, FLOOR).shiftl_toUint( + minTradeAmounts[i] = minTradeVolume.safeDiv(low, FLOOR).shiftl_toUint( int8(reg.assets[i].erc20Decimals()) ); diff --git a/contracts/facade/FacadeMonitor.sol b/contracts/facade/FacadeMonitor.sol new file mode 100644 index 0000000000..e8221a1195 --- /dev/null +++ b/contracts/facade/FacadeMonitor.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "../interfaces/IFacadeMonitor.sol"; +import "../interfaces/IRToken.sol"; +import "../libraries/Fixed.sol"; +import "../p1/RToken.sol"; +import "../plugins/assets/compoundv2/CTokenWrapper.sol"; +import "../plugins/assets/compoundv3/ICusdcV3Wrapper.sol"; +import "../plugins/assets/stargate/StargateRewardableWrapper.sol"; +import { StaticATokenV3LM } from "../plugins/assets/aave-v3/vendor/StaticATokenV3LM.sol"; +import "../plugins/assets/morpho-aave/MorphoAaveV2TokenisedDeposit.sol"; + +interface IAaveProtocolDataProvider { + function getReserveData(address asset) + external + view + returns ( + uint256 availableLiquidity, + uint256 totalStableDebt, + uint256 totalVariableDebt, + uint256 liquidityRate, + uint256 variableBorrowRate, + uint256 stableBorrowRate, + uint256 averageStableBorrowRate, + uint256 liquidityIndex, + uint256 variableBorrowIndex, + uint40 lastUpdateTimestamp + ); +} + +interface IStaticATokenLM is IERC20 { + // solhint-disable-next-line func-name-mixedcase + function UNDERLYING_ASSET_ADDRESS() external view returns (address); + + function dynamicBalanceOf(address account) external view returns (uint256); +} + +/** + * @title FacadeMonitor + * @notice A UX-friendly layer for monitoring RTokens + */ +contract FacadeMonitor is Initializable, OwnableUpgradeable, UUPSUpgradeable, IFacadeMonitor { + using FixLib for uint192; + + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + // solhint-disable-next-line var-name-mixedcase + address public immutable AAVE_V2_DATA_PROVIDER; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(MonitorParams memory params) { + AAVE_V2_DATA_PROVIDER = params.AAVE_V2_DATA_PROVIDER_ADDR; + _disableInitializers(); + } + + function init(address initialOwner) public initializer { + require(initialOwner != address(0), "invalid owner address"); + + __Ownable_init(); + __UUPSUpgradeable_init(); + _transferOwnership(initialOwner); + } + + // === Views === + + /// @return Whether batch auctions are disabled for a specific rToken + function batchAuctionsDisabled(IRToken rToken) external view returns (bool) { + return rToken.main().broker().batchTradeDisabled(); + } + + /// @return Whether any dutch auction is disabled for a specific rToken + function dutchAuctionsDisabled(IRToken rToken) external view returns (bool) { + bool disabled = false; + + IERC20[] memory erc20s = rToken.main().assetRegistry().erc20s(); + for (uint256 i = 0; i < erc20s.length; ++i) { + if (rToken.main().broker().dutchTradeDisabled(IERC20Metadata(address(erc20s[i])))) + disabled = true; + } + + return disabled; + } + + /// @return Which percentage of issuance throttle is still available for a specific rToken + function issuanceAvailable(IRToken rToken) external view returns (uint256) { + ThrottleLib.Params memory params = RTokenP1(address(rToken)).issuanceThrottleParams(); + + // Calculate hourly limit as: max(params.amtRate, supply.mul(params.pctRate)) + uint256 limit = (rToken.totalSupply() * params.pctRate) / FIX_ONE_256; // {qRTok} + if (params.amtRate > limit) limit = params.amtRate; + + uint256 issueAvailable = rToken.issuanceAvailable(); + if (issueAvailable >= limit) return FIX_ONE_256; + + return (issueAvailable * FIX_ONE_256) / limit; + } + + function redemptionAvailable(IRToken rToken) external view returns (uint256) { + ThrottleLib.Params memory params = RTokenP1(address(rToken)).redemptionThrottleParams(); + + uint256 supply = rToken.totalSupply(); + + if (supply == 0) return FIX_ONE_256; + + // Calculate hourly limit as: max(params.amtRate, supply.mul(params.pctRate)) + uint256 limit = (supply * params.pctRate) / FIX_ONE_256; // {qRTok} + if (params.amtRate > limit) limit = supply < params.amtRate ? supply : params.amtRate; + + uint256 redeemAvailable = rToken.redemptionAvailable(); + if (redeemAvailable >= limit) return FIX_ONE_256; + + return (redeemAvailable * FIX_ONE_256) / limit; + } + + function backingReedemable( + IRToken rToken, + CollPluginType collType, + IERC20 erc20 + ) external view returns (uint256) { + uint256 backingBalance; + uint256 availableLiquidity; + + if (collType == CollPluginType.AAVE_V2 || collType == CollPluginType.MORPHO_AAVE_V2) { + address underlying; + if (collType == CollPluginType.AAVE_V2) { + // AAVE V2 - Uses Static wrapper + IStaticATokenLM staticAToken = IStaticATokenLM(address(erc20)); + backingBalance = staticAToken.dynamicBalanceOf( + address(rToken.main().backingManager()) + ); + underlying = staticAToken.UNDERLYING_ASSET_ADDRESS(); + } else { + // MORPHO AAVE V2 + MorphoAaveV2TokenisedDeposit mrpTknDeposit = MorphoAaveV2TokenisedDeposit( + address(erc20) + ); + backingBalance = mrpTknDeposit.convertToAssets( + mrpTknDeposit.balanceOf(address(rToken.main().backingManager())) + ); + underlying = mrpTknDeposit.underlying(); + } + + (availableLiquidity, , , , , , , , , ) = IAaveProtocolDataProvider( + AAVE_V2_DATA_PROVIDER + ).getReserveData(underlying); + } else if (collType == CollPluginType.AAVE_V3) { + StaticATokenV3LM staticAToken = StaticATokenV3LM(address(erc20)); + IERC20 aToken = staticAToken.aToken(); + IERC20 underlying = IERC20(staticAToken.asset()); + + backingBalance = staticAToken.convertToAssets( + staticAToken.balanceOf(address(rToken.main().backingManager())) + ); + availableLiquidity = underlying.balanceOf(address(aToken)); + } else if (collType == CollPluginType.COMPOUND_V2 || collType == CollPluginType.FLUX) { + ICToken cToken; + uint256 cTokenBal; + if (collType == CollPluginType.COMPOUND_V2) { + // CompoundV2 uses a vault to wrap the CToken + CTokenWrapper cTokenVault = CTokenWrapper(address(erc20)); + cToken = ICToken(address(cTokenVault.underlying())); + cTokenBal = cTokenVault.balanceOf(address(rToken.main().backingManager())); + } else { + // FLUX - Uses FToken directly (fork of CToken) + cToken = ICToken(address(erc20)); + cTokenBal = cToken.balanceOf(address(rToken.main().backingManager())); + } + IERC20 underlying = IERC20(cToken.underlying()); + + uint256 exchangeRate = cToken.exchangeRateStored(); + + backingBalance = (cTokenBal * exchangeRate) / 1e18; + availableLiquidity = underlying.balanceOf(address(cToken)); + } else if (collType == CollPluginType.COMPOUND_V3) { + ICusdcV3Wrapper cTokenV3Wrapper = ICusdcV3Wrapper(address(erc20)); + CometInterface cTokenV3 = CometInterface(address(cTokenV3Wrapper.underlyingComet())); + IERC20 underlying = IERC20(cTokenV3.baseToken()); + + backingBalance = cTokenV3Wrapper.underlyingBalanceOf( + address(rToken.main().backingManager()) + ); + availableLiquidity = underlying.balanceOf(address(cTokenV3)); + } else if (collType == CollPluginType.STARGATE) { + StargateRewardableWrapper stgWrapper = StargateRewardableWrapper(address(erc20)); + IStargatePool stgPool = stgWrapper.pool(); + + uint256 wstgBal = stgWrapper.balanceOf(address(rToken.main().backingManager())); + + backingBalance = stgPool.amountLPtoLD(wstgBal); + availableLiquidity = stgPool.totalLiquidity(); + } + + if (availableLiquidity == 0) { + return 0; // Avoid division by zero + } + + if (availableLiquidity >= backingBalance) { + return FIX_ONE_256; + } + + // Calculate the percentage + return (availableLiquidity * FIX_ONE_256) / backingBalance; + } + + // solhint-disable-next-line no-empty-blocks + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} +} diff --git a/contracts/facade/FacadeRead.sol b/contracts/facade/FacadeRead.sol index eed25706aa..2e2ce936e0 100644 --- a/contracts/facade/FacadeRead.sol +++ b/contracts/facade/FacadeRead.sol @@ -34,7 +34,6 @@ contract FacadeRead is IFacadeRead { // Poke Main main.assetRegistry().refresh(); - main.furnace().melt(); // {BU} BasketRange memory basketsHeld = main.basketHandler().basketsHeldBy(account); @@ -75,7 +74,6 @@ contract FacadeRead is IFacadeRead { // Poke Main reg.refresh(); - main.furnace().melt(); // Compute # of baskets to create `amount` qRTok uint192 baskets = (rTok.totalSupply() > 0) // {BU} @@ -121,7 +119,6 @@ contract FacadeRead is IFacadeRead { // Poke Main main.assetRegistry().refresh(); - main.furnace().melt(); uint256 supply = rTok.totalSupply(); @@ -203,7 +200,7 @@ contract FacadeRead is IFacadeRead { IBasketHandler basketHandler = rToken.main().basketHandler(); // solhint-disable-next-line no-empty-blocks - try rToken.main().furnace().melt() {} catch {} + try rToken.main().furnace().melt() {} catch {} // <3.1.0 RTokens may revert while frozen (erc20s, deposits) = basketHandler.quote(FIX_ONE, CEIL); @@ -242,7 +239,6 @@ contract FacadeRead is IFacadeRead { { IMain main = rToken.main(); main.assetRegistry().refresh(); - main.furnace().melt(); erc20s = main.assetRegistry().erc20s(); balances = new uint256[](erc20s.length); @@ -270,25 +266,26 @@ contract FacadeRead is IFacadeRead { // === Views === + /// @param draftEra {draftEra} The draft era to query unstakings for /// @param account The account for the query - /// @return unstakings All the pending StRSR unstakings for an account - function pendingUnstakings(RTokenP1 rToken, address account) - external - view - returns (Pending[] memory unstakings) - { - StRSRP1Votes stRSR = StRSRP1Votes(address(rToken.main().stRSR())); - uint256 era = stRSR.currentEra(); - uint256 left = stRSR.firstRemainingDraft(era, account); - uint256 right = stRSR.draftQueueLen(era, account); + /// @dev Use stRSR.draftRate() to convert {qDrafts} to {qRSR} + /// @return unstakings {qDrafts} All the pending StRSR unstakings for an account, in drafts + function pendingUnstakings( + RTokenP1 rToken, + uint256 draftEra, + address account + ) external view returns (Pending[] memory unstakings) { + StRSRP1 stRSR = StRSRP1(address(rToken.main().stRSR())); + uint256 left = stRSR.firstRemainingDraft(draftEra, account); + uint256 right = stRSR.draftQueueLen(draftEra, account); unstakings = new Pending[](right - left); for (uint256 i = 0; i < right - left; i++) { - (uint192 drafts, uint64 availableAt) = stRSR.draftQueues(era, account, i + left); + (uint192 drafts, uint64 availableAt) = stRSR.draftQueues(draftEra, account, i + left); uint192 diff = drafts; if (i + left > 0) { - (uint192 prevDrafts, ) = stRSR.draftQueues(era, account, i + left - 1); + (uint192 prevDrafts, ) = stRSR.draftQueues(draftEra, account, i + left - 1); diff = drafts - prevDrafts; } diff --git a/contracts/facade/FacadeTest.sol b/contracts/facade/FacadeTest.sol index 512457c1b2..f95d350282 100644 --- a/contracts/facade/FacadeTest.sol +++ b/contracts/facade/FacadeTest.sol @@ -67,6 +67,7 @@ contract FacadeTest is IFacadeTest { erc20s ); try main.rsrTrader().manageTokens(rsrERC20s, rsrKinds) {} catch {} + try main.rsrTrader().distributeTokenToBuy() {} catch {} // Start exact RToken auctions (IERC20[] memory rTokenERC20s, TradeKind[] memory rTokenKinds) = traderERC20s( @@ -75,6 +76,7 @@ contract FacadeTest is IFacadeTest { erc20s ); try main.rTokenTrader().manageTokens(rTokenERC20s, rTokenKinds) {} catch {} + try main.rTokenTrader().distributeTokenToBuy() {} catch {} // solhint-enable no-empty-blocks } @@ -98,7 +100,6 @@ contract FacadeTest is IFacadeTest { // Poke Main reg.refresh(); - main.furnace().melt(); address backingManager = address(main.backingManager()); IERC20 rsr = main.rsr(); @@ -135,6 +136,7 @@ contract FacadeTest is IFacadeTest { IERC20[] memory traderERC20sAll = new IERC20[](erc20sAll.length); for (uint256 i = 0; i < erc20sAll.length; ++i) { if ( + erc20sAll[i] != trader.tokenToBuy() && address(trader.trades(erc20sAll[i])) == address(0) && erc20sAll[i].balanceOf(address(trader)) > 1 ) { diff --git a/contracts/interfaces/IAsset.sol b/contracts/interfaces/IAsset.sol index bd796190a7..8126aa12ce 100644 --- a/contracts/interfaces/IAsset.sol +++ b/contracts/interfaces/IAsset.sol @@ -27,12 +27,14 @@ interface IAsset is IRewardable { function refresh() external; /// Should not revert + /// low should be nonzero if the asset could be worth selling /// @return low {UoA/tok} The lower end of the price estimate /// @return high {UoA/tok} The upper end of the price estimate function price() external view returns (uint192 low, uint192 high); /// Should not revert /// lotLow should be nonzero when the asset might be worth selling + /// @dev Deprecated. Phased out in 3.1.0, but left on interface for backwards compatibility /// @return lotLow {UoA/tok} The lower end of the lot price estimate /// @return lotHigh {UoA/tok} The upper end of the lot price estimate function lotPrice() external view returns (uint192 lotLow, uint192 lotHigh); @@ -67,8 +69,14 @@ interface TestIAsset is IAsset { /// @return {s} Seconds that an oracle value is considered valid function oracleTimeout() external view returns (uint48); - /// @return {s} Seconds that the lotPrice should decay over, after stale price + /// @return {s} Seconds that the price() should decay over, after stale price function priceTimeout() external view returns (uint48); + + /// @return {UoA/tok} The last saved low price + function savedLowPrice() external view returns (uint192); + + /// @return {UoA/tok} The last saved high price + function savedHighPrice() external view returns (uint192); } /// CollateralStatus must obey a linear ordering. That is: diff --git a/contracts/interfaces/IAssetRegistry.sol b/contracts/interfaces/IAssetRegistry.sol index caeaac2f3e..add18d69b5 100644 --- a/contracts/interfaces/IAssetRegistry.sol +++ b/contracts/interfaces/IAssetRegistry.sol @@ -34,7 +34,7 @@ interface IAssetRegistry is IComponent { function init(IMain main_, IAsset[] memory assets_) external; /// Fully refresh all asset state - /// @custom:interaction + /// @custom:refresher function refresh() external; /// Register `asset` diff --git a/contracts/interfaces/IBackingManager.sol b/contracts/interfaces/IBackingManager.sol index 0699da6d6c..b9b3c5beca 100644 --- a/contracts/interfaces/IBackingManager.sol +++ b/contracts/interfaces/IBackingManager.sol @@ -2,10 +2,38 @@ pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./IAssetRegistry.sol"; +import "./IBasketHandler.sol"; import "./IBroker.sol"; import "./IComponent.sol"; +import "./IRToken.sol"; +import "./IStRSR.sol"; import "./ITrading.sol"; +/// Memory struct for RecollateralizationLibP1 + RTokenAsset +/// Struct purposes: +/// 1. Configure trading +/// 2. Stay under stack limit with fewer vars +/// 3. Cache information such as component addresses and basket quantities, to save on gas +struct TradingContext { + BasketRange basketsHeld; // {BU} + // basketsHeld.top is the number of partial baskets units held + // basketsHeld.bottom is the number of full basket units held + + // Components + IBasketHandler bh; + IAssetRegistry ar; + IStRSR stRSR; + IERC20 rsr; + IRToken rToken; + // Gov Vars + uint192 minTradeVolume; // {UoA} + uint192 maxTradeSlippage; // {1} + // Cached values + uint192[] quantities; // {tok/BU} basket quantities + uint192[] bals; // {tok} balances in BackingManager + out on trades +} + /** * @title IBackingManager * @notice The BackingManager handles changes in the ERC20 balances that back an RToken. @@ -48,6 +76,15 @@ interface IBackingManager is IComponent, ITrading { /// @param erc20s The tokens to forward /// @custom:interaction RCEI function forwardRevenue(IERC20[] calldata erc20s) external; + + /// Structs for trading + /// @param basketsHeld The number of baskets held by the BackingManager + /// @return ctx The TradingContext + /// @return reg Contents of AssetRegistry.getRegistry() + function tradingContext(BasketRange memory basketsHeld) + external + view + returns (TradingContext memory ctx, Registry memory reg); } interface TestIBackingManager is IBackingManager, TestITrading { diff --git a/contracts/interfaces/IBasketHandler.sol b/contracts/interfaces/IBasketHandler.sol index 42bb8bf092..2ed829d1b9 100644 --- a/contracts/interfaces/IBasketHandler.sol +++ b/contracts/interfaces/IBasketHandler.sol @@ -133,12 +133,14 @@ interface IBasketHandler is IComponent { function basketsHeldBy(address account) external view returns (BasketRange memory); /// Should not revert + /// low should be nonzero when BUs are worth selling /// @return low {UoA/BU} The lower end of the price estimate /// @return high {UoA/BU} The upper end of the price estimate function price() external view returns (uint192 low, uint192 high); /// Should not revert /// lotLow should be nonzero if a BU could be worth selling + /// @dev Deprecated. Phased out in 3.1.0, but left on interface for backwards compatibility /// @return lotLow {UoA/tok} The lower end of the lot price estimate /// @return lotHigh {UoA/tok} The upper end of the lot price estimate function lotPrice() external view returns (uint192 lotLow, uint192 lotHigh); diff --git a/contracts/interfaces/IBroker.sol b/contracts/interfaces/IBroker.sol index 20e2ed0cb0..fcaeac2c10 100644 --- a/contracts/interfaces/IBroker.sol +++ b/contracts/interfaces/IBroker.sol @@ -11,7 +11,7 @@ enum TradeKind { BATCH_AUCTION } -/// Cache of all (lot) prices for a pair to prevent re-lookup +/// Cache of all prices for a pair to prevent re-lookup struct TradePrices { uint192 sellLow; // {UoA/sellTok} can be 0 uint192 sellHigh; // {UoA/sellTok} should not be 0 diff --git a/contracts/interfaces/IFacadeMonitor.sol b/contracts/interfaces/IFacadeMonitor.sol new file mode 100644 index 0000000000..6c4f6f8d2d --- /dev/null +++ b/contracts/interfaces/IFacadeMonitor.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "./IRToken.sol"; + +/** + * @title IFacadeMonitor + * @notice A monitoring layer for RTokens + */ + +/// PluginType +enum CollPluginType { + AAVE_V2, + AAVE_V3, + COMPOUND_V2, + COMPOUND_V3, + STARGATE, + FLUX, + MORPHO_AAVE_V2 +} + +/** + * @title MonitorParams + * @notice The set of protocol params needed for the required calculations + * Should be defined at deployment based on network + */ + +// solhint-disable var-name-mixedcase +struct MonitorParams { + // === AAVE_V2=== + address AAVE_V2_DATA_PROVIDER_ADDR; +} + +interface IFacadeMonitor { + // === Views === + function batchAuctionsDisabled(IRToken rToken) external view returns (bool); + + function dutchAuctionsDisabled(IRToken rToken) external view returns (bool); + + function issuanceAvailable(IRToken rToken) external view returns (uint256); + + function redemptionAvailable(IRToken rToken) external view returns (uint256); + + function backingReedemable( + IRToken rToken, + CollPluginType collType, + IERC20 erc20 + ) external view returns (uint256); +} diff --git a/contracts/interfaces/IFacadeRead.sol b/contracts/interfaces/IFacadeRead.sol index 44af758dec..df5f039d64 100644 --- a/contracts/interfaces/IFacadeRead.sol +++ b/contracts/interfaces/IFacadeRead.sol @@ -85,12 +85,15 @@ interface IFacadeRead { uint256 amount; } + /// @param draftEra {draftEra} The draft era to query unstakings for /// @param account The account for the query - /// @return All the pending StRSR unstakings for an account - function pendingUnstakings(RTokenP1 rToken, address account) - external - view - returns (Pending[] memory); + /// @dev Use stRSR.draftRate() to convert {qDrafts} to {qRSR} + /// @return {qDrafts} All the pending unstakings for an account, in drafts + function pendingUnstakings( + RTokenP1 rToken, + uint256 draftEra, + address account + ) external view returns (Pending[] memory); /// Returns the prime basket /// @dev Indices are shared across return values diff --git a/contracts/interfaces/ITrade.sol b/contracts/interfaces/ITrade.sol index d05e3028f6..f9e95114f9 100644 --- a/contracts/interfaces/ITrade.sol +++ b/contracts/interfaces/ITrade.sol @@ -27,6 +27,9 @@ interface ITrade { function buy() external view returns (IERC20Metadata); + /// @return {tok} The sell amount of the trade, in whole tokens + function sellAmount() external view returns (uint192); + /// @return The timestamp at which the trade is projected to become settle-able function endTime() external view returns (uint48); diff --git a/contracts/mixins/Versioned.sol b/contracts/mixins/Versioned.sol index 7518551125..c70c7a8857 100644 --- a/contracts/mixins/Versioned.sol +++ b/contracts/mixins/Versioned.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.19; import "../interfaces/IVersioned.sol"; // This value should be updated on each release -string constant VERSION = "3.0.1"; +string constant VERSION = "3.1.0"; /** * @title Versioned diff --git a/contracts/p0/BackingManager.sol b/contracts/p0/BackingManager.sol index c22df732c7..34a28ce66a 100644 --- a/contracts/p0/BackingManager.sol +++ b/contracts/p0/BackingManager.sol @@ -32,6 +32,8 @@ contract BackingManagerP0 is TradingP0, IBackingManager { mapping(TradeKind => uint48) private tradeEnd; // {s} last endTime() of an auction per kind + mapping(IERC20 => uint192) private tokensOut; // {tok} token balances out in ITrades + constructor() { ONE_BLOCK = NetworkConfigLib.blocktime(); } @@ -69,6 +71,7 @@ contract BackingManagerP0 is TradingP0, IBackingManager { returns (ITrade trade) { trade = super.settleTrade(sell); + delete tokensOut[trade.sell()]; // if the settler is the trade contract itself, try chaining with another rebalance() if (_msgSender() == address(trade)) { @@ -86,7 +89,6 @@ contract BackingManagerP0 is TradingP0, IBackingManager { /// @custom:interaction function rebalance(TradeKind kind) external notTradingPausedOrFrozen { main.assetRegistry().refresh(); - main.furnace().melt(); // DoS prevention: unless caller is self, require 1 empty block between like-kind auctions require( @@ -135,7 +137,8 @@ contract BackingManagerP0 is TradingP0, IBackingManager { // Execute Trade ITrade trade = tryTrade(kind, req, prices); - tradeEnd[kind] = trade.endTime(); + tradeEnd[kind] = trade.endTime(); // {s} + tokensOut[trade.sell()] = trade.sellAmount(); // {tok} } else { // Haircut time compromiseBasketsNeeded(basketsHeld.bottom); @@ -149,7 +152,6 @@ contract BackingManagerP0 is TradingP0, IBackingManager { require(ArrayLib.allUnique(erc20s), "duplicate tokens"); main.assetRegistry().refresh(); - main.furnace().melt(); require(tradesOpen == 0, "trade open"); require(main.basketHandler().isReady(), "basket not ready"); @@ -166,16 +168,20 @@ contract BackingManagerP0 is TradingP0, IBackingManager { uint256 rsrBal = main.rsr().balanceOf(address(this)); if (rsrBal > 0) { main.rsr().safeTransfer(address(main.stRSR()), rsrBal); + main.stRSR().payoutRewards(); } // Mint revenue RToken // Keep backingBuffer worth of collateral before recognizing revenue - uint192 needed = main.rToken().basketsNeeded().mul(FIX_ONE.plus(backingBuffer)); // {BU} - if (basketsHeld.bottom.gt(needed)) { - main.rToken().mint(basketsHeld.bottom.minus(needed)); - needed = main.rToken().basketsNeeded().mul(FIX_ONE.plus(backingBuffer)); // keep buffer + { + uint192 baskets = (basketsHeld.bottom.div(FIX_ONE + backingBuffer)); + if (baskets > main.rToken().basketsNeeded()) { + main.rToken().mint(baskets - main.rToken().basketsNeeded()); + } } + uint192 needed = main.rToken().basketsNeeded().mul(FIX_ONE.plus(backingBuffer)); // {BU} + // Handout excess assets above what is needed, including any newly minted RToken RevenueTotals memory totals = main.distributor().totals(); for (uint256 i = 0; i < erc20s.length; i++) { @@ -203,6 +209,40 @@ contract BackingManagerP0 is TradingP0, IBackingManager { } } + // === View === + + /// Structs for trading + /// @param basketsHeld The number of baskets held by the BackingManager + /// @return ctx The TradingContext + /// @return reg Contents of AssetRegistry.getRegistry() + function tradingContext(BasketRange memory basketsHeld) + public + view + returns (TradingContext memory ctx, Registry memory reg) + { + reg = main.assetRegistry().getRegistry(); + + ctx.basketsHeld = basketsHeld; + ctx.bh = main.basketHandler(); + ctx.ar = main.assetRegistry(); + ctx.stRSR = main.stRSR(); + ctx.rsr = main.rsr(); + ctx.rToken = main.rToken(); + ctx.minTradeVolume = minTradeVolume; + ctx.maxTradeSlippage = maxTradeSlippage; + ctx.quantities = new uint192[](reg.erc20s.length); + for (uint256 i = 0; i < reg.erc20s.length; ++i) { + ctx.quantities[i] = ctx.bh.quantity(reg.erc20s[i]); + } + ctx.bals = new uint192[](reg.erc20s.length); + for (uint256 i = 0; i < reg.erc20s.length; ++i) { + ctx.bals[i] = reg.assets[i].bal(address(this)) + tokensOut[reg.erc20s[i]]; + + // include StRSR's balance for RSR + if (reg.erc20s[i] == ctx.rsr) ctx.bals[i] += reg.assets[i].bal(address(ctx.stRSR)); + } + } + // === Private === /// Compromise on how many baskets are needed in order to recollateralize-by-accounting diff --git a/contracts/p0/BasketHandler.sol b/contracts/p0/BasketHandler.sol index 357b0a7251..998c25e65f 100644 --- a/contracts/p0/BasketHandler.sol +++ b/contracts/p0/BasketHandler.sol @@ -183,6 +183,8 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { } emit BasketSet(nonce, basket.erc20s, refAmts, true); disabled = true; + + trackStatus(); } /// Switch the basket, only callable directly by governance or after a default @@ -199,7 +201,7 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { require( main.hasRole(OWNER, _msgSender()) || - (status() == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), + (lastStatus == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), "basket unrefreshable" ); _switchBasket(); @@ -377,6 +379,7 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { /// Should not revert /// lowLow should be nonzero when the asset might be worth selling + /// @dev Deprecated. Phased out in 3.1.0, but left on interface for backwards compatibility /// @return lotLow {UoA/BU} The lower end of the lot price estimate /// @return lotHigh {UoA/BU} The upper end of the lot price estimate // returns sum(quantity(erc20) * lotPrice(erc20) for erc20 in basket.erc20s) diff --git a/contracts/p0/Broker.sol b/contracts/p0/Broker.sol index 02b88de2f3..b53c2e0417 100644 --- a/contracts/p0/Broker.sol +++ b/contracts/p0/Broker.sol @@ -92,7 +92,7 @@ contract BrokerP0 is ComponentP0, IBroker { /// Disable the broker until re-enabled by governance /// @custom:protected - function reportViolation() external notTradingPausedOrFrozen { + function reportViolation() external { require(trades[_msgSender()], "unrecognized trade contract"); ITrade trade = ITrade(_msgSender()); TradeKind kind = trade.KIND(); @@ -239,6 +239,11 @@ contract BrokerP0 is ComponentP0, IBroker { "dutch auctions disabled for token pair" ); require(dutchAuctionLength > 0, "dutch auctions not enabled"); + require( + priceNotDecayed(req.sell) && priceNotDecayed(req.buy), + "dutch auctions require live prices" + ); + DutchTrade trade = DutchTrade(Clones.clone(address(dutchTradeImplementation))); trades[address(trade)] = true; @@ -251,4 +256,10 @@ contract BrokerP0 is ComponentP0, IBroker { trade.init(caller, req.sell, req.buy, req.sellAmount, dutchAuctionLength, prices); return trade; } + + /// @return true iff the price is not decayed, or it's the RTokenAsset + function priceNotDecayed(IAsset asset) private view returns (bool) { + return + asset.lastSave() == block.timestamp || address(asset.erc20()) == address(main.rToken()); + } } diff --git a/contracts/p0/Distributor.sol b/contracts/p0/Distributor.sol index d305e9b521..264d7bfe7e 100644 --- a/contracts/p0/Distributor.sol +++ b/contracts/p0/Distributor.sol @@ -33,6 +33,11 @@ contract DistributorP0 is ComponentP0, IDistributor { /// Set the RevenueShare for destination `dest`. Destinations `FURNACE` and `ST_RSR` refer to /// main.furnace() and main.stRSR(). function setDistribution(address dest, RevenueShare memory share) external governance { + // solhint-disable-next-line no-empty-blocks + try main.rsrTrader().distributeTokenToBuy() {} catch {} + // solhint-disable-next-line no-empty-blocks + try main.rTokenTrader().distributeTokenToBuy() {} catch {} + _setDistribution(dest, share); RevenueTotals memory revTotals = totals(); _ensureNonZeroDistribution(revTotals.rTokenTotal, revTotals.rsrTotal); @@ -58,13 +63,15 @@ contract DistributorP0 is ComponentP0, IDistributor { { RevenueTotals memory revTotals = totals(); uint256 totalShares = isRSR ? revTotals.rsrTotal : revTotals.rTokenTotal; - require(totalShares > 0, "nothing to distribute"); - tokensPerShare = amount / totalShares; + if (totalShares > 0) tokensPerShare = amount / totalShares; + require(tokensPerShare > 0, "nothing to distribute"); } // Evenly distribute revenue tokens per distribution share. // This rounds "early", and that's deliberate! + bool accountRewards = false; + for (uint256 i = 0; i < destinations.length(); i++) { address addrTo = destinations.at(i); @@ -76,12 +83,23 @@ contract DistributorP0 is ComponentP0, IDistributor { if (addrTo == FURNACE) { addrTo = address(main.furnace()); + if (transferAmt > 0) accountRewards = true; } else if (addrTo == ST_RSR) { addrTo = address(main.stRSR()); + if (transferAmt > 0) accountRewards = true; } erc20.safeTransferFrom(_msgSender(), addrTo, transferAmt); } emit RevenueDistributed(erc20, _msgSender(), amount); + + // Perform reward accounting + if (accountRewards) { + if (isRSR) { + main.stRSR().payoutRewards(); + } else { + main.furnace().melt(); + } + } } /// Returns the rsr + rToken shareTotals diff --git a/contracts/p0/Furnace.sol b/contracts/p0/Furnace.sol index aa99a8140c..ea0a404a2e 100644 --- a/contracts/p0/Furnace.sol +++ b/contracts/p0/Furnace.sol @@ -36,7 +36,7 @@ contract FurnaceP0 is ComponentP0, IFurnace { /// Performs any melting that has vested since last call. /// @custom:refresher - function melt() public notFrozen { + function melt() public { if (uint48(block.timestamp) < uint64(lastPayout) + PERIOD) return; // # of whole periods that have passed since lastPayout @@ -58,15 +58,9 @@ contract FurnaceP0 is ComponentP0, IFurnace { /// Ratio setting /// @custom:governance function setRatio(uint192 ratio_) public governance { - if (lastPayout > 0) { - // solhint-disable-next-line no-empty-blocks - try this.melt() {} catch { - uint48 numPeriods = uint48((block.timestamp) - lastPayout) / PERIOD; - lastPayout += numPeriods * PERIOD; - lastPayoutBal = main.rToken().balanceOf(address(this)); - } - } require(ratio_ <= MAX_RATIO, "invalid ratio"); + melt(); // cannot revert + // The ratio can safely be set to 0, though it is not recommended emit RatioSet(ratio, ratio_); ratio = ratio_; diff --git a/contracts/p0/Main.sol b/contracts/p0/Main.sol index 9493b72c5c..1859ad8ecb 100644 --- a/contracts/p0/Main.sol +++ b/contracts/p0/Main.sol @@ -37,8 +37,7 @@ contract MainP0 is Versioned, Initializable, Auth, ComponentRegistry, IMain { /// @custom:refresher function poke() external { - assetRegistry.refresh(); - if (!frozen()) furnace.melt(); + assetRegistry.refresh(); // runs furnace.melt() stRSR.payoutRewards(); // NOT basketHandler.refreshBasket } diff --git a/contracts/p0/RevenueTrader.sol b/contracts/p0/RevenueTrader.sol index a62cf8bd18..8fecf2eb76 100644 --- a/contracts/p0/RevenueTrader.sol +++ b/contracts/p0/RevenueTrader.sol @@ -32,14 +32,14 @@ contract RevenueTraderP0 is TradingP0, IRevenueTrader { /// @param sell The sell token in the trade /// @return trade The ITrade contract settled /// @custom:interaction - function settleTrade(IERC20 sell) - public - override(ITrading, TradingP0) - notTradingPausedOrFrozen - returns (ITrade trade) - { + function settleTrade(IERC20 sell) public override(ITrading, TradingP0) returns (ITrade trade) { trade = super.settleTrade(sell); - _distributeTokenToBuy(); + + // solhint-disable-next-line no-empty-blocks + try this.distributeTokenToBuy() {} catch (bytes memory errData) { + // see: docs/solidity-style.md#Catching-Empty-Data + if (errData.length == 0) revert(); // solhint-disable-line reason-string + } // unlike BackingManager, do _not_ chain trades; b2b trades of the same token are unlikely } @@ -80,10 +80,18 @@ contract RevenueTraderP0 is TradingP0, IRevenueTrader { { require(erc20s.length > 0, "empty erc20s list"); require(erc20s.length == kinds.length, "length mismatch"); + + RevenueTotals memory revTotals = main.distributor().totals(); + require( + (tokenToBuy == main.rsr() && revTotals.rsrTotal > 0) || + (address(tokenToBuy) == address(main.rToken()) && revTotals.rTokenTotal > 0), + "zero distribution" + ); + main.assetRegistry().refresh(); IAsset assetToBuy = main.assetRegistry().toAsset(tokenToBuy); - (uint192 buyLow, uint192 buyHigh) = assetToBuy.lotPrice(); // {UoA/tok} + (uint192 buyLow, uint192 buyHigh) = assetToBuy.price(); // {UoA/tok} require(buyHigh > 0 && buyHigh < FIX_MAX, "buy asset price unknown"); // For each ERC20: start auction of given kind @@ -99,7 +107,7 @@ contract RevenueTraderP0 is TradingP0, IRevenueTrader { require(address(trades[erc20]) == address(0), "trade open"); require(erc20.balanceOf(address(this)) > 0, "0 balance"); - (uint192 sellLow, uint192 sellHigh) = assetToSell.lotPrice(); // {UoA/tok} + (uint192 sellLow, uint192 sellHigh) = assetToSell.price(); // {UoA/tok} TradingLibP0.TradeInfo memory trade = TradingLibP0.TradeInfo({ sell: assetToSell, diff --git a/contracts/p0/StRSR.sol b/contracts/p0/StRSR.sol index fe3676dcef..a9a3e597ec 100644 --- a/contracts/p0/StRSR.sol +++ b/contracts/p0/StRSR.sol @@ -77,9 +77,11 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { // {qRSR} How much reward RSR was held the last time rewards were paid out uint256 internal rsrRewardsAtLastPayout; - // Era. If ever there's a total RSR wipeout, this is incremented - // This is only really here for equivalence with P1, which requires it + // Eras. These are only really here for equivalence with P1, which requires it + // If there's ever a total RSR wipeout to balances, this is incremented uint256 internal era; + // If there's ever a total RSR wipeout to pending withdrawals, this is incremented + uint256 internal draftEra; // The momentary stake/unstake rate is rsrBacking/totalStaked {RSR/stRSR} // That rate is locked in when slow unstaking *begins* @@ -136,6 +138,7 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { setRewardRatio(rewardRatio_); setWithdrawalLeak(withdrawalLeak_); era = 1; + draftEra = 1; } /// Assign reward payouts to the staker pool @@ -201,7 +204,7 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { uint256 lastAvailableAt = index > 0 ? withdrawals[account][index - 1].availableAt : 0; uint256 availableAt = Math.max(block.timestamp + unstakingDelay, lastAvailableAt); withdrawals[account].push(Withdrawal(account, rsrAmount, stakeAmount, availableAt)); - emit UnstakingStarted(index, era, account, rsrAmount, stakeAmount, availableAt); + emit UnstakingStarted(index, draftEra, account, rsrAmount, stakeAmount, availableAt); } /// Complete delayed staking for an account, up to but not including draft ID `endId` @@ -239,7 +242,7 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { require(bh.isReady(), "basket not ready"); // Execute accumulated withdrawals - emit UnstakingCompleted(start, i, era, account, total); + emit UnstakingCompleted(start, i, draftEra, account, total); main.rsr().safeTransfer(account, total); } @@ -280,7 +283,7 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { } // Execute accumulated withdrawals - emit UnstakingCancelled(start, i, era, account, total); + emit UnstakingCancelled(start, i, draftEra, account, total); uint256 stakeAmount = total; if (totalStaked > 0) stakeAmount = (total * totalStaked) / rsrBacking; @@ -335,6 +338,7 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { uint256 withdrawalRSRtoTake = (rsrBeingWithdrawn() * rsrAmount + (rsrBalance - 1)) / rsrBalance; if ( + withdrawalRSRtoTake == 0 || rsrBeingWithdrawn() - withdrawalRSRtoTake < MIN_EXCHANGE_RATE.mulu_toUint(stakeBeingWithdrawn()) ) { @@ -382,7 +386,8 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { address account = accounts.at(i); delete withdrawals[account]; } - emit AllUnstakingReset(era); + draftEra++; + emit AllUnstakingReset(draftEra); } /// @custom:governance diff --git a/contracts/p0/mixins/TradingLib.sol b/contracts/p0/mixins/TradingLib.sol index a71df6c027..6fe87988d1 100644 --- a/contracts/p0/mixins/TradingLib.sol +++ b/contracts/p0/mixins/TradingLib.sol @@ -60,7 +60,7 @@ library TradingLibP0 { ); // Cap sell amount - uint192 maxSell = maxTradeSize(trade.sell, trade.buy, trade.prices.sellHigh); // {sellTok} + uint192 maxSell = maxTradeSize(trade.sell, trade.buy, trade.prices.sellLow); // {sellTok} uint192 s = trade.sellAmount > maxSell ? maxSell : trade.sellAmount; // {sellTok} // Calculate equivalent buyAmount within [0, FIX_MAX] @@ -145,7 +145,7 @@ library TradingLibP0 { /// 2. Stay under stack limit with fewer vars /// 3. Cache information such as component addresses to save on gas - struct TradingContext { + struct TradingContextP0 { BasketRange basketsHeld; // {BU} // basketsHeld.top is the number of partial baskets units held // basketsHeld.bottom is the number of full basket units held @@ -190,7 +190,7 @@ library TradingLibP0 { // === Prepare cached values === IMain main = bm.main(); - TradingContext memory ctx = TradingContext({ + TradingContextP0 memory ctx = TradingContextP0({ basketsHeld: basketsHeld, bm: bm, bh: main.basketHandler(), @@ -241,14 +241,9 @@ library TradingLibP0 { // token balances requiring trading vs not requiring trading. Seek to decrease uncertainty // the largest amount possible with each trade. // - // How do we know this algorithm converges? - // Assumption: constant oracle prices; monotonically increasing refPerTok() - // Any volume traded narrows the BU band. Why: - // - We might increase `basketsHeld.bottom` from run-to-run, but will never decrease it - // - We might decrease the UoA amount of excess balances beyond `basketsHeld.bottom` from - // run-to-run, but will never increase it - // - We might decrease the UoA amount of missing balances up-to `basketsHeld.top` from - // run-to-run, but will never increase it + // Algorithm Invariant: every increase of basketsHeld.bottom causes basketsRange().low to + // reach a new maximum. Note that basketRange().low may decrease slightly along the way. + // Assumptions: constant oracle prices; monotonically increasing refPerTok; no supply changes // // Preconditions: // - ctx is correctly populated, with current basketsHeld.bottom + basketsHeld.top @@ -269,12 +264,12 @@ library TradingLibP0 { // - range.bottom = min(rToken.basketsNeeded, basketsHeld.bottom + least baskets purchaseable) // where "least baskets purchaseable" involves trading at the worst price, // incurring the full maxTradeSlippage, and taking up to a minTradeVolume loss due to dust. - function basketRange(TradingContext memory ctx, IERC20[] memory erc20s) + function basketRange(TradingContextP0 memory ctx, IERC20[] memory erc20s) internal view returns (BasketRange memory range) { - (uint192 buPriceLow, uint192 buPriceHigh) = ctx.bh.lotPrice(); // {UoA/BU} + (uint192 buPriceLow, uint192 buPriceHigh) = ctx.bh.price(); // {UoA/BU} // Cap ctx.basketsHeld.top if (ctx.basketsHeld.top > ctx.rToken.basketsNeeded()) { @@ -303,21 +298,15 @@ library TradingLibP0 { bal = bal.plus(asset.bal(address(ctx.stRSR))); } - { - // Skip over dust-balance assets not in the basket - (uint192 lotLow, ) = asset.lotPrice(); // {UoA/tok} - - // Intentionally include value of IFFY/DISABLED collateral - if ( - ctx.bh.quantity(erc20s[i]) == 0 && - !isEnoughToSell(asset, bal, lotLow, ctx.minTradeVolume) - ) continue; - } - (uint192 low, uint192 high) = asset.price(); // {UoA/tok} - // price() is better than lotPrice() here: it's important to not underestimate how - // much value could be in a token that is unpriced by using a decaying high lotPrice. - // price() will return [0, FIX_MAX] in this case, which is preferable. + // low decays down; high decays up + + // Skip over dust-balance assets not in the basket + // Intentionally include value of IFFY/DISABLED collateral + if ( + ctx.bh.quantity(erc20s[i]) == 0 && + !isEnoughToSell(asset, bal, low, ctx.minTradeVolume) + ) continue; // throughout these sections +/- is same as Fix.plus/Fix.minus and is Fix.gt/.lt @@ -354,7 +343,7 @@ library TradingLibP0 { // (2) Lose minTradeVolume to dust (why: auctions can return tokens) // Q: Why is this precisely where we should take out minTradeVolume? - // A: Our use of isEnoughToSell always uses the low price (lotLow, technically), + // A: Our use of isEnoughToSell always uses the low price, // so min trade volumes are always assesed based on low prices. At this point // in the calculation we have already calculated the UoA amount corresponding to // the excess token balance based on its low price, so we are already set up @@ -434,10 +423,12 @@ library TradingLibP0 { // Sell IFFY last because it may recover value in the future. // All collateral in the basket have already been guaranteed to be SOUND by upstream checks. function nextTradePair( - TradingContext memory ctx, + TradingContextP0 memory ctx, IERC20[] memory erc20s, BasketRange memory range ) private view returns (TradeInfo memory trade) { + // assert(tradesOpen == 0); // guaranteed by BackingManager.rebalance() + MaxSurplusDeficit memory maxes; maxes.surplusStatus = CollateralStatus.IFFY; // least-desirable sell status @@ -453,19 +444,19 @@ library TradingLibP0 { // {tok} = {BU} * {tok/BU} uint192 needed = range.top.mul(ctx.bh.quantity(erc20s[i]), CEIL); // {tok} if (bal.gt(needed)) { - (uint192 lotLow, uint192 lotHigh) = asset.lotPrice(); // {UoA/sellTok} - if (lotHigh == 0) continue; // Skip worthless assets + (uint192 low, uint192 high) = asset.price(); // {UoA/sellTok} + if (high == 0) continue; // Skip worthless assets // by calculating this early we can duck the stack limit but be less gas-efficient bool enoughToSell = isEnoughToSell( asset, bal.minus(needed), - lotLow, + low, ctx.minTradeVolume ); // {UoA} = {sellTok} * {UoA/sellTok} - uint192 delta = bal.minus(needed).mul(lotLow, FLOOR); + uint192 delta = bal.minus(needed).mul(low, FLOOR); // status = asset.status() if asset.isCollateral() else SOUND CollateralStatus status; // starts SOUND @@ -476,8 +467,8 @@ library TradingLibP0 { if (isBetterSurplus(maxes, status, delta) && enoughToSell) { trade.sell = asset; trade.sellAmount = bal.minus(needed); - trade.prices.sellLow = lotLow; - trade.prices.sellHigh = lotHigh; + trade.prices.sellLow = low; + trade.prices.sellHigh = high; maxes.surplusStatus = status; maxes.surplus = delta; @@ -487,17 +478,17 @@ library TradingLibP0 { needed = range.bottom.mul(ctx.bh.quantity(erc20s[i]), CEIL); // {buyTok}; if (bal.lt(needed)) { uint192 amtShort = needed.minus(bal); // {buyTok} - (uint192 lotLow, uint192 lotHigh) = asset.lotPrice(); // {UoA/buyTok} + (uint192 low, uint192 high) = asset.price(); // {UoA/buyTok} // {UoA} = {buyTok} * {UoA/buyTok} - uint192 delta = amtShort.mul(lotHigh, CEIL); + uint192 delta = amtShort.mul(high, CEIL); // The best asset to buy is whichever asset has the largest deficit if (delta.gt(maxes.deficit)) { trade.buy = ICollateral(address(asset)); trade.buyAmount = amtShort; - trade.prices.buyLow = lotLow; - trade.prices.buyHigh = lotHigh; + trade.prices.buyLow = low; + trade.prices.buyHigh = high; maxes.deficit = delta; } @@ -512,13 +503,13 @@ library TradingLibP0 { uint192 rsrAvailable = rsrAsset.bal(address(ctx.bm)).plus( rsrAsset.bal(address(ctx.stRSR)) ); - (uint192 lotLow, uint192 lotHigh) = rsrAsset.lotPrice(); // {UoA/RSR} + (uint192 low, uint192 high) = rsrAsset.price(); // {UoA/RSR} - if (lotHigh > 0 && isEnoughToSell(rsrAsset, rsrAvailable, lotLow, ctx.minTradeVolume)) { + if (high > 0 && isEnoughToSell(rsrAsset, rsrAvailable, low, ctx.minTradeVolume)) { trade.sell = rsrAsset; trade.sellAmount = rsrAvailable; - trade.prices.sellLow = lotLow; - trade.prices.sellHigh = lotHigh; + trade.prices.sellLow = low; + trade.prices.sellHigh = high; } } } diff --git a/contracts/p1/AssetRegistry.sol b/contracts/p1/AssetRegistry.sol index 098e47f93a..c556a96120 100644 --- a/contracts/p1/AssetRegistry.sol +++ b/contracts/p1/AssetRegistry.sol @@ -57,6 +57,8 @@ contract AssetRegistryP1 is ComponentP1, IAssetRegistry { // tracks basket status on basketHandler function refresh() public { // It's a waste of gas to require notPausedOrFrozen because assets can be updated directly + // Assuming an RTokenAsset is registered, furnace.melt() will also be called + uint256 length = _erc20s.length(); for (uint256 i = 0; i < length; ++i) { assets[IERC20(_erc20s.at(i))].refresh(); diff --git a/contracts/p1/BackingManager.sol b/contracts/p1/BackingManager.sol index a64ff3f412..b7191aa097 100644 --- a/contracts/p1/BackingManager.sol +++ b/contracts/p1/BackingManager.sol @@ -45,6 +45,9 @@ contract BackingManagerP1 is TradingP1, IBackingManager { IFurnace private furnace; mapping(TradeKind => uint48) private tradeEnd; // {s} last endTime() of an auction per kind + // === 3.1.0 === + mapping(IERC20 => uint192) private tokensOut; // {tok} token balances out in ITrades + // ==== Invariants ==== // tradingDelay <= MAX_TRADING_DELAY and backingBuffer <= MAX_BACKING_BUFFER @@ -90,6 +93,7 @@ contract BackingManagerP1 is TradingP1, IBackingManager { /// @return trade The ITrade contract settled /// @custom:interaction function settleTrade(IERC20 sell) public override(ITrading, TradingP1) returns (ITrade trade) { + delete tokensOut[sell]; trade = super.settleTrade(sell); // nonReentrant // if the settler is the trade contract itself, try chaining with another rebalance() @@ -113,7 +117,6 @@ contract BackingManagerP1 is TradingP1, IBackingManager { function rebalance(TradeKind kind) external nonReentrant notTradingPausedOrFrozen { // == Refresh == assetRegistry.refresh(); - furnace.melt(); // DoS prevention: unless caller is self, require 1 empty block between like-kind auctions require( @@ -149,22 +152,26 @@ contract BackingManagerP1 is TradingP1, IBackingManager { * rToken.basketsNeeded to the current basket holdings. Haircut time. */ + (TradingContext memory ctx, Registry memory reg) = tradingContext(basketsHeld); ( bool doTrade, TradeRequest memory req, TradePrices memory prices - ) = RecollateralizationLibP1.prepareRecollateralizationTrade(this, basketsHeld); + ) = RecollateralizationLibP1.prepareRecollateralizationTrade(ctx, reg); if (doTrade) { + IERC20 sellERC20 = req.sell.erc20(); + // Seize RSR if needed - if (req.sell.erc20() == rsr) { - uint256 bal = req.sell.erc20().balanceOf(address(this)); + if (sellERC20 == rsr) { + uint256 bal = sellERC20.balanceOf(address(this)); if (req.sellAmount > bal) stRSR.seizeRSR(req.sellAmount - bal); } // Execute Trade ITrade trade = tryTrade(kind, req, prices); - tradeEnd[kind] = trade.endTime(); + tradeEnd[kind] = trade.endTime(); // {s} + tokensOut[sellERC20] = trade.sellAmount(); // {tok} } else { // Haircut time compromiseBasketsNeeded(basketsHeld.bottom); @@ -184,7 +191,6 @@ contract BackingManagerP1 is TradingP1, IBackingManager { require(ArrayLib.allUnique(erc20s), "duplicate tokens"); assetRegistry.refresh(); - furnace.melt(); BasketRange memory basketsHeld = basketHandler.basketsHeldBy(address(this)); @@ -212,20 +218,23 @@ contract BackingManagerP1 is TradingP1, IBackingManager { * RToken traders according to the distribution totals. */ - // Forward any RSR held to StRSR pool; RSR should never be sold for RToken yield + // Forward any RSR held to StRSR pool and payout rewards + // RSR should never be sold for RToken yield if (rsr.balanceOf(address(this)) > 0) { // For CEI, this is an interaction "within our system" even though RSR is already live IERC20(address(rsr)).safeTransfer(address(stRSR), rsr.balanceOf(address(this))); + stRSR.payoutRewards(); } // Mint revenue RToken // Keep backingBuffer worth of collateral before recognizing revenue - uint192 needed = rToken.basketsNeeded().mul(FIX_ONE + backingBuffer); // {BU} - if (basketsHeld.bottom > needed) { - rToken.mint(basketsHeld.bottom - needed); - needed = rToken.basketsNeeded().mul(FIX_ONE + backingBuffer); // keep buffer + uint192 baskets = (basketsHeld.bottom.div(FIX_ONE + backingBuffer)); + if (baskets > rToken.basketsNeeded()) { + rToken.mint(baskets - rToken.basketsNeeded()); } + uint192 needed = rToken.basketsNeeded().mul(FIX_ONE + backingBuffer); // {BU} + // At this point, even though basketsNeeded may have changed, we are: // - We're fully collateralized // - The BU exchange rate {BU/rTok} did not decrease @@ -245,6 +254,7 @@ contract BackingManagerP1 is TradingP1, IBackingManager { // delta: {qTok}, the excess quantity of this asset that we hold uint256 delta = bal.minus(req).shiftl_toUint(int8(asset.erc20Decimals())); uint256 tokensPerShare = delta / (totals.rTokenTotal + totals.rsrTotal); + if (tokensPerShare == 0) continue; // no div-by-0: Distributor guarantees (totals.rTokenTotal + totals.rsrTotal) > 0 // initial division is intentional here! We'd rather save the dust than be unfair @@ -263,6 +273,40 @@ contract BackingManagerP1 is TradingP1, IBackingManager { // It's okay if there is leftover dust for RToken or a surplus asset (not RSR) } + // === View === + + /// Structs for trading + /// @param basketsHeld The number of baskets held by the BackingManager + /// @return ctx The TradingContext + /// @return reg Contents of AssetRegistry.getRegistry() + function tradingContext(BasketRange memory basketsHeld) + public + view + returns (TradingContext memory ctx, Registry memory reg) + { + reg = assetRegistry.getRegistry(); + + ctx.basketsHeld = basketsHeld; + ctx.bh = basketHandler; + ctx.ar = assetRegistry; + ctx.stRSR = stRSR; + ctx.rsr = rsr; + ctx.rToken = rToken; + ctx.minTradeVolume = minTradeVolume; + ctx.maxTradeSlippage = maxTradeSlippage; + ctx.quantities = new uint192[](reg.erc20s.length); + for (uint256 i = 0; i < reg.erc20s.length; ++i) { + ctx.quantities[i] = basketHandler.quantityUnsafe(reg.erc20s[i], reg.assets[i]); + } + ctx.bals = new uint192[](reg.erc20s.length); + for (uint256 i = 0; i < reg.erc20s.length; ++i) { + ctx.bals[i] = reg.assets[i].bal(address(this)) + tokensOut[reg.erc20s[i]]; + + // include StRSR's balance for RSR + if (reg.erc20s[i] == rsr) ctx.bals[i] += reg.assets[i].bal(address(stRSR)); + } + } + // === Private === /// Compromise on how many baskets are needed in order to recollateralize-by-accounting @@ -307,5 +351,5 @@ contract BackingManagerP1 is TradingP1, IBackingManager { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[39] private __gap; + uint256[38] private __gap; } diff --git a/contracts/p1/BasketHandler.sol b/contracts/p1/BasketHandler.sol index 2cb493d1a3..fa076253bd 100644 --- a/contracts/p1/BasketHandler.sol +++ b/contracts/p1/BasketHandler.sol @@ -121,6 +121,8 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { for (uint256 i = 0; i < len; ++i) refAmts[i] = basket.refAmts[basket.erc20s[i]]; emit BasketSet(nonce, basket.erc20s, refAmts, true); disabled = true; + + trackStatus(); } /// Switch the basket, only callable directly by governance or after a default @@ -137,7 +139,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { require( main.hasRole(OWNER, _msgSender()) || - (status() == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), + (lastStatus == CollateralStatus.DISABLED && !main.tradingPausedOrFrozen()), "basket unrefreshable" ); _switchBasket(); @@ -318,6 +320,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { /// Should not revert /// lowLow should be nonzero when the asset might be worth selling + /// @dev Deprecated. Phased out in 3.1.0, but left on interface for backwards compatibility /// @return lotLow {UoA/BU} The lower end of the lot price estimate /// @return lotHigh {UoA/BU} The upper end of the lot price estimate // returns sum(quantity(erc20) * lotPrice(erc20) for erc20 in basket.erc20s) @@ -421,7 +424,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { for (uint256 k = 0; k < len; ++k) { if (b.erc20s[j] == erc20sAll[k]) { erc20Index = k; - continue; + break; } } diff --git a/contracts/p1/Broker.sol b/contracts/p1/Broker.sol index cfc6100a93..0111d25bc3 100644 --- a/contracts/p1/Broker.sol +++ b/contracts/p1/Broker.sol @@ -63,6 +63,10 @@ contract BrokerP1 is ComponentP1, IBroker { // Whether Dutch Auctions are currently disabled, per ERC20 mapping(IERC20Metadata => bool) public dutchTradeDisabled; + // === 3.1.0 === + + IRToken private rToken; + // ==== Invariant ==== // (trades[addr] == true) iff this contract has created an ITrade clone at addr @@ -81,10 +85,7 @@ contract BrokerP1 is ComponentP1, IBroker { uint48 dutchAuctionLength_ ) external initializer { __Component_init(main_); - - backingManager = main_.backingManager(); - rsrTrader = main_.rsrTrader(); - rTokenTrader = main_.rTokenTrader(); + cacheComponents(); setGnosis(gnosis_); setBatchTradeImplementation(batchTradeImplementation_); @@ -93,6 +94,14 @@ contract BrokerP1 is ComponentP1, IBroker { setDutchAuctionLength(dutchAuctionLength_); } + /// Call after upgrade to >= 3.1.0 + function cacheComponents() public { + backingManager = main.backingManager(); + rsrTrader = main.rsrTrader(); + rTokenTrader = main.rTokenTrader(); + rToken = main.rToken(); + } + /// Handle a trade request by deploying a customized disposable trading contract /// @param kind TradeKind.DUTCH_AUCTION or TradeKind.BATCH_AUCTION /// @dev Requires setting an allowance in advance @@ -127,9 +136,9 @@ contract BrokerP1 is ComponentP1, IBroker { /// Disable the broker until re-enabled by governance /// @custom:protected - // checks: not paused (trading), not frozen, caller is a Trade this contract cloned + // checks: caller is a Trade this contract cloned // effects: disabled' = true - function reportViolation() external notTradingPausedOrFrozen { + function reportViolation() external { require(trades[_msgSender()], "unrecognized trade contract"); ITrade trade = ITrade(_msgSender()); TradeKind kind = trade.KIND(); @@ -256,6 +265,11 @@ contract BrokerP1 is ComponentP1, IBroker { "dutch auctions disabled for token pair" ); require(dutchAuctionLength > 0, "dutch auctions not enabled"); + require( + priceNotDecayed(req.sell) && priceNotDecayed(req.buy), + "dutch auctions require live prices" + ); + DutchTrade trade = DutchTrade(address(dutchTradeImplementation).clone()); trades[address(trade)] = true; @@ -270,10 +284,15 @@ contract BrokerP1 is ComponentP1, IBroker { return trade; } + /// @return true iff the price is not decayed, or it's the RTokenAsset + function priceNotDecayed(IAsset asset) private view returns (bool) { + return asset.lastSave() == block.timestamp || address(asset.erc20()) == address(rToken); + } + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[42] private __gap; + uint256[41] private __gap; } diff --git a/contracts/p1/Distributor.sol b/contracts/p1/Distributor.sol index ca818f5a14..776e19fe5a 100644 --- a/contracts/p1/Distributor.sol +++ b/contracts/p1/Distributor.sol @@ -57,13 +57,17 @@ contract DistributorP1 is ComponentP1, IDistributor { // destinations' = destinations.add(dest) // distribution' = distribution.set(dest, share) function setDistribution(address dest, RevenueShare memory share) external governance { + // solhint-disable-next-line no-empty-blocks + try main.rsrTrader().distributeTokenToBuy() {} catch {} + // solhint-disable-next-line no-empty-blocks + try main.rTokenTrader().distributeTokenToBuy() {} catch {} + _setDistribution(dest, share); RevenueTotals memory revTotals = totals(); _ensureNonZeroDistribution(revTotals.rTokenTotal, revTotals.rsrTotal); } struct Transfer { - IERC20 erc20; address addrTo; uint256 amount; } @@ -94,8 +98,8 @@ contract DistributorP1 is ComponentP1, IDistributor { { RevenueTotals memory revTotals = totals(); uint256 totalShares = isRSR ? revTotals.rsrTotal : revTotals.rTokenTotal; - require(totalShares > 0, "nothing to distribute"); - tokensPerShare = amount / totalShares; + if (totalShares > 0) tokensPerShare = amount / totalShares; + require(tokensPerShare > 0, "nothing to distribute"); } // Evenly distribute revenue tokens per distribution share. @@ -107,6 +111,8 @@ contract DistributorP1 is ComponentP1, IDistributor { address furnaceAddr = furnace; // gas-saver address stRSRAddr = stRSR; // gas-saver + bool accountRewards = false; + for (uint256 i = 0; i < destinations.length(); ++i) { address addrTo = destinations.at(i); @@ -118,15 +124,13 @@ contract DistributorP1 is ComponentP1, IDistributor { if (addrTo == FURNACE) { addrTo = furnaceAddr; + if (transferAmt > 0) accountRewards = true; } else if (addrTo == ST_RSR) { addrTo = stRSRAddr; + if (transferAmt > 0) accountRewards = true; } - transfers[numTransfers] = Transfer({ - erc20: erc20, - addrTo: addrTo, - amount: transferAmt - }); + transfers[numTransfers] = Transfer({ addrTo: addrTo, amount: transferAmt }); numTransfers++; } emit RevenueDistributed(erc20, caller, amount); @@ -134,7 +138,16 @@ contract DistributorP1 is ComponentP1, IDistributor { // == Interactions == for (uint256 i = 0; i < numTransfers; i++) { Transfer memory t = transfers[i]; - IERC20Upgradeable(address(t.erc20)).safeTransferFrom(caller, t.addrTo, t.amount); + IERC20Upgradeable(address(erc20)).safeTransferFrom(caller, t.addrTo, t.amount); + } + + // Perform reward accounting + if (accountRewards) { + if (isRSR) { + main.stRSR().payoutRewards(); + } else { + main.furnace().melt(); + } } } diff --git a/contracts/p1/Furnace.sol b/contracts/p1/Furnace.sol index 923ba33737..63dcc695d4 100644 --- a/contracts/p1/Furnace.sol +++ b/contracts/p1/Furnace.sol @@ -71,7 +71,7 @@ contract FurnaceP1 is ComponentP1, IFurnace { // actions: // rToken.melt(payoutAmount), paying payoutAmount to RToken holders - function melt() external notFrozen { + function melt() public { if (uint48(block.timestamp) < uint64(lastPayout) + PERIOD) return; // # of whole periods that have passed since lastPayout @@ -90,15 +90,9 @@ contract FurnaceP1 is ComponentP1, IFurnace { /// Ratio setting /// @custom:governance function setRatio(uint192 ratio_) public governance { - if (lastPayout > 0) { - // solhint-disable-next-line no-empty-blocks - try this.melt() {} catch { - uint48 numPeriods = uint48((block.timestamp) - lastPayout) / PERIOD; - lastPayout += numPeriods * PERIOD; - lastPayoutBal = rToken.balanceOf(address(this)); - } - } require(ratio_ <= MAX_RATIO, "invalid ratio"); + melt(); // cannot revert + // The ratio can safely be set to 0 to turn off payouts, though it is not recommended emit RatioSet(ratio, ratio_); ratio = ratio_; diff --git a/contracts/p1/Main.sol b/contracts/p1/Main.sol index 43bddcaed7..21781ca082 100644 --- a/contracts/p1/Main.sol +++ b/contracts/p1/Main.sol @@ -43,10 +43,9 @@ contract MainP1 is Versioned, Initializable, Auth, ComponentRegistry, UUPSUpgrad /// @dev Not intended to be used in production, only for equivalence with P0 function poke() external { // == Refresher == - assetRegistry.refresh(); + assetRegistry.refresh(); // runs furnace.melt() // == CE block == - if (!frozen()) furnace.melt(); stRSR.payoutRewards(); } diff --git a/contracts/p1/RToken.sol b/contracts/p1/RToken.sol index 8b447d4273..1c07b650ef 100644 --- a/contracts/p1/RToken.sol +++ b/contracts/p1/RToken.sol @@ -108,7 +108,6 @@ contract RTokenP1 is ComponentP1, ERC20PermitUpgradeable, IRToken { // == Refresh == assetRegistry.refresh(); - furnace.melt(); // == Checks-effects block == @@ -182,8 +181,6 @@ contract RTokenP1 is ComponentP1, ERC20PermitUpgradeable, IRToken { function redeemTo(address recipient, uint256 amount) public notFrozen { // == Refresh == assetRegistry.refresh(); - // solhint-disable-next-line no-empty-blocks - try furnace.melt() {} catch {} // nice for the redeemer, but not necessary // == Checks and Effects == @@ -255,8 +252,6 @@ contract RTokenP1 is ComponentP1, ERC20PermitUpgradeable, IRToken { ) external notFrozen { // == Refresh == assetRegistry.refresh(); - // solhint-disable-next-line no-empty-blocks - try furnace.melt() {} catch {} // nice for the redeemer, but not necessary // == Checks and Effects == diff --git a/contracts/p1/RevenueTrader.sol b/contracts/p1/RevenueTrader.sol index 43253c6b0e..998bdc951f 100644 --- a/contracts/p1/RevenueTrader.sol +++ b/contracts/p1/RevenueTrader.sol @@ -53,7 +53,12 @@ contract RevenueTraderP1 is TradingP1, IRevenueTrader { /// @custom:interaction function settleTrade(IERC20 sell) public override(ITrading, TradingP1) returns (ITrade trade) { trade = super.settleTrade(sell); // nonReentrant - _distributeTokenToBuy(); + + // solhint-disable-next-line no-empty-blocks + try this.distributeTokenToBuy() {} catch (bytes memory errData) { + // see: docs/solidity-style.md#Catching-Empty-Data + if (errData.length == 0) revert(); // solhint-disable-line reason-string + } // unlike BackingManager, do _not_ chain trades; b2b trades of the same token are unlikely } @@ -107,6 +112,12 @@ contract RevenueTraderP1 is TradingP1, IRevenueTrader { uint256 len = erc20s.length; require(len > 0, "empty erc20s list"); require(len == kinds.length, "length mismatch"); + RevenueTotals memory revTotals = distributor.totals(); + require( + (tokenToBuy == rsr && revTotals.rsrTotal > 0) || + (address(tokenToBuy) == address(rToken) && revTotals.rTokenTotal > 0), + "zero distribution" + ); // Calculate if the trade involves any RToken // Distribute tokenToBuy if supplied in ERC20s list @@ -123,10 +134,8 @@ contract RevenueTraderP1 is TradingP1, IRevenueTrader { IAsset assetToBuy = assetRegistry.toAsset(tokenToBuy); // Refresh everything if RToken is involved - if (involvesRToken) { - assetRegistry.refresh(); - furnace.melt(); - } else { + if (involvesRToken) assetRegistry.refresh(); + else { // Otherwise: refresh just the needed assets and nothing more for (uint256 i = 0; i < len; ++i) { assetRegistry.toAsset(erc20s[i]).refresh(); @@ -135,7 +144,7 @@ contract RevenueTraderP1 is TradingP1, IRevenueTrader { } // Cache and validate buyHigh - (uint192 buyLow, uint192 buyHigh) = assetToBuy.lotPrice(); // {UoA/tok} + (uint192 buyLow, uint192 buyHigh) = assetToBuy.price(); // {UoA/tok} require(buyHigh > 0 && buyHigh < FIX_MAX, "buy asset price unknown"); // For each ERC20 that isn't the tokenToBuy, start an auction of the given kind @@ -147,7 +156,7 @@ contract RevenueTraderP1 is TradingP1, IRevenueTrader { require(erc20.balanceOf(address(this)) > 0, "0 balance"); IAsset assetToSell = assetRegistry.toAsset(erc20); - (uint192 sellLow, uint192 sellHigh) = assetToSell.lotPrice(); // {UoA/tok} + (uint192 sellLow, uint192 sellHigh) = assetToSell.price(); // {UoA/tok} TradeInfo memory trade = TradeInfo({ sell: assetToSell, diff --git a/contracts/p1/StRSR.sol b/contracts/p1/StRSR.sol index 527d63f50c..faff182759 100644 --- a/contracts/p1/StRSR.sol +++ b/contracts/p1/StRSR.sol @@ -76,15 +76,15 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // === Financial State: Drafts === // Era. If drafts get wiped out due to RSR seizure, increment the era to zero draft values. // Only ever directly written by beginDraftEra() - uint256 internal draftEra; + uint256 internal draftEra; // {draftEra} // Drafts: share of the withdrawing tokens. Not transferrable and not revenue-earning. struct CumulativeDraft { // Avoid re-using uint192 in order to avoid confusion with our type system; 176 is enough uint176 drafts; // Total amount of drafts that will become available // {qDrafts} uint64 availableAt; // When the last of the drafts will become available } - // draftEra => ({account} => {drafts}) - mapping(uint256 => mapping(address => CumulativeDraft[])) public draftQueues; // {drafts} + // {draftEra} => ({account} => {qDrafts}) + mapping(uint256 => mapping(address => CumulativeDraft[])) public draftQueues; // {qDrafts} mapping(uint256 => mapping(address => uint256)) public firstRemainingDraft; // draft index uint256 private totalDrafts; // Total of all drafts {qDrafts} uint256 private draftRSR; // Amount of RSR backing all drafts {qRSR} @@ -285,7 +285,7 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // Create draft (uint256 index, uint64 availableAt) = pushDraft(account, rsrAmount); - emit UnstakingStarted(index, era, account, rsrAmount, stakeAmount, availableAt); + emit UnstakingStarted(index, draftEra, account, rsrAmount, stakeAmount, availableAt); } /// Complete an account's unstaking; callable by anyone @@ -564,6 +564,11 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab return totalDrafts; } + /// @return {draftEra} The current era for drafts (withdrawals) + function getDraftEra() external view returns (uint256) { + return draftEra; + } + // ==== Internal Functions ==== /// Assign reward payouts to the staker pool diff --git a/contracts/p1/mixins/RecollateralizationLib.sol b/contracts/p1/mixins/RecollateralizationLib.sol index dd86e45ca1..8edb10f86c 100644 --- a/contracts/p1/mixins/RecollateralizationLib.sol +++ b/contracts/p1/mixins/RecollateralizationLib.sol @@ -8,29 +8,6 @@ import "../../interfaces/IBackingManager.sol"; import "../../libraries/Fixed.sol"; import "./TradeLib.sol"; -/// Struct purposes: -/// 1. Configure trading -/// 2. Stay under stack limit with fewer vars -/// 3. Cache information such as component addresses to save on gas -struct TradingContext { - BasketRange basketsHeld; // {BU} - // basketsHeld.top is the number of partial baskets units held - // basketsHeld.bottom is the number of full basket units held - - // Components - IBackingManager bm; - IBasketHandler bh; - IAssetRegistry ar; - IStRSR stRSR; - IERC20 rsr; - IRToken rToken; - // Gov Vars - uint192 minTradeVolume; // {UoA} - uint192 maxTradeSlippage; // {1} - // Cached values - uint192[] quantities; // {tok/BU} basket quantities -} - /** * @title RecollateralizationLibP1 * @notice An informal extension of BackingManager that implements the rebalancing logic @@ -56,7 +33,7 @@ library RecollateralizationLibP1 { // let trade = nextTradePair(...) // if trade.sell is not a defaulted collateral, prepareTradeToCoverDeficit(...) // otherwise, prepareTradeSell(...) taking the minBuyAmount as the dependent variable - function prepareRecollateralizationTrade(IBackingManager bm, BasketRange memory basketsHeld) + function prepareRecollateralizationTrade(TradingContext memory ctx, Registry memory reg) external view returns ( @@ -65,31 +42,8 @@ library RecollateralizationLibP1 { TradePrices memory prices ) { - IMain main = bm.main(); - - // === Prepare TradingContext cache === - TradingContext memory ctx; - - ctx.basketsHeld = basketsHeld; - ctx.bm = bm; - ctx.bh = main.basketHandler(); - ctx.ar = main.assetRegistry(); - ctx.stRSR = main.stRSR(); - ctx.rsr = main.rsr(); - ctx.rToken = main.rToken(); - ctx.minTradeVolume = bm.minTradeVolume(); - ctx.maxTradeSlippage = bm.maxTradeSlippage(); - - // Calculate quantities - Registry memory reg = ctx.ar.getRegistry(); - ctx.quantities = new uint192[](reg.erc20s.length); - for (uint256 i = 0; i < reg.erc20s.length; ++i) { - ctx.quantities[i] = ctx.bh.quantityUnsafe(reg.erc20s[i], reg.assets[i]); - } - - // ============================ - // Compute a target basket range for trading - {BU} + // The basket range is the full range of projected outcomes for the rebalancing process BasketRange memory range = basketRange(ctx, reg); // Select a pair to trade next, if one exists @@ -131,22 +85,17 @@ library RecollateralizationLibP1 { // token balances requiring trading vs not requiring trading. Seek to decrease uncertainty // the largest amount possible with each trade. // - // How do we know this algorithm converges? - // Assumption: constant oracle prices; monotonically increasing refPerTok() - // Any volume traded narrows the BU band. Why: - // - We might increase `basketsHeld.bottom` from run-to-run, but will never decrease it - // - We might decrease the UoA amount of excess balances beyond `basketsHeld.bottom` from - // run-to-run, but will never increase it - // - We might decrease the UoA amount of missing balances up-to `basketsHeld.top` from - // run-to-run, but will never increase it + // Algorithm Invariant: every increase of basketsHeld.bottom causes basketsRange().low to + // reach a new maximum. Note that basketRange().low may decrease slightly along the way. + // Assumptions: constant oracle prices; monotonically increasing refPerTok; no supply changes // // Preconditions: // - ctx is correctly populated, with current basketsHeld.bottom + basketsHeld.top // - reg contains erc20 + asset + quantities arrays in same order and without duplicates // Trading Strategy: // - We will not aim to hold more than rToken.basketsNeeded() BUs - // - No double trades: if we buy B in one trade, we won't sell B in another trade - // Caveat: Unless the asset we're selling is IFFY/DISABLED + // - No double trades: capital converted from token A to token B should not go to token C + // unless the clearing price was outside the expected price range // - The best price we might get for a trade is at the high sell price and low buy price // - The worst price we might get for a trade is at the low sell price and // the high buy price, multiplied by ( 1 - maxTradeSlippage ) @@ -164,7 +113,12 @@ library RecollateralizationLibP1 { view returns (BasketRange memory range) { - (uint192 buPriceLow, uint192 buPriceHigh) = ctx.bh.lotPrice(); // {UoA/BU} + // tradesOpen will be 0 when called by prepareRecollateralizationTrade() + // tradesOpen can be > 0 when called by RTokenAsset.basketRange() + + (uint192 buPriceLow, uint192 buPriceHigh) = ctx.bh.price(); // {UoA/BU} + require(buPriceLow > 0 && buPriceHigh < FIX_MAX, "BUs unpriced"); + uint192 basketsNeeded = ctx.rToken.basketsNeeded(); // {BU} // Cap ctx.basketsHeld.top @@ -189,28 +143,17 @@ library RecollateralizationLibP1 { // Exclude RToken balances to avoid double counting value if (reg.erc20s[i] == IERC20(address(ctx.rToken))) continue; - uint192 bal = reg.assets[i].bal(address(ctx.bm)); // {tok} - - // For RSR, include the staking balance - if (reg.erc20s[i] == ctx.rsr) { - bal = bal.plus(reg.assets[i].bal(address(ctx.stRSR))); - } - - if (ctx.quantities[i] == 0) { - // Skip over dust-balance assets not in the basket - (uint192 lotLow, ) = reg.assets[i].lotPrice(); // {UoA/tok} + (uint192 low, uint192 high) = reg.assets[i].price(); // {UoA/tok} - // Intentionally include value of IFFY/DISABLED collateral - if (!TradeLib.isEnoughToSell(reg.assets[i], bal, lotLow, ctx.minTradeVolume)) { - continue; - } + // Skip over dust-balance assets not in the basket + // Intentionally include value of IFFY/DISABLED collateral + if ( + ctx.quantities[i] == 0 && + !TradeLib.isEnoughToSell(reg.assets[i], ctx.bals[i], low, ctx.minTradeVolume) + ) { + continue; } - (uint192 low, uint192 high) = reg.assets[i].price(); // {UoA/tok} - // price() is better than lotPrice() here: it's important to not underestimate how - // much value could be in a token that is unpriced by using a decaying high lotPrice. - // price() will return [0, FIX_MAX] in this case, which is preferable. - // throughout these sections +/- is same as Fix.plus/Fix.minus and is Fix.gt/.lt // deltaTop: optimistic case @@ -220,17 +163,21 @@ library RecollateralizationLibP1 { // {tok} = {tok/BU} * {BU} uint192 anchor = ctx.quantities[i].mul(ctx.basketsHeld.top, CEIL); - if (anchor > bal) { + if (anchor > ctx.bals[i]) { // deficit: deduct optimistic estimate of baskets missing // {BU} = {UoA/tok} * {tok} / {UoA/BU} - deltaTop -= int256(uint256(low.mulDiv(anchor - bal, buPriceHigh, FLOOR))); + deltaTop -= int256( + uint256(low.mulDiv(anchor - ctx.bals[i], buPriceHigh, FLOOR)) + ); // does not need underflow protection: using low price of asset } else { // surplus: add-in optimistic estimate of baskets purchaseable // {BU} = {UoA/tok} * {tok} / {UoA/BU} - deltaTop += int256(uint256(high.safeMulDiv(bal - anchor, buPriceLow, CEIL))); + deltaTop += int256( + uint256(high.safeMulDiv(ctx.bals[i] - anchor, buPriceLow, CEIL)) + ); } } @@ -242,12 +189,12 @@ library RecollateralizationLibP1 { // (1) Sum token value at low price // {UoA} = {UoA/tok} * {tok} - uint192 val = low.mul(bal - anchor, FLOOR); + uint192 val = low.mul(ctx.bals[i] - anchor, FLOOR); // (2) Lose minTradeVolume to dust (why: auctions can return tokens) // Q: Why is this precisely where we should take out minTradeVolume? - // A: Our use of isEnoughToSell always uses the low price (lotLow, technically), - // so min trade volumes are always assesed based on low prices. At this point + // A: Our use of isEnoughToSell always uses the low price, + // so min trade volumes are always assessed based on low prices. At this point // in the calculation we have already calculated the UoA amount corresponding to // the excess token balance based on its low price, so we are already set up // to straightforwardly deduct the minTradeVolume before trying to buy BUs. @@ -305,9 +252,9 @@ library RecollateralizationLibP1 { /// prices.buyLow {UoA/buyTok} The best-case price of the buy token on secondary markets /// prices.buyHigh {UoA/buyTok} The worst-case price of the buy token on secondary markets /// - // Defining "sell" and "buy": - // If bal(e) > (quantity(e) * range.top), then e is in surplus by the difference - // If bal(e) < (quantity(e) * range.bottom), then e is in deficit by the difference + // For each asset e: + // If bal(e) > (quantity(e) * range.top), then e is in surplus by the difference + // If bal(e) < (quantity(e) * range.bottom), then e is in deficit by the difference // // First, ignoring RSR: // `trade.sell` is the token from erc20s with the greatest surplus value (in UoA), @@ -330,26 +277,33 @@ library RecollateralizationLibP1 { Registry memory reg, BasketRange memory range ) private view returns (TradeInfo memory trade) { + // assert(tradesOpen == 0); // guaranteed by BackingManager.rebalance() + MaxSurplusDeficit memory maxes; maxes.surplusStatus = CollateralStatus.IFFY; // least-desirable sell status + uint256 rsrIndex = reg.erc20s.length; // invalid index, to-start + // Iterate over non-RSR/non-RToken assets // (no space on the stack to cache erc20s.length) for (uint256 i = 0; i < reg.erc20s.length; ++i) { - if (reg.erc20s[i] == ctx.rsr || address(reg.erc20s[i]) == address(ctx.rToken)) continue; - - uint192 bal = reg.assets[i].bal(address(ctx.bm)); // {tok} + if (address(reg.erc20s[i]) == address(ctx.rToken)) continue; + else if (reg.erc20s[i] == ctx.rsr) { + rsrIndex = i; + continue; + } // {tok} = {BU} * {tok/BU} // needed(Top): token balance needed for range.top baskets: quantity(e) * range.top uint192 needed = range.top.mul(ctx.quantities[i], CEIL); // {tok} - if (bal.gt(needed)) { - (uint192 lotLow, uint192 lotHigh) = reg.assets[i].lotPrice(); // {UoA/sellTok} - if (lotHigh == 0) continue; // skip over worthless assets + if (ctx.bals[i].gt(needed)) { + (uint192 low, uint192 high) = reg.assets[i].price(); // {UoA/sellTok} + + if (high == 0) continue; // skip over worthless assets // {UoA} = {sellTok} * {UoA/sellTok} - uint192 delta = bal.minus(needed).mul(lotLow, FLOOR); + uint192 delta = ctx.bals[i].minus(needed).mul(low, FLOOR); // status = asset.status() if asset.isCollateral() else SOUND CollateralStatus status; // starts SOUND @@ -363,15 +317,15 @@ library RecollateralizationLibP1 { isBetterSurplus(maxes, status, delta) && TradeLib.isEnoughToSell( reg.assets[i], - bal.minus(needed), - lotLow, + ctx.bals[i].minus(needed), + low, ctx.minTradeVolume ) ) { trade.sell = reg.assets[i]; - trade.sellAmount = bal.minus(needed); - trade.prices.sellLow = lotLow; - trade.prices.sellHigh = lotHigh; + trade.sellAmount = ctx.bals[i].minus(needed); + trade.prices.sellLow = low; + trade.prices.sellHigh = high; maxes.surplusStatus = status; maxes.surplus = delta; @@ -380,19 +334,19 @@ library RecollateralizationLibP1 { // needed(Bottom): token balance needed at bottom of the basket range needed = range.bottom.mul(ctx.quantities[i], CEIL); // {buyTok}; - if (bal.lt(needed)) { - uint192 amtShort = needed.minus(bal); // {buyTok} - (uint192 lotLow, uint192 lotHigh) = reg.assets[i].lotPrice(); // {UoA/buyTok} + if (ctx.bals[i].lt(needed)) { + uint192 amtShort = needed.minus(ctx.bals[i]); // {buyTok} + (uint192 low, uint192 high) = reg.assets[i].price(); // {UoA/buyTok} // {UoA} = {buyTok} * {UoA/buyTok} - uint192 delta = amtShort.mul(lotHigh, CEIL); + uint192 delta = amtShort.mul(high, CEIL); // The best asset to buy is whichever asset has the largest deficit if (delta.gt(maxes.deficit)) { trade.buy = reg.assets[i]; trade.buyAmount = amtShort; - trade.prices.buyLow = lotLow; - trade.prices.buyHigh = lotHigh; + trade.prices.buyLow = low; + trade.prices.buyHigh = high; maxes.deficit = delta; } @@ -402,21 +356,22 @@ library RecollateralizationLibP1 { // Use RSR if needed if (address(trade.sell) == address(0) && address(trade.buy) != address(0)) { - IAsset rsrAsset = ctx.ar.toAsset(ctx.rsr); - - uint192 rsrAvailable = rsrAsset.bal(address(ctx.bm)).plus( - rsrAsset.bal(address(ctx.stRSR)) - ); - (uint192 lotLow, uint192 lotHigh) = rsrAsset.lotPrice(); // {UoA/RSR} + (uint192 low, uint192 high) = reg.assets[rsrIndex].price(); // {UoA/RSR} + // if rsr does not have a registered asset the below array accesses will revert if ( - lotHigh > 0 && - TradeLib.isEnoughToSell(rsrAsset, rsrAvailable, lotLow, ctx.minTradeVolume) + high > 0 && + TradeLib.isEnoughToSell( + reg.assets[rsrIndex], + ctx.bals[rsrIndex], + low, + ctx.minTradeVolume + ) ) { - trade.sell = rsrAsset; - trade.sellAmount = rsrAvailable; - trade.prices.sellLow = lotLow; - trade.prices.sellHigh = lotHigh; + trade.sell = reg.assets[rsrIndex]; + trade.sellAmount = ctx.bals[rsrIndex]; + trade.prices.sellLow = low; + trade.prices.sellHigh = high; } } } diff --git a/contracts/p1/mixins/TradeLib.sol b/contracts/p1/mixins/TradeLib.sol index f0921dd511..8d3c8e01c9 100644 --- a/contracts/p1/mixins/TradeLib.sol +++ b/contracts/p1/mixins/TradeLib.sol @@ -62,7 +62,7 @@ library TradeLib { ); // Cap sell amount - uint192 maxSell = maxTradeSize(trade.sell, trade.buy, trade.prices.sellHigh); // {sellTok} + uint192 maxSell = maxTradeSize(trade.sell, trade.buy, trade.prices.sellLow); // {sellTok} uint192 s = trade.sellAmount > maxSell ? maxSell : trade.sellAmount; // {sellTok} // Calculate equivalent buyAmount within [0, FIX_MAX] diff --git a/contracts/p1/mixins/Trading.sol b/contracts/p1/mixins/Trading.sol index 24b2044d38..1c6217e8ec 100644 --- a/contracts/p1/mixins/Trading.sol +++ b/contracts/p1/mixins/Trading.sol @@ -97,7 +97,7 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl // == Interactions == (uint256 soldAmt, uint256 boughtAmt) = trade.settle(); - emit TradeSettled(trade, trade.sell(), trade.buy(), soldAmt, boughtAmt); + emit TradeSettled(trade, sell, trade.buy(), soldAmt, boughtAmt); } /// Try to initiate a trade with a trading partner provided by the broker @@ -119,7 +119,7 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl TradePrices memory prices ) internal returns (ITrade trade) { IERC20 sell = req.sell.erc20(); - assert(address(trades[sell]) == address(0)); + assert(address(trades[sell]) == address(0)); // ensure calling class has checked this // Set allowance via custom approval -- first sets allowance to 0, then sets allowance // to either the requested amount or the maximum possible amount, if that fails. diff --git a/contracts/plugins/assets/AppreciatingFiatCollateral.sol b/contracts/plugins/assets/AppreciatingFiatCollateral.sol index bf7cef6022..60e575cf71 100644 --- a/contracts/plugins/assets/AppreciatingFiatCollateral.sol +++ b/contracts/plugins/assets/AppreciatingFiatCollateral.sol @@ -88,7 +88,7 @@ abstract contract AppreciatingFiatCollateral is FiatCollateral { // uint192(<) is equivalent to Fix.lt if (underlyingRefPerTok < exposedReferencePrice) { - exposedReferencePrice = hiddenReferencePrice; + exposedReferencePrice = underlyingRefPerTok; markStatus(CollateralStatus.DISABLED); } else if (hiddenReferencePrice > exposedReferencePrice) { exposedReferencePrice = hiddenReferencePrice; diff --git a/contracts/plugins/assets/Asset.sol b/contracts/plugins/assets/Asset.sol index 1bb044c239..302a6a6731 100644 --- a/contracts/plugins/assets/Asset.sol +++ b/contracts/plugins/assets/Asset.sol @@ -7,10 +7,14 @@ import "../../interfaces/IAsset.sol"; import "./OracleLib.sol"; import "./VersionedAsset.sol"; +uint48 constant ORACLE_TIMEOUT_BUFFER = 300; // {s} 5 minutes + contract Asset is IAsset, VersionedAsset { using FixLib for uint192; using OracleLib for AggregatorV3Interface; + uint192 public constant MAX_HIGH_PRICE_BUFFER = 2 * FIX_ONE; // {UoA/tok} 200% + AggregatorV3Interface public immutable chainlinkFeed; // {UoA/tok} IERC20Metadata public immutable erc20; @@ -38,7 +42,7 @@ contract Asset is IAsset, VersionedAsset { /// @param oracleError_ {1} The % the oracle feed can be off by /// @param maxTradeVolume_ {UoA} The max trade volume, in UoA /// @param oracleTimeout_ {s} The number of seconds until a oracle value becomes invalid - /// @dev oracleTimeout_ is also used as the timeout value in lotPrice(), should be highest of + /// @dev oracleTimeout_ is also used as the timeout value in price(), should be highest of /// all assets' oracleTimeout in a collateral if there are multiple oracles constructor( uint48 priceTimeout_, @@ -60,7 +64,7 @@ contract Asset is IAsset, VersionedAsset { erc20 = erc20_; erc20Decimals = erc20.decimals(); maxTradeVolume = maxTradeVolume_; - oracleTimeout = oracleTimeout_; + oracleTimeout = oracleTimeout_ + ORACLE_TIMEOUT_BUFFER; // add 300s as a buffer } /// Can revert, used by other contract functions in order to catch errors @@ -108,54 +112,69 @@ contract Asset is IAsset, VersionedAsset { } /// Should not revert + /// low should be nonzero if the asset could be worth selling /// @dev Should be general enough to not need to be overridden - /// @return {UoA/tok} The lower end of the price estimate - /// @return {UoA/tok} The upper end of the price estimate - function price() public view virtual returns (uint192, uint192) { - try this.tryPrice() returns (uint192 low, uint192 high, uint192) { - assert(low <= high); - return (low, high); - } catch (bytes memory errData) { - // see: docs/solidity-style.md#Catching-Empty-Data - if (errData.length == 0) revert(); // solhint-disable-line reason-string - return (0, FIX_MAX); - } - } - - /// Should not revert - /// lotLow should be nonzero when the asset might be worth selling - /// @dev Should be general enough to not need to be overridden - /// @return lotLow {UoA/tok} The lower end of the lot price estimate - /// @return lotHigh {UoA/tok} The upper end of the lot price estimate - function lotPrice() external view virtual returns (uint192 lotLow, uint192 lotHigh) { + /// @return _low {UoA/tok} The lower end of the price estimate + /// @return _high {UoA/tok} The upper end of the price estimate + /// @notice If the price feed is broken, _low will decay downwards and _high will decay upwards + /// If tryPrice() is broken for more than `oracleTimeout + priceTimeout` seconds, + /// _low will be 0 and _high will be FIX_MAX. + /// Because the price decay begins at `oracleTimeout` seconds and not `updateTime` from the + /// price feed, the price feed can be broken for up to `2 * oracleTimeout` seconds without + /// affecting the price estimate. This could happen if the Asset is refreshed just before + /// the oracleTimeout is reached, forcing a second period of oracleTimeout to pass before + /// the price begins to decay. + function price() public view virtual returns (uint192 _low, uint192 _high) { try this.tryPrice() returns (uint192 low, uint192 high, uint192) { // if the price feed is still functioning, use that - lotLow = low; - lotHigh = high; + _low = low; + _high = high; } catch (bytes memory errData) { // see: docs/solidity-style.md#Catching-Empty-Data if (errData.length == 0) revert(); // solhint-disable-line reason-string - // if the price feed is broken, use a decayed historical value + // if the price feed is broken, decay _low downwards and _high upwards uint48 delta = uint48(block.timestamp) - lastSave; // {s} if (delta <= oracleTimeout) { - lotLow = savedLowPrice; - lotHigh = savedHighPrice; + // use saved prices for at least the oracleTimeout + _low = savedLowPrice; + _high = savedHighPrice; } else if (delta >= oracleTimeout + priceTimeout) { - return (0, 0); // no price after full timeout + // unpriced after a full timeout + return (0, FIX_MAX); } else { // oracleTimeout <= delta <= oracleTimeout + priceTimeout - // {1} = {s} / {s} - uint192 lotMultiplier = divuu(oracleTimeout + priceTimeout - delta, priceTimeout); - + // Decay _high upwards to 3x savedHighPrice // {UoA/tok} = {UoA/tok} * {1} - lotLow = savedLowPrice.mul(lotMultiplier); - lotHigh = savedHighPrice.mul(lotMultiplier); + _high = savedHighPrice.safeMul( + FIX_ONE + MAX_HIGH_PRICE_BUFFER.muluDivu(delta - oracleTimeout, priceTimeout), + ROUND + ); // during overflow should not revert + + // if _high is FIX_MAX, leave at UNPRICED + if (_high != FIX_MAX) { + // Decay _low downwards from savedLowPrice to 0 + // {UoA/tok} = {UoA/tok} * {1} + _low = savedLowPrice.muluDivu( + oracleTimeout + priceTimeout - delta, + priceTimeout + ); + // during overflow should revert since a FIX_MAX _low breaks everything + } } } - assert(lotLow <= lotHigh); + assert(_low <= _high); + } + + /// Should not revert + /// lotLow should be nonzero when the asset might be worth selling + /// @dev Deprecated. Phased out in 3.1.0, but left on interface for backwards compatibility + /// @return lotLow {UoA/tok} The lower end of the lot price estimate + /// @return lotHigh {UoA/tok} The upper end of the lot price estimate + function lotPrice() external view virtual returns (uint192 lotLow, uint192 lotHigh) { + return price(); } /// @return {tok} The balance of the ERC20 in whole tokens diff --git a/contracts/plugins/assets/EURFiatCollateral.sol b/contracts/plugins/assets/EURFiatCollateral.sol index 67d0c12f34..dfc36ff73e 100644 --- a/contracts/plugins/assets/EURFiatCollateral.sol +++ b/contracts/plugins/assets/EURFiatCollateral.sol @@ -27,6 +27,8 @@ contract EURFiatCollateral is FiatCollateral { ) FiatCollateral(config) { require(address(targetUnitChainlinkFeed_) != address(0), "missing targetUnit feed"); require(targetUnitOracleTimeout_ > 0, "targetUnitOracleTimeout zero"); + require(config.defaultThreshold > 0, "defaultThreshold zero"); + targetUnitChainlinkFeed = targetUnitChainlinkFeed_; targetUnitOracleTimeout = targetUnitOracleTimeout_; } diff --git a/contracts/plugins/assets/FiatCollateral.sol b/contracts/plugins/assets/FiatCollateral.sol index 9110117bc5..d3afad43c5 100644 --- a/contracts/plugins/assets/FiatCollateral.sol +++ b/contracts/plugins/assets/FiatCollateral.sol @@ -75,6 +75,10 @@ contract FiatCollateral is ICollateral, Asset { } require(config.delayUntilDefault <= 1209600, "delayUntilDefault too long"); + // Note: This contract is designed to allow setting defaultThreshold = 0 to disable + // default checks. You can apply the check below to child contracts when required + // require(config.defaultThreshold > 0, "defaultThreshold zero"); + targetName = config.targetName; delayUntilDefault = config.delayUntilDefault; @@ -122,7 +126,7 @@ contract FiatCollateral is ICollateral, Asset { function refresh() public virtual override(Asset, IAsset) { CollateralStatus oldStatus = status(); - // Check for soft default + save lotPrice + // Check for soft default + save price try this.tryPrice() returns (uint192 low, uint192 high, uint192 pegPrice) { // {UoA/tok}, {UoA/tok}, {target/ref} // (0, 0) is a valid price; (0, FIX_MAX) is unpriced diff --git a/contracts/plugins/assets/L2LSDCollateral.sol b/contracts/plugins/assets/L2LSDCollateral.sol index 0fc8e40884..60b0bd8329 100644 --- a/contracts/plugins/assets/L2LSDCollateral.sol +++ b/contracts/plugins/assets/L2LSDCollateral.sol @@ -30,6 +30,7 @@ abstract contract L2LSDCollateral is AppreciatingFiatCollateral { ) AppreciatingFiatCollateral(config, revenueHiding) { require(address(_exchangeRateChainlinkFeed) != address(0), "missing exchangeRate feed"); require(_exchangeRateChainlinkTimeout != 0, "exchangeRateChainlinkTimeout zero"); + require(config.defaultThreshold > 0, "defaultThreshold zero"); exchangeRateChainlinkFeed = _exchangeRateChainlinkFeed; exchangeRateChainlinkTimeout = _exchangeRateChainlinkTimeout; @@ -52,7 +53,7 @@ abstract contract L2LSDCollateral is AppreciatingFiatCollateral { // uint192(<) is equivalent to Fix.lt if (underlyingRefPerTok < exposedReferencePrice) { - exposedReferencePrice = hiddenReferencePrice; + exposedReferencePrice = underlyingRefPerTok; markStatus(CollateralStatus.DISABLED); } else if (hiddenReferencePrice > exposedReferencePrice) { exposedReferencePrice = hiddenReferencePrice; diff --git a/contracts/plugins/assets/NonFiatCollateral.sol b/contracts/plugins/assets/NonFiatCollateral.sol index 2e6b3c531f..1923dea24a 100644 --- a/contracts/plugins/assets/NonFiatCollateral.sol +++ b/contracts/plugins/assets/NonFiatCollateral.sol @@ -27,6 +27,8 @@ contract NonFiatCollateral is FiatCollateral { ) FiatCollateral(config) { require(address(targetUnitChainlinkFeed_) != address(0), "missing targetUnit feed"); require(targetUnitOracleTimeout_ > 0, "targetUnitOracleTimeout zero"); + require(config.defaultThreshold > 0, "defaultThreshold zero"); + targetUnitChainlinkFeed = targetUnitChainlinkFeed_; targetUnitOracleTimeout = targetUnitOracleTimeout_; } diff --git a/contracts/plugins/assets/RTokenAsset.sol b/contracts/plugins/assets/RTokenAsset.sol index 68a9da9863..e9487fe671 100644 --- a/contracts/plugins/assets/RTokenAsset.sol +++ b/contracts/plugins/assets/RTokenAsset.sol @@ -18,12 +18,14 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { using OracleLib for AggregatorV3Interface; // Component addresses are not mutable in protocol, so it's safe to cache these - IMain public immutable main; - IBasketHandler public immutable basketHandler; IAssetRegistry public immutable assetRegistry; + IBasketHandler public immutable basketHandler; IBackingManager public immutable backingManager; + IFurnace public immutable furnace; + IERC20 public immutable rsr; + IStRSR public immutable stRSR; - IERC20Metadata public immutable erc20; + IERC20Metadata public immutable erc20; // The RToken uint8 public immutable erc20Decimals; @@ -37,10 +39,13 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { require(address(erc20_) != address(0), "missing erc20"); require(maxTradeVolume_ > 0, "invalid max trade volume"); - main = erc20_.main(); - basketHandler = main.basketHandler(); + IMain main = erc20_.main(); assetRegistry = main.assetRegistry(); + basketHandler = main.basketHandler(); backingManager = main.backingManager(); + furnace = main.furnace(); + rsr = main.rsr(); + stRSR = main.stRSR(); erc20 = IERC20Metadata(address(erc20_)); erc20Decimals = erc20_.decimals(); @@ -55,10 +60,8 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { /// `basketHandler.price()`. When `range.bottom == range.top` then there is no compounding. /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate - function tryPrice(bool useLotPrice) external view virtual returns (uint192 low, uint192 high) { - (uint192 lowBUPrice, uint192 highBUPrice) = useLotPrice - ? basketHandler.lotPrice() - : basketHandler.price(); // {UoA/BU} + function tryPrice() external view virtual returns (uint192 low, uint192 high) { + (uint192 lowBUPrice, uint192 highBUPrice) = basketHandler.price(); // {UoA/BU} require(lowBUPrice != 0 && highBUPrice != FIX_MAX, "invalid price"); assert(lowBUPrice <= highBUPrice); // not obviously true just by inspection @@ -79,21 +82,21 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { assert(low <= high); // not obviously true } - // solhint-disable no-empty-blocks function refresh() public virtual override { - // No need to save lastPrice; can piggyback off the backing collateral's lotPrice() + // No need to save lastPrice; can piggyback off the backing collateral's saved prices + + furnace.melt(); + if (msg.sender != address(assetRegistry)) assetRegistry.refresh(); cachedOracleData.cachedAtTime = 0; // force oracle refresh } - // solhint-enable no-empty-blocks - /// Should not revert /// @dev See `tryPrice` caveat about possible compounding error in calculating price /// @return {UoA/tok} The lower end of the price estimate /// @return {UoA/tok} The upper end of the price estimate function price() public view virtual returns (uint192, uint192) { - try this.tryPrice(false) returns (uint192 low, uint192 high) { + try this.tryPrice() returns (uint192 low, uint192 high) { return (low, high); } catch (bytes memory errData) { // see: docs/solidity-style.md#Catching-Empty-Data @@ -104,18 +107,11 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { /// Should not revert /// lotLow should be nonzero when the asset might be worth selling - /// @dev See `tryPrice` caveat about possible compounding error in calculating price + /// @dev Deprecated. Phased out in 3.1.0, but left on interface for backwards compatibility /// @return lotLow {UoA/tok} The lower end of the lot price estimate /// @return lotHigh {UoA/tok} The upper end of the lot price estimate - function lotPrice() external view returns (uint192 lotLow, uint192 lotHigh) { - try this.tryPrice(true) returns (uint192 low, uint192 high) { - lotLow = low; - lotHigh = high; - } catch (bytes memory errData) { - // see: docs/solidity-style.md#Catching-Empty-Data - if (errData.length == 0) revert(); // solhint-disable-line reason-string - return (0, 0); - } + function lotPrice() external view virtual returns (uint192 lotLow, uint192 lotHigh) { + return price(); } /// @return {tok} The balance of the ERC20 in whole tokens @@ -143,10 +139,15 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { // solhint-enable no-empty-blocks + /// Force an update to the cache, including refreshing underlying assets + /// @dev Can revert if RToken is unpriced function forceUpdatePrice() external { _updateCachedPrice(); } + /// @dev Can revert if RToken is unpriced + /// @return rTokenPrice {UoA/tok} The mean price estimate + /// @return updatedAt {s} The timestamp of the cache update function latestPrice() external returns (uint192 rTokenPrice, uint256 updatedAt) { // Situations that require an update, from most common to least common. if ( @@ -158,15 +159,17 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { _updateCachedPrice(); } - return (cachedOracleData.cachedPrice, cachedOracleData.cachedAtTime); + rTokenPrice = cachedOracleData.cachedPrice; + updatedAt = cachedOracleData.cachedAtTime; } // ==== Private ==== // Update Oracle Data function _updateCachedPrice() internal { - (uint192 low, uint192 high) = price(); + assetRegistry.refresh(); // will call furnace.melt() + (uint192 low, uint192 high) = price(); require(low != 0 && high != FIX_MAX, "invalid price"); cachedOracleData = CachedOracleData( @@ -178,7 +181,7 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { ); } - /// Computationally expensive basketRange calculation; used in price() & lotPrice() + /// Computationally expensive basketRange calculation; used in price() function basketRange() private view returns (BasketRange memory range) { BasketRange memory basketsHeld = basketHandler.basketsHeldBy(address(backingManager)); uint192 basketsNeeded = IRToken(address(erc20)).basketsNeeded(); // {BU} @@ -193,24 +196,9 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { // the absence of an external price feed. Any RToken that gets reasonably big // should switch over to an asset with a price feed. - TradingContext memory ctx; - - ctx.basketsHeld = basketsHeld; - ctx.bm = backingManager; - ctx.bh = basketHandler; - ctx.ar = assetRegistry; - ctx.stRSR = main.stRSR(); - ctx.rsr = main.rsr(); - ctx.rToken = main.rToken(); - ctx.minTradeVolume = backingManager.minTradeVolume(); - ctx.maxTradeSlippage = backingManager.maxTradeSlippage(); - - // Calculate quantities - Registry memory reg = ctx.ar.getRegistry(); - ctx.quantities = new uint192[](reg.erc20s.length); - for (uint256 i = 0; i < reg.erc20s.length; ++i) { - ctx.quantities[i] = ctx.bh.quantityUnsafe(reg.erc20s[i], reg.assets[i]); - } + (TradingContext memory ctx, Registry memory reg) = backingManager.tradingContext( + basketsHeld + ); // will exclude UoA value from RToken balances at BackingManager range = RecollateralizationLibP1.basketRange(ctx, reg); diff --git a/contracts/plugins/assets/VersionedAsset.sol b/contracts/plugins/assets/VersionedAsset.sol index ac8371e7f2..b36945769d 100644 --- a/contracts/plugins/assets/VersionedAsset.sol +++ b/contracts/plugins/assets/VersionedAsset.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.19; import "../../interfaces/IVersioned.sol"; // This value should be updated on each release -string constant ASSET_VERSION = "3.0.1"; +string constant ASSET_VERSION = "3.1.0"; /** * @title VersionedAsset diff --git a/contracts/plugins/assets/aave-v3/AaveV3FiatCollateral.sol b/contracts/plugins/assets/aave-v3/AaveV3FiatCollateral.sol index 2edfd5d65b..ba1843351c 100644 --- a/contracts/plugins/assets/aave-v3/AaveV3FiatCollateral.sol +++ b/contracts/plugins/assets/aave-v3/AaveV3FiatCollateral.sol @@ -20,7 +20,9 @@ contract AaveV3FiatCollateral is AppreciatingFiatCollateral { /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide constructor(CollateralConfig memory config, uint192 revenueHiding) AppreciatingFiatCollateral(config, revenueHiding) - {} + { + require(config.defaultThreshold > 0, "defaultThreshold zero"); + } // solhint-enable no-empty-blocks diff --git a/contracts/plugins/assets/aave/ATokenFiatCollateral.sol b/contracts/plugins/assets/aave/ATokenFiatCollateral.sol index f6e98c267e..14e72a72ca 100644 --- a/contracts/plugins/assets/aave/ATokenFiatCollateral.sol +++ b/contracts/plugins/assets/aave/ATokenFiatCollateral.sol @@ -41,7 +41,9 @@ contract ATokenFiatCollateral is AppreciatingFiatCollateral { /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide constructor(CollateralConfig memory config, uint192 revenueHiding) AppreciatingFiatCollateral(config, revenueHiding) - {} + { + require(config.defaultThreshold > 0, "defaultThreshold zero"); + } // solhint-enable no-empty-blocks diff --git a/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol b/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol index 594db5465e..59e921e774 100644 --- a/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol +++ b/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol @@ -33,6 +33,7 @@ contract AnkrStakedEthCollateral is AppreciatingFiatCollateral { ) AppreciatingFiatCollateral(config, revenueHiding) { require(address(_targetPerTokChainlinkFeed) != address(0), "missing targetPerTok feed"); require(_targetPerTokChainlinkTimeout != 0, "targetPerTokChainlinkTimeout zero"); + require(config.defaultThreshold > 0, "defaultThreshold zero"); targetPerTokChainlinkFeed = _targetPerTokChainlinkFeed; targetPerTokChainlinkTimeout = _targetPerTokChainlinkTimeout; diff --git a/contracts/plugins/assets/cbeth/CBETHCollateral.sol b/contracts/plugins/assets/cbeth/CBETHCollateral.sol index 5c190e6050..40eb3a9d6e 100644 --- a/contracts/plugins/assets/cbeth/CBETHCollateral.sol +++ b/contracts/plugins/assets/cbeth/CBETHCollateral.sol @@ -32,6 +32,7 @@ contract CBEthCollateral is AppreciatingFiatCollateral { ) AppreciatingFiatCollateral(config, revenueHiding) { require(address(_targetPerTokChainlinkFeed) != address(0), "missing targetPerTok feed"); require(_targetPerTokChainlinkTimeout != 0, "targetPerTokChainlinkTimeout zero"); + require(config.defaultThreshold > 0, "defaultThreshold zero"); targetPerTokChainlinkFeed = _targetPerTokChainlinkFeed; targetPerTokChainlinkTimeout = _targetPerTokChainlinkTimeout; diff --git a/contracts/plugins/assets/cbeth/CBETHCollateralL2.sol b/contracts/plugins/assets/cbeth/CBETHCollateralL2.sol index b745028f54..4e98b7c3f2 100644 --- a/contracts/plugins/assets/cbeth/CBETHCollateralL2.sol +++ b/contracts/plugins/assets/cbeth/CBETHCollateralL2.sol @@ -41,6 +41,7 @@ contract CBEthCollateralL2 is L2LSDCollateral { { require(address(_targetPerTokChainlinkFeed) != address(0), "missing targetPerTok feed"); require(_targetPerTokChainlinkTimeout != 0, "targetPerTokChainlinkTimeout zero"); + require(config.defaultThreshold > 0, "defaultThreshold zero"); targetPerTokChainlinkFeed = _targetPerTokChainlinkFeed; targetPerTokChainlinkTimeout = _targetPerTokChainlinkTimeout; diff --git a/contracts/plugins/assets/cbeth/README.md b/contracts/plugins/assets/cbeth/README.md index 15deec4f2c..351074009d 100644 --- a/contracts/plugins/assets/cbeth/README.md +++ b/contracts/plugins/assets/cbeth/README.md @@ -15,6 +15,7 @@ This plugin allows `CBETH` holders to use their tokens as collateral in the Rese ### Functions #### refPerTok {ref/tok} + The L1 implementation (CBETHCollateral.sol) uses `token.exchange_rate()` to get the cbETH/ETH {ref/tok} contract exchange rate. The L2 implementation (CBETHCollateralL2.sol) uses the relevant chainlink oracle to get the cbETH/ETH {ref/tok} contract exchange rate (oraclized from the L1). diff --git a/contracts/plugins/assets/compoundv2/CTokenFiatCollateral.sol b/contracts/plugins/assets/compoundv2/CTokenFiatCollateral.sol index a60744893a..ce76a72635 100644 --- a/contracts/plugins/assets/compoundv2/CTokenFiatCollateral.sol +++ b/contracts/plugins/assets/compoundv2/CTokenFiatCollateral.sol @@ -29,6 +29,8 @@ contract CTokenFiatCollateral is AppreciatingFiatCollateral { constructor(CollateralConfig memory config, uint192 revenueHiding) AppreciatingFiatCollateral(config, revenueHiding) { + require(config.defaultThreshold > 0, "defaultThreshold zero"); + ICToken _cToken = ICToken(address(config.erc20)); address _underlying = _cToken.underlying(); uint8 _referenceERC20Decimals; diff --git a/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol b/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol index f0a44584b5..3d7dcae18f 100644 --- a/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol +++ b/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol @@ -30,6 +30,7 @@ contract CTokenNonFiatCollateral is CTokenFiatCollateral { ) CTokenFiatCollateral(config, revenueHiding) { require(address(targetUnitChainlinkFeed_) != address(0), "missing targetUnit feed"); require(targetUnitOracleTimeout_ > 0, "targetUnitOracleTimeout zero"); + require(config.defaultThreshold > 0, "defaultThreshold zero"); targetUnitChainlinkFeed = targetUnitChainlinkFeed_; targetUnitOracleTimeout = targetUnitOracleTimeout_; } diff --git a/contracts/plugins/assets/compoundv2/CTokenWrapper.sol b/contracts/plugins/assets/compoundv2/CTokenWrapper.sol index 286787d42a..27b37d8382 100644 --- a/contracts/plugins/assets/compoundv2/CTokenWrapper.sol +++ b/contracts/plugins/assets/compoundv2/CTokenWrapper.sol @@ -35,9 +35,11 @@ contract CTokenWrapper is RewardableERC20Wrapper { // === Overrides === function _claimAssetRewards() internal virtual override { + address[] memory holders = new address[](1); address[] memory cTokens = new address[](1); + holders[0] = address(this); cTokens[0] = address(underlying); - comptroller.claimComp(address(this), cTokens); + comptroller.claimComp(holders, cTokens, false, true); } // No overrides of _deposit()/_withdraw() necessary: no staking required diff --git a/contracts/plugins/assets/compoundv2/ICToken.sol b/contracts/plugins/assets/compoundv2/ICToken.sol index 9dafd86c80..c83f9a3552 100644 --- a/contracts/plugins/assets/compoundv2/ICToken.sol +++ b/contracts/plugins/assets/compoundv2/ICToken.sol @@ -33,10 +33,26 @@ interface ICToken is IERC20Metadata { function redeem(uint256 redeemTokens) external returns (uint256); } +interface TestICToken is ICToken { + /** + * @notice Sender borrows assets from the protocol to their own address + * @param borrowAmount The amount of the underlying asset to borrow + * @return uint 0=success, otherwise a failure + */ + function borrow(uint256 borrowAmount) external returns (uint256); +} + interface IComptroller { /// Claim comp for an account, to an account - function claimComp(address account, address[] memory cTokens) external; + function claimComp( + address[] memory holders, + address[] memory cTokens, + bool borrowers, + bool suppliers + ) external; /// @return The address for COMP token function getCompAddress() external view returns (address); + + function enterMarkets(address[] calldata) external returns (uint256[] memory); } diff --git a/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol b/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol index 5e7bd1238c..17d46dc908 100644 --- a/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol +++ b/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol @@ -19,12 +19,6 @@ import "./vendor/IComet.sol"; * UoA = USD */ contract CTokenV3Collateral is AppreciatingFiatCollateral { - struct CometCollateralConfig { - IERC20 rewardERC20; - uint256 reservesThresholdIffy; - uint256 reservesThresholdDisabled; - } - using OracleLib for AggregatorV3Interface; using FixLib for uint192; @@ -39,16 +33,13 @@ contract CTokenV3Collateral is AppreciatingFiatCollateral { uint192 revenueHiding, uint256 reservesThresholdIffy_ ) AppreciatingFiatCollateral(config, revenueHiding) { + require(config.defaultThreshold > 0, "defaultThreshold zero"); rewardERC20 = ICusdcV3Wrapper(address(config.erc20)).rewardERC20(); comet = IComet(address(ICusdcV3Wrapper(address(erc20)).underlyingComet())); reservesThresholdIffy = reservesThresholdIffy_; cometDecimals = comet.decimals(); } - function bal(address account) external view override(Asset, IAsset) returns (uint192) { - return shiftl_toFix(erc20.balanceOf(account), -int8(erc20Decimals)); - } - /// DEPRECATED: claimRewards() will be removed from all assets and collateral plugins function claimRewards() external override(Asset, IRewardable) { IRewardable(address(erc20)).claimRewards(); @@ -76,7 +67,7 @@ contract CTokenV3Collateral is AppreciatingFiatCollateral { // uint192(<) is equivalent to Fix.lt if (underlyingRefPerTok < exposedReferencePrice) { - exposedReferencePrice = hiddenReferencePrice; + exposedReferencePrice = underlyingRefPerTok; markStatus(CollateralStatus.DISABLED); } else if (hiddenReferencePrice > exposedReferencePrice) { exposedReferencePrice = hiddenReferencePrice; diff --git a/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol b/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol index 5b7b176061..afbab80784 100644 --- a/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol +++ b/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol @@ -43,7 +43,7 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { } /// @return number of decimals - function decimals() public pure override returns (uint8) { + function decimals() public pure override(IERC20Metadata, WrappedERC20) returns (uint8) { return 6; } @@ -81,7 +81,7 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { address dst, uint256 amount ) internal { - if (!hasPermission(src, operator)) revert Unauthorized(); + if (!underlyingComet.hasPermission(src, operator)) revert Unauthorized(); // {Comet} uint256 srcBal = underlyingComet.balanceOf(src); if (amount > srcBal) amount = srcBal; @@ -203,7 +203,10 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { rewardsClaimed[src] = accrued; rewardsAddr.claimTo(address(underlyingComet), address(this), address(this), true); - IERC20(rewardERC20).safeTransfer(dst, owed); + + uint256 bal = rewardERC20.balanceOf(address(this)); + if (owed > bal) owed = bal; + rewardERC20.safeTransfer(dst, owed); } emit RewardsClaimed(rewardERC20, owed); } diff --git a/contracts/plugins/assets/compoundv3/ICusdcV3Wrapper.sol b/contracts/plugins/assets/compoundv3/ICusdcV3Wrapper.sol index 89a9dcfb35..de2ab80ebe 100644 --- a/contracts/plugins/assets/compoundv3/ICusdcV3Wrapper.sol +++ b/contracts/plugins/assets/compoundv3/ICusdcV3Wrapper.sol @@ -10,8 +10,8 @@ import "../../../interfaces/IRewardable.sol"; interface ICusdcV3Wrapper is IWrappedERC20, IRewardable { struct UserBasic { uint104 principal; - uint64 baseTrackingAccrued; uint64 baseTrackingIndex; + uint64 baseTrackingAccrued; uint256 rewardsClaimed; } diff --git a/contracts/plugins/assets/compoundv3/WrappedERC20.sol b/contracts/plugins/assets/compoundv3/WrappedERC20.sol index b3287711d7..290a2da080 100644 --- a/contracts/plugins/assets/compoundv3/WrappedERC20.sol +++ b/contracts/plugins/assets/compoundv3/WrappedERC20.sol @@ -75,6 +75,13 @@ abstract contract WrappedERC20 is IWrappedERC20 { return _symbol; } + /** + * @dev Returns the decimals places of the token. + */ + function decimals() public pure virtual returns (uint8) { + return 18; + } + /** * @dev See {IERC20-totalSupply}. */ diff --git a/contracts/plugins/assets/compoundv3/vendor/CometExtInterface.sol b/contracts/plugins/assets/compoundv3/vendor/CometExtInterface.sol index a144d69112..70d9664aac 100644 --- a/contracts/plugins/assets/compoundv3/vendor/CometExtInterface.sol +++ b/contracts/plugins/assets/compoundv3/vendor/CometExtInterface.sol @@ -95,4 +95,12 @@ abstract contract CometExtInterface { function allowance(address owner, address spender) external view virtual returns (uint256); event Approval(address indexed owner, address indexed spender, uint256 amount); + + /** + * @notice Determine if the manager has permission to act on behalf of the owner + * @param owner The owner account + * @param manager The manager account + * @return Whether or not the manager has permission + */ + function hasPermission(address owner, address manager) external view virtual returns (bool); } diff --git a/contracts/plugins/assets/curve/CurveStableCollateral.sol b/contracts/plugins/assets/curve/CurveStableCollateral.sol index 5d6f985401..c59994fd56 100644 --- a/contracts/plugins/assets/curve/CurveStableCollateral.sol +++ b/contracts/plugins/assets/curve/CurveStableCollateral.sol @@ -88,7 +88,7 @@ contract CurveStableCollateral is AppreciatingFiatCollateral, PoolTokens { // uint192(<) is equivalent to Fix.lt if (underlyingRefPerTok < exposedReferencePrice) { - exposedReferencePrice = hiddenReferencePrice; + exposedReferencePrice = underlyingRefPerTok; markStatus(CollateralStatus.DISABLED); } else if (hiddenReferencePrice > exposedReferencePrice) { exposedReferencePrice = hiddenReferencePrice; diff --git a/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol b/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol index 7fd4fe005b..ad3cd6ac8e 100644 --- a/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol +++ b/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol @@ -13,6 +13,9 @@ interface ICurveMetaPool is ICurvePool, IERC20Metadata { * This plugin contract is intended for 2-fiattoken stable metapools that * DO NOT involve RTokens, such as LUSD-fraxBP or MIM-3CRV. * + * Does not support older metapools that have a separate contract for the + * metapool's LP token. + * * tok = ConvexStakingWrapper(PairedUSDToken/USDBasePool) * ref = PairedUSDToken/USDBasePool pool invariant * tar = USD diff --git a/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol b/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol index 420e002f4a..780a083a8b 100644 --- a/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol +++ b/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol @@ -42,6 +42,11 @@ contract CurveStableRTokenMetapoolCollateral is CurveStableMetapoolCollateral { pairedAssetRegistry = IRToken(address(pairedToken)).main().assetRegistry(); } + function refresh() public override { + pairedAssetRegistry.refresh(); // refresh all registered assets + super.refresh(); // already handles all necessary default checks + } + /// Can revert, used by `_anyDepeggedOutsidePool()` /// Should not return FIX_MAX for low /// @return lowPaired {UoA/pairedTok} The low price estimate of the paired token diff --git a/contracts/plugins/assets/curve/crv/CurveGaugeWrapper.sol b/contracts/plugins/assets/curve/crv/CurveGaugeWrapper.sol index e4c893f024..8531894bb9 100644 --- a/contracts/plugins/assets/curve/crv/CurveGaugeWrapper.sol +++ b/contracts/plugins/assets/curve/crv/CurveGaugeWrapper.sol @@ -17,6 +17,16 @@ interface ILiquidityGauge { function withdraw(uint256 _value) external; } +// Note: Only supports CRV rewards. If a Curve pool with multiple reward tokens is +// used, other reward tokens beyond CRV will never be claimed and distributed to +// depositors. These unclaimed rewards will be lost forever. + +// In addition to this, each wrapper deployment must be tested individually, regardless +// of the number of reward tokens it has. This contract is not compatible with all gauges +// and may revert depending on the Curve Gauge being used. For example, the +// `RewardsOnlyGauge` does not have a user_checkpoint() function, which means the +// MINTER.mint() call in this contract would revert in that case. + contract CurveGaugeWrapper is RewardableERC20Wrapper { using SafeERC20 for IERC20; @@ -46,6 +56,7 @@ contract CurveGaugeWrapper is RewardableERC20Wrapper { gauge.withdraw(_amount); } + // claim rewards - only supports CRV rewards, may not work for all gauges function _claimAssetRewards() internal virtual override { MINTER.mint(address(gauge)); } diff --git a/contracts/plugins/assets/curve/cvx/vendor/ConvexStakingWrapper.sol b/contracts/plugins/assets/curve/cvx/vendor/ConvexStakingWrapper.sol index 250d5b63ae..6653f450c2 100644 --- a/contracts/plugins/assets/curve/cvx/vendor/ConvexStakingWrapper.sol +++ b/contracts/plugins/assets/curve/cvx/vendor/ConvexStakingWrapper.sol @@ -9,7 +9,6 @@ import "@openzeppelin/contracts-v0.7/token/ERC20/SafeERC20.sol"; import "@openzeppelin/contracts-v0.7/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts-v0.7/utils/ReentrancyGuard.sol"; import "./IRewardStaking.sol"; -import "./CvxMining.sol"; interface IBooster { function poolInfo(uint256 _pid) @@ -23,6 +22,8 @@ interface IBooster { address _stash, bool _shutdown ); + + function earmarkRewards(uint256 _pid) external returns (bool); } interface IConvexDeposits { @@ -39,9 +40,13 @@ interface IConvexDeposits { ) external; } +interface ITokenWrapper { + function token() external view returns (address); +} + // if used as collateral some modifications will be needed to fit the specific platform -// Based on audited contracts: https://github.com/convex-eth/platform/blob/main/contracts/contracts/wrappers/CvxCrvStakingWrapper.sol +// Based on audited contracts: https://github.com/convex-eth/platform/blob/933ace34d896e6684345c6795bf33d4089fbd8f6/contracts/contracts/wrappers/ConvexStakingWrapper.sol contract ConvexStakingWrapper is ERC20, ReentrancyGuard { using SafeERC20 for IERC20; using SafeMath for uint256; @@ -54,8 +59,8 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { struct RewardType { address reward_token; address reward_pool; - uint128 reward_integral; - uint128 reward_remaining; + uint256 reward_integral; + uint256 reward_remaining; mapping(address => uint256) reward_integral_for; mapping(address => uint256) claimable_reward; } @@ -75,11 +80,10 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { //rewards RewardType[] public rewards; mapping(address => uint256) public registeredRewards; + mapping(address => address) public rewardRedirect; //management bool public isInit; - address public owner; - bool internal _isShutdown; string internal _tokenname; string internal _tokensymbol; @@ -91,15 +95,15 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { bool _wrapped ); event Withdrawn(address indexed _user, uint256 _amount, bool _unwrapped); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event RewardRedirected(address indexed _account, address _forward); + event RewardAdded(address _token); + event UserCheckpoint(address _userA, address _userB); event RewardsClaimed(IERC20 indexed erc20, uint256 indexed amount); constructor() public ERC20("StakedConvexToken", "stkCvx") {} function initialize(uint256 _poolId) external virtual { require(!isInit, "already init"); - owner = msg.sender; - emit OwnershipTransferred(address(0), owner); (address _lptoken, address _token, , address _rewards, , ) = IBooster(convexBooster) .poolInfo(_poolId); @@ -131,32 +135,6 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { return 18; } - modifier onlyOwner() { - require(owner == msg.sender, "Ownable: caller is not the owner"); - _; - } - - function transferOwnership(address newOwner) public virtual onlyOwner { - require(newOwner != address(0), "Ownable: new owner is the zero address"); - emit OwnershipTransferred(owner, newOwner); - owner = newOwner; - } - - function renounceOwnership() public virtual onlyOwner { - emit OwnershipTransferred(owner, address(0)); - owner = address(0); - } - - function shutdown() external onlyOwner { - _isShutdown = true; - } - - function isShutdown() public view returns (bool) { - if (_isShutdown) return true; - (, , , , , bool isShutdown_) = IBooster(convexBooster).poolInfo(convexPoolId); - return isShutdown_; - } - function setApprovals() public { IERC20(curveToken).safeApprove(convexBooster, 0); IERC20(curveToken).safeApprove(convexBooster, uint256(-1)); @@ -192,12 +170,18 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { //send to self to warmup state //slither-disable-next-line unchecked-transfer IERC20(cvx).transfer(address(this), 0); + emit RewardAdded(crv); + emit RewardAdded(cvx); } uint256 extraCount = IRewardStaking(mainPool).extraRewardsLength(); for (uint256 i = 0; i < extraCount; i++) { address extraPool = IRewardStaking(mainPool).extraRewards(i); address extraToken = IRewardStaking(extraPool).rewardToken(); + //from pool 151, extra reward tokens are wrapped + if (convexPoolId >= 151) { + extraToken = ITokenWrapper(extraToken).token(); + } if (extraToken == cvx) { //update cvx reward pool address rewards[CVX_INDEX].reward_pool = extraPool; @@ -205,13 +189,14 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { //add new token to list rewards.push( RewardType({ - reward_token: IRewardStaking(extraPool).rewardToken(), + reward_token: extraToken, reward_pool: extraPool, reward_integral: 0, reward_remaining: 0 }) ); registeredRewards[extraToken] = rewards.length; //mark registered at index+1 + emit RewardAdded(extraToken); } } } @@ -235,6 +220,15 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { return totalSupply(); } + //internal transfer function to transfer rewards out on claim + function _transferReward( + address _token, + address _to, + uint256 _amount + ) internal virtual { + IERC20(_token).safeTransfer(_to, _amount); + } + function _calcRewardIntegral( uint256 _index, address[2] memory _accounts, @@ -243,16 +237,19 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { bool _isClaim ) internal { RewardType storage reward = rewards[_index]; + if (reward.reward_token == address(0)) { + return; + } //get difference in balance and remaining rewards //getReward is unguarded so we use reward_remaining to keep track of how much was actually claimed uint256 bal = IERC20(reward.reward_token).balanceOf(address(this)); - // uint256 d_reward = bal.sub(reward.reward_remaining); - if (_supply > 0 && bal.sub(reward.reward_remaining) > 0) { + //check that balance increased and update integral + if (_supply > 0 && bal > reward.reward_remaining) { reward.reward_integral = reward.reward_integral + - uint128(bal.sub(reward.reward_remaining).mul(1e20).div(_supply)); + (bal.sub(reward.reward_remaining).mul(1e20).div(_supply)); } //update user integrals @@ -266,20 +263,20 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { if (_isClaim || userI < reward.reward_integral) { if (_isClaim) { uint256 receiveable = reward.claimable_reward[_accounts[u]].add( - _balances[u].mul(uint256(reward.reward_integral).sub(userI)).div(1e20) + _balances[u].mul(reward.reward_integral.sub(userI)).div(1e20) ); if (receiveable > 0) { reward.claimable_reward[_accounts[u]] = 0; //cheat for gas savings by transfering to the second index in accounts list //if claiming only the 0 index will update so 1 index can hold forwarding info //guaranteed to have an address in u+1 so no need to check - IERC20(reward.reward_token).safeTransfer(_accounts[u + 1], receiveable); + _transferReward(reward.reward_token, _accounts[u + 1], receiveable); bal = bal.sub(receiveable); } } else { reward.claimable_reward[_accounts[u]] = reward .claimable_reward[_accounts[u]] - .add(_balances[u].mul(uint256(reward.reward_integral).sub(userI)).div(1e20)); + .add(_balances[u].mul(reward.reward_integral.sub(userI)).div(1e20)); } reward.reward_integral_for[_accounts[u]] = reward.reward_integral; } @@ -287,7 +284,7 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { //update remaining reward here since balance could have changed if claiming if (bal != reward.reward_remaining) { - reward.reward_remaining = uint128(bal); + reward.reward_remaining = bal; } } @@ -297,16 +294,13 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { depositedBalance[0] = _getDepositedBalance(_accounts[0]); depositedBalance[1] = _getDepositedBalance(_accounts[1]); - if (!isShutdown()) { - IRewardStaking(convexPool).getReward(address(this), true); - } - - _claimExtras(); + IRewardStaking(convexPool).getReward(address(this), true); uint256 rewardCount = rewards.length; for (uint256 i = 0; i < rewardCount; i++) { _calcRewardIntegral(i, _accounts, depositedBalance, supply, false); } + emit UserCheckpoint(_accounts[0], _accounts[1]); } function _checkpointAndClaim(address[2] memory _accounts) internal nonReentrant { @@ -316,17 +310,11 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { IRewardStaking(convexPool).getReward(address(this), true); - _claimExtras(); - uint256 rewardCount = rewards.length; for (uint256 i = 0; i < rewardCount; i++) { _calcRewardIntegral(i, _accounts, depositedBalance, supply, true); } - } - - //claim any rewards not part of the convex pool - function _claimExtras() internal virtual { - //override and add external reward claiming + emit UserCheckpoint(_accounts[0], _accounts[1]); } function user_checkpoint(address _account) external returns (bool) { @@ -340,81 +328,54 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { //run earned as a mutable function to claim everything before calculating earned rewards function earned(address _account) external returns (EarnedData[] memory claimable) { - IRewardStaking(convexPool).getReward(address(this), true); - _claimExtras(); - return _earned(_account); - } - - //run earned as a non-mutative function that may not claim everything, but should report standard convex rewards - function earnedView(address _account) external view returns (EarnedData[] memory claimable) { + //checkpoint to pull in and tally new rewards + _checkpoint([_account, address(0)]); return _earned(_account); } function _earned(address _account) internal view returns (EarnedData[] memory claimable) { - uint256 supply = _getTotalSupply(); - // uint256 depositedBalance = _getDepositedBalance(_account); uint256 rewardCount = rewards.length; claimable = new EarnedData[](rewardCount); for (uint256 i = 0; i < rewardCount; i++) { RewardType storage reward = rewards[i]; - - //change in reward is current balance - remaining reward + earned - uint256 bal = IERC20(reward.reward_token).balanceOf(address(this)); - uint256 d_reward = bal.sub(reward.reward_remaining); - - //some rewards (like minted cvx) may not have a reward pool directly on the convex pool so check if it exists - if (reward.reward_pool != address(0)) { - //add earned from the convex reward pool for the given token - d_reward = d_reward.add(IRewardStaking(reward.reward_pool).earned(address(this))); - } - - uint256 I = reward.reward_integral; - if (supply > 0) { - I = I + d_reward.mul(1e20).div(supply); + if (reward.reward_token == address(0)) { + continue; } - uint256 newlyClaimable = _getDepositedBalance(_account) - .mul(I.sub(reward.reward_integral_for[_account])) - .div(1e20); - claimable[i].amount = claimable[i].amount.add( - reward.claimable_reward[_account].add(newlyClaimable) - ); + claimable[i].amount = reward.claimable_reward[_account]; claimable[i].token = reward.reward_token; - - //calc cvx minted from crv and add to cvx claimables - //note: crv is always index 0 so will always run before cvx - if (i == CRV_INDEX) { - //because someone can call claim for the pool outside of checkpoints, need to recalculate crv without the local balance - I = reward.reward_integral; - if (supply > 0) { - I = - I + - IRewardStaking(reward.reward_pool).earned(address(this)).mul(1e20).div( - supply - ); - } - newlyClaimable = _getDepositedBalance(_account) - .mul(I.sub(reward.reward_integral_for[_account])) - .div(1e20); - claimable[CVX_INDEX].amount = CvxMining.ConvertCrvToCvx(newlyClaimable); - claimable[CVX_INDEX].token = cvx; - } } return claimable; } function claimRewards() external { - uint256 cvxOldBal = IERC20(cvx).balanceOf(msg.sender); - uint256 crvOldBal = IERC20(crv).balanceOf(msg.sender); - _checkpointAndClaim([address(msg.sender), address(msg.sender)]); - emit RewardsClaimed(IERC20(cvx), IERC20(cvx).balanceOf(msg.sender) - cvxOldBal); - emit RewardsClaimed(IERC20(crv), IERC20(crv).balanceOf(msg.sender) - crvOldBal); + address _account = rewardRedirect[msg.sender] == address(0) + ? msg.sender + : rewardRedirect[msg.sender]; + + uint256 cvxOldBal = IERC20(cvx).balanceOf(_account); + uint256 crvOldBal = IERC20(crv).balanceOf(_account); + _checkpointAndClaim([msg.sender, _account]); + emit RewardsClaimed(IERC20(cvx), IERC20(cvx).balanceOf(_account) - cvxOldBal); + emit RewardsClaimed(IERC20(crv), IERC20(crv).balanceOf(_account) - crvOldBal); + } + + //set any claimed rewards to automatically go to a different address + //set address to zero to disable + function setRewardRedirect(address _to) external nonReentrant { + rewardRedirect[msg.sender] = _to; + emit RewardRedirected(msg.sender, _to); } function getReward(address _account) external { - //claim directly in checkpoint logic to save a bit of gas - _checkpointAndClaim([_account, _account]); + //check if there is a redirect address + if (rewardRedirect[_account] != address(0)) { + _checkpointAndClaim([_account, rewardRedirect[_account]]); + } else { + //claim directly in checkpoint logic to save a bit of gas + _checkpointAndClaim([_account, _account]); + } } function getReward(address _account, address _forwardTo) external { @@ -426,8 +387,6 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { //deposit a curve token function deposit(uint256 _amount, address _to) external { - require(!isShutdown(), "shutdown"); - //dont need to call checkpoint since _mint() will if (_amount > 0) { @@ -441,8 +400,6 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { //stake a convex token function stake(uint256 _amount, address _to) external { - require(!isShutdown(), "shutdown"); - //dont need to call checkpoint since _mint() will if (_amount > 0) { @@ -488,5 +445,10 @@ contract ConvexStakingWrapper is ERC20, ReentrancyGuard { ) internal override { _checkpoint([_from, _to]); } + + //helper function + function earmarkRewards() external returns (bool) { + return IBooster(convexBooster).earmarkRewards(convexPoolId); + } } // slither-disable-end reentrancy-no-eth \ No newline at end of file diff --git a/contracts/plugins/assets/dsr/SDaiCollateral.sol b/contracts/plugins/assets/dsr/SDaiCollateral.sol index 8e7643575f..5401b2ad5f 100644 --- a/contracts/plugins/assets/dsr/SDaiCollateral.sol +++ b/contracts/plugins/assets/dsr/SDaiCollateral.sol @@ -35,6 +35,7 @@ contract SDaiCollateral is AppreciatingFiatCollateral { uint192 revenueHiding, IPot _pot ) AppreciatingFiatCollateral(config, revenueHiding) { + require(config.defaultThreshold > 0, "defaultThreshold zero"); pot = _pot; } diff --git a/contracts/plugins/assets/erc20/RewardableERC20.sol b/contracts/plugins/assets/erc20/RewardableERC20.sol index 58fd23855c..ed741e15ec 100644 --- a/contracts/plugins/assets/erc20/RewardableERC20.sol +++ b/contracts/plugins/assets/erc20/RewardableERC20.sol @@ -7,11 +7,14 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../../../interfaces/IRewardable.sol"; +uint256 constant SHARE_DECIMAL_OFFSET = 9; // to prevent reward rounding issues + /** * @title RewardableERC20 * @notice An abstract class that can be extended to create rewardable wrapper. * @notice `_claimAssetRewards` keeps tracks of rewards by snapshotting the balance * and calculating the difference between the current balance and the previous balance. + * Limitation: Currently supports only one single reward token. * @dev To inherit: * - override _claimAssetRewards() * - call ERC20 constructor elsewhere during construction @@ -19,11 +22,11 @@ import "../../../interfaces/IRewardable.sol"; abstract contract RewardableERC20 is IRewardable, ERC20, ReentrancyGuard { using SafeERC20 for IERC20; - uint256 public immutable one; // {qShare/share} + uint256 public immutable one; // 1e9 * {qShare/share} IERC20 public immutable rewardToken; - uint256 public rewardsPerShare; // {qRewards/share} - mapping(address => uint256) public lastRewardsPerShare; // {qRewards/share} + uint256 public rewardsPerShare; // 1e9 * {qRewards/share} + mapping(address => uint256) public lastRewardsPerShare; // 1e9 * {qRewards/share} mapping(address => uint256) public accumulatedRewards; // {qRewards} mapping(address => uint256) public claimedRewards; // {qRewards} @@ -35,9 +38,11 @@ abstract contract RewardableERC20 is IRewardable, ERC20, ReentrancyGuard { /// @dev Extending class must ensure ERC20 constructor is called constructor(IERC20 _rewardToken, uint8 _decimals) { rewardToken = _rewardToken; - one = 10**_decimals; // set via pass-in to prevent inheritance issues + // set via pass-in to prevent inheritance issues + one = 10**(_decimals + SHARE_DECIMAL_OFFSET); } + // claim rewards - Only supports one single reward token function claimRewards() external nonReentrant { _claimAndSyncRewards(); _syncAccount(msg.sender); @@ -47,7 +52,7 @@ abstract contract RewardableERC20 is IRewardable, ERC20, ReentrancyGuard { function _syncAccount(address account) internal { if (account == address(0)) return; - // {qRewards/share} + // 1e9 * {qRewards/share} uint256 accountRewardsPerShare = lastRewardsPerShare[account]; // {qShare} @@ -56,37 +61,48 @@ abstract contract RewardableERC20 is IRewardable, ERC20, ReentrancyGuard { // {qRewards} uint256 _accumulatedRewards = accumulatedRewards[account]; - // {qRewards/share} + // 1e9 * {qRewards/share} uint256 _rewardsPerShare = rewardsPerShare; if (accountRewardsPerShare < _rewardsPerShare) { - // {qRewards/share} + // 1e9 * {qRewards/share} uint256 delta = _rewardsPerShare - accountRewardsPerShare; - // {qRewards} = {qRewards/share} * {qShare} + // {qRewards} = (1e9 * {qRewards/share}) * {qShare} / (1e9 * {qShare/share}) _accumulatedRewards += (delta * shares) / one; } lastRewardsPerShare[account] = _rewardsPerShare; accumulatedRewards[account] = _accumulatedRewards; } + function _rewardTokenBalance() internal view virtual returns (uint256) { + return rewardToken.balanceOf(address(this)); + } + + function _distributeReward(address account, uint256 amt) internal virtual { + rewardToken.safeTransfer(account, amt); + } + function _claimAndSyncRewards() internal virtual { uint256 _totalSupply = totalSupply(); if (_totalSupply == 0) { return; } _claimAssetRewards(); - uint256 balanceAfterClaimingRewards = rewardToken.balanceOf(address(this)); + uint256 balanceAfterClaimingRewards = _rewardTokenBalance(); uint256 _rewardsPerShare = rewardsPerShare; uint256 _previousBalance = lastRewardBalance; if (balanceAfterClaimingRewards > _previousBalance) { - uint256 delta = balanceAfterClaimingRewards - _previousBalance; + uint256 delta = balanceAfterClaimingRewards - _previousBalance; // {qRewards} + + // 1e9 * {qRewards/share} = {qRewards} * (1e9 * {qShare/share}) / {qShare} uint256 deltaPerShare = (delta * one) / _totalSupply; + // {qRewards} = {qRewards} + (1e9*(qRewards/share)) * {qShare} / (1e9*{qShare/share}) balanceAfterClaimingRewards = _previousBalance + (deltaPerShare * _totalSupply) / one; - // {qRewards/share} += {qRewards} * {qShare/share} / {qShare} + // 1e9 * {qRewards/share} += {qRewards} * (1e9*{qShare/share}) / {qShare} _rewardsPerShare += deltaPerShare; } @@ -105,7 +121,7 @@ abstract contract RewardableERC20 is IRewardable, ERC20, ReentrancyGuard { claimedRewards[account] = accumulatedRewards[account]; - uint256 currentRewardTokenBalance = rewardToken.balanceOf(address(this)); + uint256 currentRewardTokenBalance = _rewardTokenBalance(); // This is just to handle the edge case where totalSupply() == 0 and there // are still reward tokens in the contract. @@ -113,9 +129,9 @@ abstract contract RewardableERC20 is IRewardable, ERC20, ReentrancyGuard { ? currentRewardTokenBalance - lastRewardBalance : 0; - rewardToken.safeTransfer(account, claimableRewards); + _distributeReward(account, claimableRewards); - currentRewardTokenBalance = rewardToken.balanceOf(address(this)); + currentRewardTokenBalance = _rewardTokenBalance(); lastRewardBalance = currentRewardTokenBalance > nonDistributed ? currentRewardTokenBalance - nonDistributed : 0; diff --git a/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol b/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol index e2a4ec927f..6ae34a21a8 100644 --- a/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol +++ b/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol @@ -30,6 +30,10 @@ abstract contract RewardableERC20Wrapper is RewardableERC20 { string memory _symbol, IERC20 _rewardToken ) ERC20(_name, _symbol) RewardableERC20(_rewardToken, _underlying.decimals()) { + require( + address(_rewardToken) != address(_underlying), + "reward and underlying cannot match" + ); underlying = _underlying; underlyingDecimals = _underlying.decimals(); } diff --git a/contracts/plugins/assets/erc20/RewardableERC4626Vault.sol b/contracts/plugins/assets/erc20/RewardableERC4626Vault.sol index 284f717c2e..3966e66ea9 100644 --- a/contracts/plugins/assets/erc20/RewardableERC4626Vault.sol +++ b/contracts/plugins/assets/erc20/RewardableERC4626Vault.sol @@ -27,7 +27,9 @@ abstract contract RewardableERC4626Vault is ERC4626, RewardableERC20 { ) ERC4626(_asset, _name, _symbol) RewardableERC20(_rewardToken, _asset.decimals() + _decimalsOffset()) - {} + { + require(address(_rewardToken) != address(_asset), "reward and asset cannot match"); + } // solhint-enable no-empty-blocks diff --git a/contracts/plugins/assets/frax-eth/README.md b/contracts/plugins/assets/frax-eth/README.md index 4e95c0a05a..7d32cc254a 100644 --- a/contracts/plugins/assets/frax-eth/README.md +++ b/contracts/plugins/assets/frax-eth/README.md @@ -34,4 +34,4 @@ This function returns rate of `frxETH/sfrxETH`, getting from [pricePerShare()](h #### tryPrice -This function uses `refPerTok`, the chainlink price of `ETH/frxETH`, and the chainlink price of `USD/ETH` to return the current price range of the collateral. +This function uses `refPerTok` and the chainlink price of `USD/ETH` to return the current price range of the collateral. Once an oracle becomes available for `frxETH/ETH`, this function should be modified to use it and return the appropiate `pegPrice`. diff --git a/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol b/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol index 4697ec0da0..c3dbe91379 100644 --- a/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol +++ b/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol @@ -7,6 +7,12 @@ import "../AppreciatingFiatCollateral.sol"; import "../OracleLib.sol"; import "./vendor/IsfrxEth.sol"; +/** + * ************************************************************ + * WARNING: this plugin is not ready to be used in Production + * ************************************************************ + */ + /** * @title SFraxEthCollateral * @notice Collateral plugin for Frax-ETH, @@ -23,14 +29,16 @@ contract SFraxEthCollateral is AppreciatingFiatCollateral { /// @param config.chainlinkFeed Feed units: {UoA/target} constructor(CollateralConfig memory config, uint192 revenueHiding) AppreciatingFiatCollateral(config, revenueHiding) - {} + { + require(config.defaultThreshold > 0, "defaultThreshold zero"); + } // solhint-enable no-empty-blocks /// Can revert, used by other contract functions in order to catch errors /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate - /// @return pegPrice {target/ref} The actual price observed in the peg + /// @return pegPrice {target/ref} FIX_ONE until an oracle becomes available function tryPrice() external view @@ -49,6 +57,8 @@ contract SFraxEthCollateral is AppreciatingFiatCollateral { high = p + err; // assert(low <= high); obviously true just by inspection + // TODO: Currently not checking for depegs between `frxETH` and `ETH` + // Should be modified to use a `frxETH/ETH` oracle when available pegPrice = targetPerRef(); } diff --git a/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol b/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol index 783896f2c0..9267f40e76 100644 --- a/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol +++ b/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol @@ -35,6 +35,8 @@ contract LidoStakedEthCollateral is AppreciatingFiatCollateral { ) AppreciatingFiatCollateral(config, revenueHiding) { require(address(_targetPerRefChainlinkFeed) != address(0), "missing targetPerRef feed"); require(_targetPerRefChainlinkTimeout > 0, "targetPerRefChainlinkTimeout zero"); + require(config.defaultThreshold > 0, "defaultThreshold zero"); + targetPerRefChainlinkFeed = _targetPerRefChainlinkFeed; targetPerRefChainlinkTimeout = _targetPerRefChainlinkTimeout; } diff --git a/contracts/plugins/assets/morpho-aave/MorphoAaveV2TokenisedDeposit.sol b/contracts/plugins/assets/morpho-aave/MorphoAaveV2TokenisedDeposit.sol index eff7ccd9b5..bc5f32abd8 100644 --- a/contracts/plugins/assets/morpho-aave/MorphoAaveV2TokenisedDeposit.sol +++ b/contracts/plugins/assets/morpho-aave/MorphoAaveV2TokenisedDeposit.sol @@ -3,13 +3,12 @@ pragma solidity 0.8.19; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; -import { IMorpho, IMorphoRewardsDistributor, IMorphoUsersLens } from "./IMorpho.sol"; +import { IMorpho, IMorphoUsersLens } from "./IMorpho.sol"; import { MorphoTokenisedDeposit, MorphoTokenisedDepositConfig } from "./MorphoTokenisedDeposit.sol"; struct MorphoAaveV2TokenisedDepositConfig { IMorpho morphoController; IMorphoUsersLens morphoLens; - IMorphoRewardsDistributor rewardsDistributor; IERC20Metadata underlyingERC20; IERC20Metadata poolToken; ERC20 rewardToken; @@ -22,7 +21,6 @@ contract MorphoAaveV2TokenisedDeposit is MorphoTokenisedDeposit { MorphoTokenisedDeposit( MorphoTokenisedDepositConfig({ morphoController: config.morphoController, - rewardsDistributor: config.rewardsDistributor, underlyingERC20: config.underlyingERC20, poolToken: config.poolToken, rewardToken: config.rewardToken diff --git a/contracts/plugins/assets/morpho-aave/MorphoFiatCollateral.sol b/contracts/plugins/assets/morpho-aave/MorphoFiatCollateral.sol index 5959b944ec..248c24084c 100644 --- a/contracts/plugins/assets/morpho-aave/MorphoFiatCollateral.sol +++ b/contracts/plugins/assets/morpho-aave/MorphoFiatCollateral.sol @@ -28,6 +28,7 @@ contract MorphoFiatCollateral is AppreciatingFiatCollateral { AppreciatingFiatCollateral(config, revenueHiding) { require(address(config.erc20) != address(0), "missing erc20"); + require(config.defaultThreshold > 0, "defaultThreshold zero"); MorphoTokenisedDeposit vault = MorphoTokenisedDeposit(address(config.erc20)); oneShare = 10**vault.decimals(); refDecimals = int8(uint8(IERC20Metadata(vault.asset()).decimals())); diff --git a/contracts/plugins/assets/morpho-aave/MorphoNonFiatCollateral.sol b/contracts/plugins/assets/morpho-aave/MorphoNonFiatCollateral.sol index 27449c2883..3f1fe73110 100644 --- a/contracts/plugins/assets/morpho-aave/MorphoNonFiatCollateral.sol +++ b/contracts/plugins/assets/morpho-aave/MorphoNonFiatCollateral.sol @@ -16,13 +16,13 @@ contract MorphoNonFiatCollateral is MorphoFiatCollateral { using OracleLib for AggregatorV3Interface; using FixLib for uint192; - AggregatorV3Interface public immutable targetUnitChainlinkFeed; // {target/ref} + AggregatorV3Interface public immutable targetUnitChainlinkFeed; // {UoA/target} uint48 public immutable targetUnitOracleTimeout; // {s} /// @dev config.erc20 must be a MorphoTokenisedDeposit - /// @param config.chainlinkFeed Feed units: {UoA/target} + /// @param config.chainlinkFeed Feed units: {target/ref} /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide - /// @param targetUnitChainlinkFeed_ Feed units: {target/ref} + /// @param targetUnitChainlinkFeed_ Feed units: {UoA/target} /// @param targetUnitOracleTimeout_ {s} oracle timeout to use for targetUnitChainlinkFeed constructor( CollateralConfig memory config, @@ -48,11 +48,12 @@ contract MorphoNonFiatCollateral is MorphoFiatCollateral { uint192 pegPrice ) { - // {tar/ref} Get current market peg - pegPrice = targetUnitChainlinkFeed.price(targetUnitOracleTimeout); + pegPrice = chainlinkFeed.price(oracleTimeout); // {target/ref} // {UoA/tok} = {UoA/target} * {target/ref} * {ref/tok} - uint192 p = chainlinkFeed.price(oracleTimeout).mul(pegPrice).mul(_underlyingRefPerTok()); + uint192 p = targetUnitChainlinkFeed.price(targetUnitOracleTimeout).mul(pegPrice).mul( + _underlyingRefPerTok() + ); uint192 err = p.mul(oracleError, CEIL); high = p + err; diff --git a/contracts/plugins/assets/morpho-aave/MorphoTokenisedDeposit.sol b/contracts/plugins/assets/morpho-aave/MorphoTokenisedDeposit.sol index d2664e782c..e2bf558fe5 100644 --- a/contracts/plugins/assets/morpho-aave/MorphoTokenisedDeposit.sol +++ b/contracts/plugins/assets/morpho-aave/MorphoTokenisedDeposit.sol @@ -3,23 +3,34 @@ pragma solidity 0.8.19; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; -import { IMorpho, IMorphoRewardsDistributor, IMorphoUsersLens } from "./IMorpho.sol"; +import { IMorpho, IMorphoUsersLens } from "./IMorpho.sol"; import { RewardableERC4626Vault } from "../erc20/RewardableERC4626Vault.sol"; struct MorphoTokenisedDepositConfig { IMorpho morphoController; - IMorphoRewardsDistributor rewardsDistributor; IERC20Metadata underlyingERC20; IERC20Metadata poolToken; ERC20 rewardToken; } abstract contract MorphoTokenisedDeposit is RewardableERC4626Vault { - IMorphoRewardsDistributor public immutable rewardsDistributor; + struct MorphoTokenisedDepositRewardsAccountingState { + uint256 totalAccumulatedBalance; + uint256 totalPaidOutBalance; + uint256 pendingBalance; + uint256 availableBalance; + uint256 remainingPeriod; + uint256 lastSync; + } + + uint256 private constant PAYOUT_PERIOD = 7 days; + IMorpho public immutable morphoController; address public immutable poolToken; address public immutable underlying; + MorphoTokenisedDepositRewardsAccountingState private state; + constructor(MorphoTokenisedDepositConfig memory config) RewardableERC4626Vault( config.underlyingERC20, @@ -31,17 +42,53 @@ abstract contract MorphoTokenisedDeposit is RewardableERC4626Vault { underlying = address(config.underlyingERC20); morphoController = config.morphoController; poolToken = address(config.poolToken); - rewardsDistributor = config.rewardsDistributor; + state.lastSync = uint48(block.timestamp); } - function rewardTokenBalance(address account) external returns (uint256 claimableRewards) { + function sync() external { _claimAndSyncRewards(); - _syncAccount(account); - claimableRewards = accumulatedRewards[account] - claimedRewards[account]; } - // solhint-disable-next-line no-empty-blocks - function _claimAssetRewards() internal virtual override {} + function _claimAssetRewards() internal override { + // If we detect any new balances add it to pending and reset payout period + uint256 totalAccumulated = state.totalPaidOutBalance + rewardToken.balanceOf(address(this)); + uint256 newlyAccumulated = totalAccumulated - state.totalAccumulatedBalance; + + uint256 timeDelta = block.timestamp - state.lastSync; + if (timeDelta != 0 && state.remainingPeriod != 0) { + if (timeDelta > state.remainingPeriod) { + timeDelta = state.remainingPeriod; + } + + uint256 amtToPayOut = (state.pendingBalance * timeDelta) / state.remainingPeriod; + state.pendingBalance -= amtToPayOut; + state.availableBalance += amtToPayOut; + } + + if (newlyAccumulated != 0) { + state.totalAccumulatedBalance = totalAccumulated; + state.pendingBalance += newlyAccumulated; + + state.remainingPeriod = PAYOUT_PERIOD; + } else { + state.remainingPeriod = state.remainingPeriod < timeDelta + ? 0 + : state.remainingPeriod - timeDelta; + } + + state.lastSync = block.timestamp; + } + + function _rewardTokenBalance() internal view override returns (uint256) { + return state.availableBalance; + } + + function _distributeReward(address account, uint256 amt) internal override { + state.totalPaidOutBalance += amt; + state.availableBalance -= amt; + + SafeERC20.safeTransfer(rewardToken, account, amt); + } function getMorphoPoolBalance(address poolToken) internal view virtual returns (uint256); diff --git a/contracts/plugins/assets/rocket-eth/RethCollateral.sol b/contracts/plugins/assets/rocket-eth/RethCollateral.sol index f7f4386650..97c58aaef4 100644 --- a/contracts/plugins/assets/rocket-eth/RethCollateral.sol +++ b/contracts/plugins/assets/rocket-eth/RethCollateral.sol @@ -31,6 +31,7 @@ contract RethCollateral is AppreciatingFiatCollateral { ) AppreciatingFiatCollateral(config, revenueHiding) { require(address(_targetPerTokChainlinkFeed) != address(0), "missing targetPerTok feed"); require(_targetPerTokChainlinkTimeout != 0, "targetPerTokChainlinkTimeout zero"); + require(config.defaultThreshold > 0, "defaultThreshold zero"); targetPerTokChainlinkFeed = _targetPerTokChainlinkFeed; targetPerTokChainlinkTimeout = _targetPerTokChainlinkTimeout; diff --git a/contracts/plugins/assets/stargate/StargatePoolFiatCollateral.sol b/contracts/plugins/assets/stargate/StargatePoolFiatCollateral.sol index 8aaf4b2381..d31d7b04df 100644 --- a/contracts/plugins/assets/stargate/StargatePoolFiatCollateral.sol +++ b/contracts/plugins/assets/stargate/StargatePoolFiatCollateral.sol @@ -22,6 +22,7 @@ contract StargatePoolFiatCollateral is AppreciatingFiatCollateral { constructor(CollateralConfig memory config, uint192 revenueHiding) AppreciatingFiatCollateral(config, revenueHiding) { + require(config.defaultThreshold > 0, "defaultThreshold zero"); pool = StargateRewardableWrapper(address(config.erc20)).pool(); } diff --git a/contracts/plugins/assets/yearnv2/YearnV2CurveFiatCollateral.sol b/contracts/plugins/assets/yearnv2/YearnV2CurveFiatCollateral.sol index 9121982562..322eca9a75 100644 --- a/contracts/plugins/assets/yearnv2/YearnV2CurveFiatCollateral.sol +++ b/contracts/plugins/assets/yearnv2/YearnV2CurveFiatCollateral.sol @@ -3,9 +3,11 @@ pragma solidity 0.8.19; import "../curve/CurveStableCollateral.sol"; -interface IYearnV2 { - /// @return {qLP token/tok} - function pricePerShare() external view returns (uint256); +interface IPricePerShareHelper { + /// @param vault The yToken address + /// @param amount {qTok} + /// @return {qLP Token} + function amountToShares(address vault, uint256 amount) external view returns (uint256); } /** @@ -16,28 +18,27 @@ interface IYearnV2 { * tar = USD * UoA = USD * - * More on the ref token: crvUSDUSDC-f has a virtual price >=1. The ref token to measure is not the + * More on the ref token: crvUSDUSDC-f has a virtual price. The ref token to measure is not the * balance of crvUSDUSDC-f that the LP token is redeemable for, but the balance of the virtual * token that underlies crvUSDUSDC-f. This virtual token is an evolving mix of USDC and crvUSD. * - * Revenue hiding should be set to the largest % drawdown in a Yearn vault that should - * not result in default. While it is extremely rare for Yearn to have drawdowns, - * in principle it is possible and should be planned for. - * - * No rewards. + * Should only be used for Stable pools. + * No rewards (handled internally by the Yearn vault). + * Revenue hiding can be kept very small since stable curve pools should be up-only. */ contract YearnV2CurveFiatCollateral is CurveStableCollateral { using FixLib for uint192; - // solhint-disable no-empty-blocks + IPricePerShareHelper public immutable pricePerShareHelper; constructor( CollateralConfig memory config, uint192 revenueHiding, - PTConfiguration memory ptConfig - ) CurveStableCollateral(config, revenueHiding, ptConfig) {} - - // solhint-enable no-empty-blocks + PTConfiguration memory ptConfig, + IPricePerShareHelper pricePerShareHelper_ + ) CurveStableCollateral(config, revenueHiding, ptConfig) { + pricePerShareHelper = pricePerShareHelper_; + } /// Can revert, used by other contract functions in order to catch errors /// Should not return FIX_MAX for low @@ -91,12 +92,18 @@ contract YearnV2CurveFiatCollateral is CurveStableCollateral { /// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens function _underlyingRefPerTok() internal view virtual override returns (uint192) { // {ref/tok} = {ref/LP token} * {LP token/tok} - return _safeWrap(curvePool.get_virtual_price()).mul(_pricePerShare()); + return _safeWrap(curvePool.get_virtual_price()).mul(_pricePerShare(), FLOOR); } /// @return {LP token/tok} function _pricePerShare() internal view returns (uint192) { - // {LP token/tok} = {qLP token/tok} * {LP token/qLP token} - return shiftl_toFix(IYearnV2(address(erc20)).pricePerShare(), -int8(erc20Decimals)); + uint256 supply = erc20.totalSupply(); // {qTok} + uint256 shares = pricePerShareHelper.amountToShares(address(erc20), supply); // {qLP Token} + + // yvCurve tokens always have the same number of decimals as the underlying curve LP token, + // so we can divide the quanta units without converting to whole units + + // {LP token/tok} = {LP token} / {tok} + return divuu(shares, supply); } } diff --git a/contracts/plugins/governance/Governance.sol b/contracts/plugins/governance/Governance.sol index c20978fa02..96de231912 100644 --- a/contracts/plugins/governance/Governance.sol +++ b/contracts/plugins/governance/Governance.sol @@ -8,6 +8,9 @@ import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.so import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; import "../../interfaces/IStRSRVotes.sol"; +import "../../libraries/NetworkConfigLib.sol"; + +uint256 constant ONE_DAY = 86400; // {s} /* * @title Governance @@ -30,7 +33,9 @@ contract Governance is // 100% uint256 public constant ONE_HUNDRED_PERCENT = 1e8; // {micro %} - // solhint-disable no-empty-blocks + // solhint-disable-next-line var-name-mixedcase + uint256 public immutable MIN_VOTING_DELAY; // {block} equal to ONE_DAY + constructor( IStRSRVotes token_, TimelockController timelock_, @@ -44,7 +49,12 @@ contract Governance is GovernorVotes(IVotes(address(token_))) GovernorVotesQuorumFraction(quorumPercent) GovernorTimelockControl(timelock_) - {} + { + MIN_VOTING_DELAY = + (ONE_DAY + NetworkConfigLib.blocktime() - 1) / + NetworkConfigLib.blocktime(); // ONE_DAY, in blocks + requireValidVotingDelay(votingDelay_); + } // solhint-enable no-empty-blocks @@ -56,6 +66,11 @@ contract Governance is return super.votingPeriod(); } + function setVotingDelay(uint256 newVotingDelay) public override { + requireValidVotingDelay(newVotingDelay); + super.setVotingDelay(newVotingDelay); // has onlyGovernance modifier + } + /// @return {qStRSR} The number of votes required in order for a voter to become a proposer function proposalThreshold() public @@ -175,4 +190,8 @@ contract Governance is uint256 currentEra = IStRSRVotes(address(token)).currentEra(); return currentEra == pastEra; } + + function requireValidVotingDelay(uint256 newVotingDelay) private view { + require(newVotingDelay >= MIN_VOTING_DELAY, "invalid votingDelay"); + } } diff --git a/contracts/plugins/mocks/AssetMock.sol b/contracts/plugins/mocks/AssetMock.sol index b6abe6fffb..0396a5ea35 100644 --- a/contracts/plugins/mocks/AssetMock.sol +++ b/contracts/plugins/mocks/AssetMock.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.19; import "../assets/Asset.sol"; contract AssetMock is Asset { + bool public stale; + uint192 private lowPrice; uint192 private highPrice; @@ -12,7 +14,7 @@ contract AssetMock is Asset { /// @param oracleError_ {1} The % the oracle feed can be off by /// @param maxTradeVolume_ {UoA} The max trade volume, in UoA /// @param oracleTimeout_ {s} The number of seconds until a oracle value becomes invalid - /// @dev oracleTimeout_ is also used as the timeout value in lotPrice(), should be highest of + /// @dev oracleTimeout_ is also used as the timeout value in price(), should be highest of /// all assets' oracleTimeout in a collateral if there are multiple oracles constructor( uint48 priceTimeout_, @@ -40,13 +42,18 @@ contract AssetMock is Asset { uint192 ) { + require(!stale, "stale price"); return (lowPrice, highPrice, 0); } /// Should not revert /// Refresh saved prices function refresh() public virtual override { - // pass + stale = false; + } + + function setStale(bool _stale) external { + stale = _stale; } function setPrice(uint192 low, uint192 high) external { diff --git a/contracts/plugins/mocks/CTokenWrapperMock.sol b/contracts/plugins/mocks/CTokenWrapperMock.sol index c0cd0922a6..78a93b44af 100644 --- a/contracts/plugins/mocks/CTokenWrapperMock.sol +++ b/contracts/plugins/mocks/CTokenWrapperMock.sol @@ -42,9 +42,11 @@ contract CTokenWrapperMock is ERC20Mock, IRewardable { revert("reverting claim rewards"); } uint256 oldBal = comp.balanceOf(msg.sender); + address[] memory holders = new address[](1); address[] memory cTokens = new address[](1); + holders[0] = msg.sender; cTokens[0] = address(underlying); - comptroller.claimComp(msg.sender, cTokens); + comptroller.claimComp(holders, cTokens, false, true); emit RewardsClaimed(IERC20(address(comp)), comp.balanceOf(msg.sender) - oldBal); } diff --git a/contracts/plugins/mocks/ComptrollerMock.sol b/contracts/plugins/mocks/ComptrollerMock.sol index 9f95726479..249bcdb088 100644 --- a/contracts/plugins/mocks/ComptrollerMock.sol +++ b/contracts/plugins/mocks/ComptrollerMock.sol @@ -19,8 +19,14 @@ contract ComptrollerMock is IComptroller { compBalances[recipient] = amount; } - function claimComp(address holder, address[] memory) external { + function claimComp( + address[] memory holders, + address[] memory, + bool, + bool + ) external { // Mint amount and update internal balances + address holder = holders[0]; if (address(compToken) != address(0)) { uint256 amount = compBalances[holder]; compBalances[holder] = 0; @@ -31,4 +37,9 @@ contract ComptrollerMock is IComptroller { function getCompAddress() external view returns (address) { return address(compToken); } + + // mock + function enterMarkets(address[] calldata) external returns (uint256[] memory) { + return new uint256[](1); + } } diff --git a/contracts/plugins/mocks/RevenueTraderBackComp.sol b/contracts/plugins/mocks/RevenueTraderBackComp.sol index ed76f53346..73069f15ad 100644 --- a/contracts/plugins/mocks/RevenueTraderBackComp.sol +++ b/contracts/plugins/mocks/RevenueTraderBackComp.sol @@ -14,8 +14,10 @@ contract RevenueTraderCompatibleV2 is RevenueTraderP1, IRevenueTraderComp { erc20s[0] = sell; TradeKind[] memory kinds = new TradeKind[](1); kinds[0] = TradeKind.DUTCH_AUCTION; + // Mirror V3 logic (only the section relevant to tests) - this.manageTokens(erc20s, kinds); + // solhint-disable-next-line no-empty-blocks + try this.manageTokens(erc20s, kinds) {} catch {} } function version() public pure virtual override(Versioned, IVersioned) returns (string memory) { diff --git a/contracts/plugins/mocks/upgrades/FacadeMonitorV2.sol b/contracts/plugins/mocks/upgrades/FacadeMonitorV2.sol new file mode 100644 index 0000000000..ebbfc6b1c2 --- /dev/null +++ b/contracts/plugins/mocks/upgrades/FacadeMonitorV2.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "../../../facade/FacadeMonitor.sol"; + +/** + * @title FacadeMonitorV2 + * @notice Mock to test upgradeability for the FacadeMonitor contract + */ +contract FacadeMonitorV2 is FacadeMonitor { + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(MonitorParams memory params) FacadeMonitor(params) {} + + uint256 public newValue; + + function setNewValue(uint256 newValue_) external onlyOwner { + newValue = newValue_; + } + + function version() public pure returns (string memory) { + return "2.0.0"; + } +} diff --git a/contracts/plugins/trading/GnosisTrade.sol b/contracts/plugins/trading/GnosisTrade.sol index 9f52e6387a..c494ecee57 100644 --- a/contracts/plugins/trading/GnosisTrade.sol +++ b/contracts/plugins/trading/GnosisTrade.sol @@ -43,7 +43,8 @@ contract GnosisTrade is ITrade { address public origin; IERC20Metadata public sell; // address of token this trade is selling IERC20Metadata public buy; // address of token this trade is buying - uint256 public initBal; // {qTok}, this trade's balance of `sell` when init() was called + uint256 public initBal; // {qSellTok}, this trade's balance of `sell` when init() was called + uint192 public sellAmount; // {sellTok}, quantity of whole tokens being sold; dup with initBal uint48 public endTime; // timestamp after which this trade's auction can be settled uint192 public worstCasePrice; // {buyTok/sellTok}, the worst price we expect to get at Auction // We expect Gnosis Auction either to meet or beat worstCasePrice, or to return the `sell` @@ -89,7 +90,8 @@ contract GnosisTrade is ITrade { sell = req.sell.erc20(); buy = req.buy.erc20(); - initBal = sell.balanceOf(address(this)); + initBal = sell.balanceOf(address(this)); // {qSellTok} + sellAmount = shiftl_toFix(initBal, -int8(sell.decimals())); // {sellTok} require(initBal <= type(uint96).max, "initBal too large"); require(initBal >= req.sellAmount, "unfunded trade"); @@ -107,8 +109,8 @@ contract GnosisTrade is ITrade { ); // Downsize our sell amount to adjust for fee - // {qTok} = {qTok} * {1} / {1} - uint96 sellAmount = uint96( + // {qSellTok} = {qSellTok} * {1} / {1} + uint96 _sellAmount = uint96( _divrnd( req.sellAmount * FEE_DENOMINATOR, FEE_DENOMINATOR + gnosis.feeNumerator(), @@ -143,7 +145,7 @@ contract GnosisTrade is ITrade { buy, endTime, endTime, - sellAmount, + _sellAmount, minBuyAmount, minBuyAmtPerOrder, 0, diff --git a/docs/collateral.md b/docs/collateral.md index 7c95883f58..e6ae0e039c 100644 --- a/docs/collateral.md +++ b/docs/collateral.md @@ -42,12 +42,14 @@ interface IAsset is IRewardable { function refresh() external; /// Should not revert + /// low should be nonzero when the asset might be worth selling /// @return low {UoA/tok} The lower end of the price estimate /// @return high {UoA/tok} The upper end of the price estimate function price() external view returns (uint192 low, uint192 high); /// Should not revert /// lotLow should be nonzero when the asset might be worth selling + /// @dev Deprecated. Phased out in 3.1.0, but left on interface for backwards compatibility /// @return lotLow {UoA/tok} The lower end of the lot price estimate /// @return lotHigh {UoA/tok} The upper end of the lot price estimate function lotPrice() external view returns (uint192 lotLow, uint192 lotHigh); @@ -219,7 +221,7 @@ This would be sensible for many UNI v2 pools, but someone holding value in a two Revenue Hiding should be employed when the function underlying `refPerTok()` is not necessarily _strongly_ non-decreasing, or simply if there is uncertainty surrounding the guarantee. In general we recommend including a very small amount (1e-6) of revenue hiding for all appreciating collateral. This is already implemented in [AppreciatingFiatCollateral.sol](../contracts/plugins/assets/AppreciatingFiatCollateral.sol). -When implementing Revenue Hiding, the `price/lotPrice()` functions should NOT hide revenue; they should use the current underlying exchange rate to calculate a best-effort estimate of what the collateral will trade at on secondary markets. A side-effect of this approach is that the RToken's price on markets becomes more variable. +When implementing Revenue Hiding, the `price` function should NOT hide revenue; they should use the current underlying exchange rate to calculate a best-effort estimate of what the collateral will trade at on secondary markets. A side-effect of this approach is that the RToken's price on markets becomes more variable. ## Important Properties for Collateral Plugins @@ -252,7 +254,7 @@ There is a simple ERC20 wrapper that can be easily extended at [RewardableERC20W Because it’s called at the beginning of many transactions, `refresh()` should never revert. If `refresh()` encounters a critical error, it should change the Collateral contract’s state so that `status()` becomes `DISABLED`. -To prevent `refresh()` from reverting due to overflow or other numeric errors, the base collateral plugin [Fiat Collateral](../contracts/plugins/assets/FiatCollateral.sol) has a `tryPrice()` function that encapsulates both the oracle lookup as well as any subsequent math required. This function is always executed via a try-catch in `price()`/`lotPrice()`/`refresh()`. Extenders of this contract should not have to override any of these three functions, just `tryPrice()`. +To prevent `refresh()` from reverting due to overflow or other numeric errors, the base collateral plugin [Fiat Collateral](../contracts/plugins/assets/FiatCollateral.sol) has a `tryPrice()` function that encapsulates both the oracle lookup as well as any subsequent math required. This function is always executed via a try-catch in `price()`/`refresh()`. Extenders of this contract should not have to override any of these three functions, just `tryPrice()`. ### The `IFFY` status should be temporary. @@ -299,7 +301,7 @@ The values returned by the following view methods should never change: Collateral implementors who extend from [Fiat Collateral](../contracts/plugins/assets/FiatCollateral.sol) or [AppreciatingFiatCollateral.sol](../contracts/plugins/assets/AppreciatingFiatCollateral.sol) can restrict their attention to overriding the following three functions: -- `tryPrice()` (not on the ICollateral interface; used by `price()`/`lotPrice()`/`refresh()`) +- `tryPrice()` (not on the ICollateral interface; used by `price()`/`refresh()`) - `refPerTok()` - `targetPerRef()` @@ -362,23 +364,21 @@ Should never revert. Should return the tightest possible lower and upper estimate for the price of the token on secondary markets. +The difference between the upper and lower estimate should not exceed 5%, though this is not a hard-and-fast rule. When the difference (usually arising from an oracleError) is large, it can lead to [the price estimation of the RToken](../contracts/plugins/assets/RTokenAsset.sol) somewhat degrading. While this is not usually an issue it can come into play when one RToken is using another RToken as collateral either directly or indirectly through an LP token. If there is RSR overcollateralization then this issue is mitigated. + Lower estimate must be <= upper estimate. -Should return `(0, FIX_MAX)` if pricing data is unavailable or stale. +Under no price data, the low estimate shoulddecay downwards and high estimate upwards. -Should be gas-efficient. +Should return `(0, FIX_MAX)` if pricing data is _completely_ unavailable or stale. -The difference between the upper and lower estimate should not exceed ~5%, though this is not a hard-and-fast rule. When the difference (usually arising from an oracleError) is large, it can lead to [the price estimation of the RToken](../contracts/plugins/assets/RTokenAsset.sol) somewhat degrading. While this is not usually an issue it can come into play when one RToken is using another RToken as collateral either directly or indirectly through an LP token. If there is RSR overcollateralization then this issue is mitigated. +Should be gas-efficient. ### lotPrice() `{UoA/tok}` -Should never revert. - -Lower estimate must be <= upper estimate. - -The low estimate should be nonzero while the asset is worth selling. +Deprecated. Phased out in 3.1.0, but left on interface for backwards compatibility. -Should be gas-efficient. +Recommend implement `lotPrice()` by calling `price()`. If you are inheriting from any of our existing collateral plugins, this is already done for you. See [Asset.sol](../contracts/plugins/Asset.sol) for the implementation. ### refPerTok() `{ref/tok}` diff --git a/docs/deployed-addresses/1-FacadeMonitor.md b/docs/deployed-addresses/1-FacadeMonitor.md new file mode 100644 index 0000000000..e8cf1a8c05 --- /dev/null +++ b/docs/deployed-addresses/1-FacadeMonitor.md @@ -0,0 +1,7 @@ +# FacadeMonitor (Mainnet) + +## Facade Monitor Proxy + +| Contract | Address | +| -------- | --------------------------------------------------------------------------------------------------------------------- | +| FacadeMonitor (Proxy) | [0xAeA6BD7b231C0eC7f35C2bdf47A76053D09dbD09](https://etherscan.io/address/0xAeA6BD7b231C0eC7f35C2bdf47A76053D09dbD09) | diff --git a/docs/deployed-addresses/8453-FacadeMonitor.md b/docs/deployed-addresses/8453-FacadeMonitor.md new file mode 100644 index 0000000000..4cba0e181a --- /dev/null +++ b/docs/deployed-addresses/8453-FacadeMonitor.md @@ -0,0 +1,8 @@ +8453-FacadeMonitor.md +# FacadeMonitor (Base) + +## Facade Monitor Proxy + +| Contract | Address | +| --------------------- | --------------------------------------------------------------------------------------------------------------------- | +| FacadeMonitor (Proxy) | [0x5bfc6df700ef23741B2e01Bd45826E4c9735ae60](https://basescan.org/address/0x5bfc6df700ef23741B2e01Bd45826E4c9735ae60) | diff --git a/docs/deployment.md b/docs/deployment.md index 6fbf565027..98c96678ef 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -220,7 +220,7 @@ yarn deploy:run:confirm --network mainnet This checks that: -- For each asset, confirm `lotPrice()` and `price()` are close. +- For each asset, confirm: - `main.tradingPaused()` and `main.issuancePaused()` are true - `timelockController.minDelay()` is > 1e12 diff --git a/docs/exhaustive-tests.md b/docs/exhaustive-tests.md index fef7f481e0..5fafdb48f3 100644 --- a/docs/exhaustive-tests.md +++ b/docs/exhaustive-tests.md @@ -1,6 +1,6 @@ # Exhaustive Testing -The exhaustive tests include `Furnace.test.ts`, `RToken.test.ts`, `ZTradingExteremes.test.ts` and `ZZStRSR.test.ts`, and are meant to test the protocol when given permutations of input values on the extreme ends of the spectrum of possiblities. +The exhaustive tests include `Broker.test.ts`, `Furnace.test.ts`, `RToken.test.ts`, `ZTradingExtremes.test.ts` and `ZZStRSR.test.ts`, and are meant to test the protocol when given permutations of input values on the extreme ends of the spectrum of possiblities. The env vars related to exhaustive testing are `EXTREME` and `SLOW`. @@ -12,7 +12,7 @@ I'm assuming you've already got `gcloud` installed on your dev machine. If not, ```bash gcloud auth login -gcloud config set project rtoken-fuzz +gcloud config set project rtoken-testing gcloud config list project # assumed defaults @@ -39,7 +39,7 @@ gcloud compute config-ssh Jump onto the instance: ``` -ssh exhaustive.us-central1-a.rtoken-fuzz +ssh exhaustive.us-central1-a.rtoken-testing ``` Add Matt's special seasoning, for tmux and emacs QoL improvements (NOTE: This sets the tmux `ctrl-b` to `ctrl-z`): @@ -93,7 +93,7 @@ gcloud compute config-ssh Jump onto the instance: ``` -ssh exhaustive.us-central1-a.rtoken-fuzz +ssh exhaustive.us-central1-a.rtoken-testing ``` ## 3) Run the tests @@ -113,7 +113,7 @@ Tmux and run the tests: ``` tmux -bash ./scripts/run-exhaustive-tests.sh +bash ./scripts/exhaustive-tests/run-exhaustive-tests.sh ``` When the test are complete, you'll find the console output in `tmux-1.log` and `tmux-2.log`. diff --git a/docs/monitoring.md b/docs/monitoring.md new file mode 100644 index 0000000000..df1c5f8baf --- /dev/null +++ b/docs/monitoring.md @@ -0,0 +1,35 @@ +# Monitoring the Reserve Protocol and Rtokens + +This document provides an overview of the monitoring setup for the Reserve Protocol and RTokens on both the Ethereum and Base networks. The monitoring is conducted through the [Hypernative](https://app.hypernative.xyz/) platform, utilizing the `FacadeMonitor` contract to retrieve the status for specific RTokens. This monitoring setup ensures continuous vigilance over the Reserve Protocol and RTokens, with alerts promptly notifying relevant channels in case of any issues. + +## Checks/Alerts + +The following alerts are currently setup for RTokens deployed in Mainnet and Base: + +### Status (Basket Handler) - HIGH + +Checks if the status of the Basket Handler for a specific RToken is SOUND. If not, triggers an alert via Slack, Discord, Telegram, and Pager Duty. + +### Fully collateralized (Basket Handler) - HIGH + +Checks if the Basket Handler for a specific RToken is FULLY COLLATERALIZED. If not, triggers an alert via Slack, Discord, Telegram, and Pager Duty. + +### Batch Auctions Disabled - HIGH + +Checks if the batch auctions for a specific RToken are DISABLED. If true, triggers an alert via Slack, Discord, Telegram, and Pager Duty. + +### Dutch Auctions Disabled - HIGH + +Checks if the any of the dutch auctions for a specific RToken is DISABLED. If true, triggers an alert via Slack, Discord, Telegram, and Pager Duty. + +### Issuance Depleted - MEDIUM + +Triggers and alert via Slack if the Issuance Throttle for a specific RToken is consumed > 99% + +### Redemption Depleted - MEDIUM + +Triggers and alert via Slack if the Redemption Throttle for a specific RToken is consumed > 99% + +### Backing Fully Redeemable- MEDIUM + +Triggers and alert via Slack if the backing of a specific RToken is not redeemable 100% on the underlying Defi Protocol. Provides checks for AAVE V2, AAVE V3, Compound V2, Compound V3, Stargate, Flux, and Morpho AAVE V2. diff --git a/docs/pause-freeze-states.md b/docs/pause-freeze-states.md new file mode 100644 index 0000000000..17b2785fcd --- /dev/null +++ b/docs/pause-freeze-states.md @@ -0,0 +1,73 @@ +# Pause Freeze States + +Some protocol functions may be halted while the protocol is either (i) issuance-paused; (ii) trading-paused; or (iii) frozen. Below is a table that shows which protocol interactions (`@custom:interaction`) and refreshers (`@custom:refresher`) execute during paused/frozen states, as of the 3.1.0 release. + +All governance functions (`@custom:governance`) remain enabled during all paused/frozen states. They are not mentioned here. + +A :heavy_check_mark: indicates the function still executes in this state. +A :x: indicates it reverts. + +| Function | Issuance-Paused | Trading-Paused | Frozen | +| --------------------------------------- | ------------------ | ----------------------- | ----------------------- | +| `BackingManager.claimRewards()` | :heavy_check_mark: | :x: | :x: | +| `BackingManager.claimRewardsSingle()` | :heavy_check_mark: | :x: | :x: | +| `BackingManager.grantRTokenAllowance()` | :heavy_check_mark: | :heavy_check_mark: | :x: | +| `BackingManager.forwardRevenue()` | :heavy_check_mark: | :x: | :x: | +| `BackingManager.rebalance()` | :heavy_check_mark: | :x: | :x: | +| `BackingManager.settleTrade()` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `BasketHandler.refreshBasket()` | :heavy_check_mark: | :x: (unless governance) | :x: (unless governance) | +| `Broker.openTrade()` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `Broker.reportViolation()` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `Distributor.distribute()` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `Furnace.melt()` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `Main.poke()` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `RevenueTrader.claimRewards()` | :heavy_check_mark: | :x: | :x: | +| `RevenueTrader.claimRewardsSingle()` | :heavy_check_mark: | :x: | :x: | +| `RevenueTrader.distributeTokenToBuy()` | :heavy_check_mark: | :x: | :x: | +| `RevenueTrader.manageTokens()` | :heavy_check_mark: | :x: | :x: | +| `RevenueTrader.returnTokens()` | :heavy_check_mark: | :x: | :x: | +| `RevenueTrader.settleTrade()` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `RToken.issue()` | :x: | :heavy_check_mark: | :x: | +| `RToken.issueTo()` | :x: | :heavy_check_mark: | :x: | +| `RToken.redeem()` | :heavy_check_mark: | :heavy_check_mark: | :x: | +| `RToken.redeemTo()` | :heavy_check_mark: | :heavy_check_mark: | :x: | +| `RToken.redeemCustom()` | :heavy_check_mark: | :heavy_check_mark: | :x: | +| `StRSR.cancelUnstake()` | :heavy_check_mark: | :heavy_check_mark: | :x: | +| `StRSR.payoutRewards()` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `StRSR.stake()` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| `StRSR.seizeRSR()` | :heavy_check_mark: | :x: | :x: | +| `StRSR.unstake()` | :heavy_check_mark: | :x: | :x: | +| `StRSR.withdraw()` | :heavy_check_mark: | :x: | :x: | + +## Issuance-pause + +The issuance-paused states indicates that RToken issuance should be paused, and _only_ that. It is a narrow control knob that is designed solely to protect against a case where bad debt is being injected into the protocol, say, because default detection for an asset has a false negative. + +## Trading-pause + +The trading-paused state has significantly more scope than the issuance-paused state. It is designed to prevent against cases where the protocol may trade unneccesarily. Many other functions in addition to just `BackingManager.rebalance()` and `RevenueTrader.manageTokens()` are halted. In general anything that manages the backing and revenue for an RToken is halted. This may become neccessary to use due to (among other things): + +- An asset's `price()` malfunctions or is manipulated +- A collateral's default detection has a false positive or negative + +## Freezing + +The scope of freezing is the largest, and it should be used least frequently. Nearly all protocol interactions (`@custom:interaction`) are halted. Any refreshers (`@custom:refresher`) remain enabled, as well as `StRSR.stake()` and the "wrap up" routine `*.settleTrade()`. + +An important function of freezing is to provide a finite time for governance to push through a repair proposal an RToken in the event that a 0-day is discovered that requires a contract upgrade. + +### `Furnace.melt()` + +It is necessary for `Furnace.melt()` to remain emabled in order to allow `RTokenAsset.refresh()` to update its `price()`. Any revenue RToken that has already accumulated at the Furnace will continue to be melted, but the flow of new revenue RToken into the contract is halted. + +### `StRSR.payoutRewards()` + +It is necessary for `StRSR.payoutRewards()` to remain enabled in order for `StRSR.stake()` to use the up-to-date StRSR-RSR exchange rate. If it did not, then in the event of freezing there would be an unfair benefit to new stakers. Any revenue RSR that has already accumulated at the StRSR contract will continue to be paid out, but the flow of new revenue RSR into the contract is halted. + +### `StRSR.stake()` + +It is important for `StRSR.stake()` to remain emabled while frozen in order to allow honest RSR to flow into an RToken to vote against malicious governance proposals. + +### `*.settleTrade()` + +The settleTrade functionality must remain enabled in order to maintain the property that dutch auctions will discover the optimal price. If settleTrade were halted, it could become possible for a dutch auction to clear at a much lower price than it should have, simply because bidding was disabled during the earlier portion of the auction. diff --git a/docs/recollateralization.md b/docs/recollateralization.md index aecb345c94..06cf836594 100644 --- a/docs/recollateralization.md +++ b/docs/recollateralization.md @@ -64,21 +64,7 @@ If there does not exist a trade that meets these constraints, then the protocol #### Trade Sizing -The `IAsset` interface defines two types of prices: - -```solidity -/// @return low {UoA/tok} The lower end of the price estimate -/// @return high {UoA/tok} The upper end of the price estimate -function price() external view returns (uint192 low, uint192 high); - -/// lotLow should be nonzero when the asset might be worth selling -/// @return lotLow {UoA/tok} The lower end of the lot price estimate -/// @return lotHigh {UoA/tok} The upper end of the lot price estimate -function lotPrice() external view returns (uint192 lotLow, uint192 lotHigh); - -``` - -All trades have a worst-case exchange rate that is a function of (among other things) the selling asset's `lotPrice().low` and the buying asset's `lotPrice().high`. +All trades have a worst-case exchange rate that is a function of (among other things) the selling asset's `price().low` and the buying asset's `price().high`. #### Trade Examples diff --git a/scripts/addresses/84531-RTKN-tmp-deployments.json b/scripts/addresses/84531-RTKN-tmp-deployments.json new file mode 100644 index 0000000000..e7ec7dc68f --- /dev/null +++ b/scripts/addresses/84531-RTKN-tmp-deployments.json @@ -0,0 +1,19 @@ +{ + "facadeWrite": "0x0903048fD4E948c60451B41A48B35E0bafc0967F", + "main": "0x1274F03639932140bBd48D8376a39ee86EbFEe66", + "components": { + "assetRegistry": "0x09909aD4e15167f18dc42f86F12Ba85137Fc51a3", + "backingManager": "0xd53F642B04ba005E9A27FC82961F7c1563BEF301", + "basketHandler": "0xE9E22548C92EF74c02A9dab73c33eBcEb53cA216", + "broker": "0x7466593929d61308C89ce651029B7019E644b398", + "distributor": "0xd3e333fb488e7DF8BA49D98399a7f42d7fAc7b2C", + "furnace": "0xfe702Ff577B0a9B3865a59af31D039fA92739d39", + "rsrTrader": "0xF0c203Be2ac6747C107D119FCb3d8BED28d9A2db", + "rTokenTrader": "0xc2C5542ceF5d6C8c79b538ff3c3DA976720F93bf", + "rToken": "0x41d5a65ba05bEB7C5Ce01FF3eFb2c52eF2D46469", + "stRSR": "0xB8f96Ec61B4f209F7562bC10375b374f8305De97" + }, + "rTokenAsset": "0xa9063D1153DA2160A298ea83CA388c827c623A5D", + "governance": "0x326A8309f9b5f1ee06e832cdc168eac7feBA2Bea", + "timelock": "0x9686C510f9b5d101c75f659D0Fd3De20c01649dE" +} diff --git a/scripts/confirmation/1_confirm_assets.ts b/scripts/confirmation/1_confirm_assets.ts index 0c62bfbac1..b5ea27b8ec 100644 --- a/scripts/confirmation/1_confirm_assets.ts +++ b/scripts/confirmation/1_confirm_assets.ts @@ -2,7 +2,8 @@ import hre from 'hardhat' import { getChainId } from '../../common/blockchain-utils' import { developmentChains, networkConfig } from '../../common/configuration' -import { CollateralStatus } from '../../common/constants' +import { CollateralStatus, MAX_UINT192 } from '../../common/constants' +import { getLatestBlockTimestamp } from '#/utils/time' import { getDeploymentFile, IAssetCollDeployments, @@ -27,18 +28,20 @@ async function main() { const assets = Object.values(assetsColls.assets) const collateral = Object.values(assetsColls.collateral) - // Confirm lotPrice() == price() for (const a of assets) { console.log(`confirming asset ${a}`) const asset = await hre.ethers.getContractAt('Asset', a) - const [lotLow, lotHigh] = await asset.lotPrice() const [low, high] = await asset.price() // {UoA/tok} - if (low.eq(0) || high.eq(0)) throw new Error('misconfigured oracle') - - if (!lotLow.eq(low) || !lotHigh.eq(high)) { - console.log('lotLow, low, lotHigh, high', lotLow, low, lotHigh, high) - throw new Error('lot price off') - } + const timestamp = await getLatestBlockTimestamp(hre) + if ( + low.eq(0) || + low.eq(MAX_UINT192) || + high.eq(0) || + high.eq(MAX_UINT192) || + await asset.lastSave() !== timestamp || + await asset.lastSave() !== timestamp + ) + throw new Error('misconfigured oracle') } // Collateral @@ -49,14 +52,18 @@ async function main() { if ((await coll.status()) != CollateralStatus.SOUND) throw new Error('collateral unsound') - const [lotLow, lotHigh] = await coll.lotPrice() const [low, high] = await coll.price() // {UoA/tok} - if (low.eq(0) || high.eq(0)) throw new Error('misconfigured oracle') - - if (!lotLow.eq(low) || !lotHigh.eq(high)) { - console.log('lotLow, low, lotHigh, high', lotLow, low, lotHigh, high) - throw new Error('lot price off') - } + const timestamp = await getLatestBlockTimestamp(hre) + if ( + low.eq(0) || + low.eq(MAX_UINT192) || + high.eq(0) || + high.eq(MAX_UINT192) || + await coll.lastSave() !== timestamp || + await coll.lastSave() !== timestamp + ) + throw new Error('misconfigured oracle') + } } } diff --git a/scripts/deploy.ts b/scripts/deploy.ts index eb9d82f713..e2916e7d00 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -62,6 +62,8 @@ async function main() { 'phase2-assets/collaterals/deploy_cbeth_collateral.ts', 'phase2-assets/collaterals/deploy_morpho_aavev2_plugin.ts', 'phase2-assets/collaterals/deploy_aave_v3_usdc.ts', + 'phase2-assets/collaterals/deploy_yearn_v2_curve_usdc.ts', + 'phase2-assets/collaterals/deploy_yearn_v2_curve_usdp.ts', 'phase2-assets/collaterals/deploy_sfrax.ts' ) } else if (chainId == '8453' || chainId == '84531') { diff --git a/scripts/deployment/phase1-common/1_deploy_libraries.ts b/scripts/deployment/phase1-common/1_deploy_libraries.ts index 35fc34e373..78a4efa683 100644 --- a/scripts/deployment/phase1-common/1_deploy_libraries.ts +++ b/scripts/deployment/phase1-common/1_deploy_libraries.ts @@ -8,7 +8,6 @@ import { BasketLibP1, CvxMining, RecollateralizationLibP1 } from '../../../typec let tradingLib: RecollateralizationLibP1 let basketLib: BasketLibP1 -let cvxMiningLib: CvxMining async function main() { // ==== Read Configuration ==== @@ -16,7 +15,7 @@ async function main() { const chainId = await getChainId(hre) console.log( - `Deploying TradingLib, BasketLib, and CvxMining to network ${hre.network.name} (${chainId}) with burner account: ${burner.address}` + `Deploying TradingLib, BasketLib to network ${hre.network.name} (${chainId}) with burner account: ${burner.address}` ) if (!networkConfig[chainId]) { @@ -46,20 +45,9 @@ async function main() { fs.writeFileSync(deploymentFilename, JSON.stringify(deployments, null, 2)) - // Deploy CvxMining external library - if (!baseL2Chains.includes(hre.network.name)) { - const CvxMiningFactory = await ethers.getContractFactory('CvxMining') - cvxMiningLib = await CvxMiningFactory.connect(burner).deploy() - await cvxMiningLib.deployed() - deployments.cvxMiningLib = cvxMiningLib.address - - fs.writeFileSync(deploymentFilename, JSON.stringify(deployments, null, 2)) - } - console.log(`Deployed to ${hre.network.name} (${chainId}): TradingLib: ${tradingLib.address} BasketLib: ${basketLib.address} - CvxMiningLib: ${cvxMiningLib ? cvxMiningLib.address : 'N/A'} Deployment file: ${deploymentFilename}`) } diff --git a/scripts/deployment/phase1-common/3_deploy_rsrAsset.ts b/scripts/deployment/phase1-common/3_deploy_rsrAsset.ts index e67a33f602..f33da05e81 100644 --- a/scripts/deployment/phase1-common/3_deploy_rsrAsset.ts +++ b/scripts/deployment/phase1-common/3_deploy_rsrAsset.ts @@ -6,7 +6,7 @@ import { networkConfig } from '../../../common/configuration' import { ZERO_ADDRESS } from '../../../common/constants' import { fp } from '../../../common/numbers' import { getDeploymentFile, getDeploymentFilename, IDeployments } from '../../deployment/common' -import { priceTimeout, oracleTimeout, validateImplementations } from '../../deployment/utils' +import { priceTimeout, validateImplementations } from '../../deployment/utils' import { Asset } from '../../../typechain' let rsrAsset: Asset @@ -36,7 +36,7 @@ async function main() { tokenAddress: deployments.prerequisites.RSR, rewardToken: ZERO_ADDRESS, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24h + oracleTimeout: '86400', // 24h }) rsrAsset = await ethers.getContractAt('Asset', rsrAssetAddr) diff --git a/scripts/deployment/phase2-assets/1_deploy_assets.ts b/scripts/deployment/phase2-assets/1_deploy_assets.ts index cab49a5515..93a8a69392 100644 --- a/scripts/deployment/phase2-assets/1_deploy_assets.ts +++ b/scripts/deployment/phase2-assets/1_deploy_assets.ts @@ -10,7 +10,7 @@ import { IAssetCollDeployments, fileExists, } from '../../deployment/common' -import { priceTimeout, oracleTimeout } from '../../deployment/utils' +import { priceTimeout } from '../../deployment/utils' import { Asset } from '../../../typechain' async function main() { @@ -44,7 +44,7 @@ async function main() { oracleError: fp('0.01').toString(), // 1% tokenAddress: networkConfig[chainId].tokens.stkAAVE, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr }) await (await ethers.getContractAt('Asset', stkAAVEAsset)).refresh() @@ -60,7 +60,7 @@ async function main() { oracleError: fp('0.01').toString(), // 1% tokenAddress: networkConfig[chainId].tokens.COMP, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr }) await (await ethers.getContractAt('Asset', compAsset)).refresh() diff --git a/scripts/deployment/phase2-assets/2_deploy_collateral.ts b/scripts/deployment/phase2-assets/2_deploy_collateral.ts index cd8fdaa334..5a58c3bea8 100644 --- a/scripts/deployment/phase2-assets/2_deploy_collateral.ts +++ b/scripts/deployment/phase2-assets/2_deploy_collateral.ts @@ -12,7 +12,7 @@ import { getDeploymentFilename, fileExists, } from '../common' -import { combinedError, priceTimeout, oracleTimeout, revenueHiding } from '../utils' +import { combinedError, priceTimeout, revenueHiding } from '../utils' import { ICollateral, ATokenMock, StaticATokenLM } from '../../../typechain' async function main() { @@ -43,7 +43,7 @@ async function main() { let collateral: ICollateral /******** Deploy Fiat Collateral - DAI **************************/ - const daiOracleTimeout = baseL2Chains.includes(hre.network.name) ? 86400 : 3600 // 24 hr (Base) or 1 hour + const daiOracleTimeout = baseL2Chains.includes(hre.network.name) ? '86400' : '3600' // 24 hr (Base) or 1 hour const daiOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% if (networkConfig[chainId].tokens.DAI && networkConfig[chainId].chainlinkFeeds.DAI) { @@ -53,7 +53,7 @@ async function main() { oracleError: daiOracleError.toString(), tokenAddress: networkConfig[chainId].tokens.DAI, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, daiOracleTimeout).toString(), + oracleTimeout: daiOracleTimeout, targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(daiOracleError).toString(), delayUntilDefault: bn('86400').toString(), // 24h @@ -69,7 +69,7 @@ async function main() { fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) } - const usdcOracleTimeout = 86400 // 24 hr + const usdcOracleTimeout = '86400' // 24 hr const usdcOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% /******** Deploy Fiat Collateral - USDC **************************/ @@ -80,7 +80,7 @@ async function main() { oracleError: usdcOracleError.toString(), tokenAddress: networkConfig[chainId].tokens.USDC, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, usdcOracleTimeout).toString(), // 24 hr + oracleTimeout: usdcOracleTimeout, // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(usdcOracleError).toString(), delayUntilDefault: bn('86400').toString(), // 24h @@ -97,7 +97,7 @@ async function main() { } /******** Deploy Fiat Collateral - USDT **************************/ - const usdtOracleTimeout = 86400 // 24 hr + const usdtOracleTimeout = '86400' // 24 hr const usdtOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% if (networkConfig[chainId].tokens.USDT && networkConfig[chainId].chainlinkFeeds.USDT) { @@ -107,7 +107,7 @@ async function main() { oracleError: usdtOracleError.toString(), tokenAddress: networkConfig[chainId].tokens.USDT, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, usdtOracleTimeout).toString(), // 24 hr + oracleTimeout: usdtOracleTimeout, // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(usdtOracleError).toString(), delayUntilDefault: bn('86400').toString(), // 24h @@ -132,7 +132,7 @@ async function main() { oracleError: fp('0.01').toString(), // 1% tokenAddress: networkConfig[chainId].tokens.USDP, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.02').toString(), // 2% delayUntilDefault: bn('86400').toString(), // 24h @@ -156,7 +156,7 @@ async function main() { oracleError: fp('0.003').toString(), // 0.3% tokenAddress: networkConfig[chainId].tokens.TUSD, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.013').toString(), // 1.3% delayUntilDefault: bn('86400').toString(), // 24h @@ -179,7 +179,7 @@ async function main() { oracleError: fp('0.005').toString(), // 0.5% tokenAddress: networkConfig[chainId].tokens.BUSD, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.015').toString(), // 1.5% delayUntilDefault: bn('86400').toString(), // 24h @@ -203,7 +203,7 @@ async function main() { oracleError: usdcOracleError.toString(), tokenAddress: networkConfig[chainId].tokens.USDbC, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, usdcOracleTimeout).toString(), // 24 hr + oracleTimeout: usdcOracleTimeout, // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(usdcOracleError).toString(), // 1.3% delayUntilDefault: bn('86400').toString(), // 24h @@ -249,7 +249,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% staticAToken: adaiStaticToken.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -293,7 +293,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% staticAToken: ausdcStaticToken.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -337,7 +337,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% staticAToken: ausdtStaticToken.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -380,7 +380,7 @@ async function main() { oracleError: fp('0.005').toString(), // 0.5% staticAToken: abusdStaticToken.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.015').toString(), // 1.5% delayUntilDefault: bn('86400').toString(), // 24h @@ -424,7 +424,7 @@ async function main() { oracleError: fp('0.01').toString(), // 1% staticAToken: ausdpStaticToken.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.02').toString(), // 2% delayUntilDefault: bn('86400').toString(), // 24h @@ -445,6 +445,242 @@ async function main() { const btcOracleError = fp('0.005') // 0.5% const combinedBTCWBTCError = combinedError(wbtcOracleError, btcOracleError) + /*** Compound V2 not available in Base L2s */ + if (!baseL2Chains.includes(hre.network.name)) { + /******** Deploy CToken Fiat Collateral - cDAI **************************/ + const CTokenFactory = await ethers.getContractFactory('CTokenWrapper') + const cDai = await ethers.getContractAt('IERC20Metadata', networkConfig[chainId].tokens.cDAI!) + + const cDaiVault = await CTokenFactory.deploy( + networkConfig[chainId].tokens.cDAI!, + `${await cDai.name()} Vault`, + `${await cDai.symbol()}-VAULT`, + networkConfig[chainId].COMPTROLLER! + ) + + await cDaiVault.deployed() + + console.log( + `Deployed Vault for cDAI on ${hre.network.name} (${chainId}): ${cDaiVault.address} ` + ) + + const { collateral: cDaiCollateral } = await hre.run('deploy-ctoken-fiat-collateral', { + priceTimeout: priceTimeout.toString(), + priceFeed: networkConfig[chainId].chainlinkFeeds.DAI, + oracleError: fp('0.0025').toString(), // 0.25% + cToken: cDaiVault.address, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '3600', // 1 hr + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.0125').toString(), // 1.25% + delayUntilDefault: bn('86400').toString(), // 24h + revenueHiding: revenueHiding.toString(), + }) + collateral = await ethers.getContractAt('ICollateral', cDaiCollateral) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.cDAI = cDaiCollateral + assetCollDeployments.erc20s.cDAI = cDaiVault.address + deployedCollateral.push(cDaiCollateral.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + /******** Deploy CToken Fiat Collateral - cUSDC **************************/ + const cUsdc = await ethers.getContractAt('IERC20Metadata', networkConfig[chainId].tokens.cUSDC!) + + const cUsdcVault = await CTokenFactory.deploy( + networkConfig[chainId].tokens.cUSDC!, + `${await cUsdc.name()} Vault`, + `${await cUsdc.symbol()}-VAULT`, + networkConfig[chainId].COMPTROLLER! + ) + + await cUsdcVault.deployed() + + console.log( + `Deployed Vault for cUSDC on ${hre.network.name} (${chainId}): ${cUsdcVault.address} ` + ) + + const { collateral: cUsdcCollateral } = await hre.run('deploy-ctoken-fiat-collateral', { + priceTimeout: priceTimeout.toString(), + priceFeed: networkConfig[chainId].chainlinkFeeds.USDC, + oracleError: fp('0.0025').toString(), // 0.25% + cToken: cUsdcVault.address, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '86400', // 24 hr + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.0125').toString(), // 1.25% + delayUntilDefault: bn('86400').toString(), // 24h + revenueHiding: revenueHiding.toString(), + }) + collateral = await ethers.getContractAt('ICollateral', cUsdcCollateral) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.cUSDC = cUsdcCollateral + assetCollDeployments.erc20s.cUSDC = cUsdcVault.address + deployedCollateral.push(cUsdcCollateral.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + /******** Deploy CToken Fiat Collateral - cUSDT **************************/ + const cUsdt = await ethers.getContractAt('IERC20Metadata', networkConfig[chainId].tokens.cUSDT!) + + const cUsdtVault = await CTokenFactory.deploy( + networkConfig[chainId].tokens.cUSDT!, + `${await cUsdt.name()} Vault`, + `${await cUsdt.symbol()}-VAULT`, + networkConfig[chainId].COMPTROLLER! + ) + + await cUsdtVault.deployed() + + console.log( + `Deployed Vault for cUSDT on ${hre.network.name} (${chainId}): ${cUsdtVault.address} ` + ) + + const { collateral: cUsdtCollateral } = await hre.run('deploy-ctoken-fiat-collateral', { + priceTimeout: priceTimeout.toString(), + priceFeed: networkConfig[chainId].chainlinkFeeds.USDT, + oracleError: fp('0.0025').toString(), // 0.25% + cToken: cUsdtVault.address, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '86400', // 24 hr + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.0125').toString(), // 1.25% + delayUntilDefault: bn('86400').toString(), // 24h + revenueHiding: revenueHiding.toString(), + }) + collateral = await ethers.getContractAt('ICollateral', cUsdtCollateral) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.cUSDT = cUsdtCollateral + assetCollDeployments.erc20s.cUSDT = cUsdtVault.address + deployedCollateral.push(cUsdtCollateral.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + /******** Deploy CToken Fiat Collateral - cUSDP **************************/ + const cUsdp = await ethers.getContractAt('IERC20Metadata', networkConfig[chainId].tokens.cUSDP!) + + const cUsdpVault = await CTokenFactory.deploy( + networkConfig[chainId].tokens.cUSDP!, + `${await cUsdp.name()} Vault`, + `${await cUsdp.symbol()}-VAULT`, + networkConfig[chainId].COMPTROLLER! + ) + + await cUsdpVault.deployed() + + console.log( + `Deployed Vault for cUSDP on ${hre.network.name} (${chainId}): ${cUsdpVault.address} ` + ) + + const { collateral: cUsdpCollateral } = await hre.run('deploy-ctoken-fiat-collateral', { + priceTimeout: priceTimeout.toString(), + priceFeed: networkConfig[chainId].chainlinkFeeds.USDP, + oracleError: fp('0.01').toString(), // 1% + cToken: cUsdpVault.address, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '3600', // 1 hr + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.02').toString(), // 2% + delayUntilDefault: bn('86400').toString(), // 24h + revenueHiding: revenueHiding.toString(), + }) + collateral = await ethers.getContractAt('ICollateral', cUsdpCollateral) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.cUSDP = cUsdpCollateral + assetCollDeployments.erc20s.cUSDP = cUsdpVault.address + deployedCollateral.push(cUsdpCollateral.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + /******** Deploy CToken Non-Fiat Collateral - cWBTC **************************/ + const cWBTC = await ethers.getContractAt('IERC20Metadata', networkConfig[chainId].tokens.cWBTC!) + + const cWBTCVault = await CTokenFactory.deploy( + networkConfig[chainId].tokens.cWBTC!, + `${await cWBTC.name()} Vault`, + `${await cWBTC.symbol()}-VAULT`, + networkConfig[chainId].COMPTROLLER! + ) + + await cWBTCVault.deployed() + + console.log( + `Deployed Vault for cWBTC on ${hre.network.name} (${chainId}): ${cWBTCVault.address} ` + ) + + const { collateral: cWBTCCollateral } = await hre.run('deploy-ctoken-nonfiat-collateral', { + priceTimeout: priceTimeout.toString(), + referenceUnitFeed: networkConfig[chainId].chainlinkFeeds.WBTC, + targetUnitFeed: networkConfig[chainId].chainlinkFeeds.BTC, + combinedOracleError: combinedBTCWBTCError.toString(), + cToken: cWBTCVault.address, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '86400', // 24 hr + targetUnitOracleTimeout: '3600', // 1 hr + targetName: hre.ethers.utils.formatBytes32String('BTC'), + defaultThreshold: fp('0.01').add(combinedBTCWBTCError).toString(), // ~3.5% + delayUntilDefault: bn('86400').toString(), // 24h + revenueHiding: revenueHiding.toString(), + }) + collateral = await ethers.getContractAt('ICollateral', cWBTCCollateral) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.cWBTC = cWBTCCollateral + assetCollDeployments.erc20s.cWBTC = cWBTCVault.address + deployedCollateral.push(cWBTCCollateral.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + /******** Deploy CToken Self-Referential Collateral - cETH **************************/ + const cETH = await ethers.getContractAt('IERC20Metadata', networkConfig[chainId].tokens.cETH!) + + const cETHVault = await CTokenFactory.deploy( + networkConfig[chainId].tokens.cETH!, + `${await cETH.name()} Vault`, + `${await cETH.symbol()}-VAULT`, + networkConfig[chainId].COMPTROLLER! + ) + + await cETHVault.deployed() + + console.log( + `Deployed Vault for cETH on ${hre.network.name} (${chainId}): ${cETHVault.address} ` + ) + + const { collateral: cETHCollateral } = await hre.run( + 'deploy-ctoken-selfreferential-collateral', + { + priceTimeout: priceTimeout.toString(), + priceFeed: networkConfig[chainId].chainlinkFeeds.ETH, + oracleError: fp('0.005').toString(), // 0.5% + cToken: cETHVault.address, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '3600', // 1 hr + targetName: hre.ethers.utils.formatBytes32String('ETH'), + revenueHiding: revenueHiding.toString(), + referenceERC20Decimals: '18', + } + ) + collateral = await ethers.getContractAt('ICollateral', cETHCollateral) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.cETH = cETHCollateral + assetCollDeployments.erc20s.cETH = cETHVault.address + deployedCollateral.push(cETHCollateral.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + } + /******** Deploy Non-Fiat Collateral - wBTC **************************/ if ( networkConfig[chainId].tokens.WBTC && @@ -458,8 +694,8 @@ async function main() { combinedOracleError: combinedBTCWBTCError.toString(), tokenAddress: networkConfig[chainId].tokens.WBTC, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr - targetUnitOracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '86400', // 24 hr + targetUnitOracleTimeout: '3600', // 1 hr targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: fp('0.01').add(combinedBTCWBTCError).toString(), // ~3.5% delayUntilDefault: bn('86400').toString(), // 24h @@ -477,7 +713,7 @@ async function main() { /******** Deploy Self Referential Collateral - wETH **************************/ if (networkConfig[chainId].tokens.WETH && networkConfig[chainId].chainlinkFeeds.ETH) { - const ethOracleTimeout = baseL2Chains.includes(hre.network.name) ? 1200 : 3600 // 20 min (Base) or 1 hr + const ethOracleTimeout = baseL2Chains.includes(hre.network.name) ? '1200' : '3600' // 20 min (Base) or 1 hr const ethOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.0015') : fp('0.005') // 0.15% (Base) or 0.5% const { collateral: wETHCollateral } = await hre.run('deploy-selfreferential-collateral', { @@ -486,7 +722,7 @@ async function main() { oracleError: ethOracleError.toString(), tokenAddress: networkConfig[chainId].tokens.WETH, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, ethOracleTimeout).toString(), + oracleTimeout: ethOracleTimeout, targetName: hre.ethers.utils.formatBytes32String('ETH'), }) collateral = await ethers.getContractAt('ICollateral', wETHCollateral) @@ -515,8 +751,8 @@ async function main() { oracleError: eurtError.toString(), // 2% tokenAddress: networkConfig[chainId].tokens.EURT, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr - targetUnitOracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr + targetUnitOracleTimeout: '86400', // 24 hr targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: fp('0.03').toString(), // 3% delayUntilDefault: bn('86400').toString(), // 24h diff --git a/scripts/deployment/phase2-assets/assets/deploy_crv.ts b/scripts/deployment/phase2-assets/assets/deploy_crv.ts index f0db202b9b..80eb3f6c19 100644 --- a/scripts/deployment/phase2-assets/assets/deploy_crv.ts +++ b/scripts/deployment/phase2-assets/assets/deploy_crv.ts @@ -10,7 +10,7 @@ import { IAssetCollDeployments, fileExists, } from '../../../deployment/common' -import { priceTimeout, oracleTimeout } from '../../../deployment/utils' +import { priceTimeout } from '../../../deployment/utils' import { Asset } from '../../../../typechain' async function main() { @@ -43,7 +43,7 @@ async function main() { oracleError: fp('0.01').toString(), // 1% tokenAddress: networkConfig[chainId].tokens.CRV, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr }) await (await ethers.getContractAt('Asset', crvAsset)).refresh() diff --git a/scripts/deployment/phase2-assets/assets/deploy_cvx.ts b/scripts/deployment/phase2-assets/assets/deploy_cvx.ts index 1c5aaa57ce..cb7eb2d5d2 100644 --- a/scripts/deployment/phase2-assets/assets/deploy_cvx.ts +++ b/scripts/deployment/phase2-assets/assets/deploy_cvx.ts @@ -10,7 +10,7 @@ import { IAssetCollDeployments, fileExists, } from '../../../deployment/common' -import { priceTimeout, oracleTimeout } from '../../../deployment/utils' +import { priceTimeout } from '../../../deployment/utils' import { Asset } from '../../../../typechain' async function main() { @@ -43,7 +43,7 @@ async function main() { oracleError: fp('0.02').toString(), // 2% tokenAddress: networkConfig[chainId].tokens.CVX, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr }) await (await ethers.getContractAt('Asset', cvxAsset)).refresh() diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_aave_v3_usdbc.ts b/scripts/deployment/phase2-assets/collaterals/deploy_aave_v3_usdbc.ts index e7fbc98512..f930201a21 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_aave_v3_usdbc.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_aave_v3_usdbc.ts @@ -13,7 +13,7 @@ import { } from '../../common' import { bn, fp } from '#/common/numbers' import { AaveV3FiatCollateral } from '../../../../typechain' -import { priceTimeout, revenueHiding, oracleTimeout } from '../../utils' +import { priceTimeout, revenueHiding } from '../../utils' // This file specifically deploys Aave V3 USDC collateral @@ -77,7 +77,7 @@ async function main() { oracleError: fp('0.003'), // 3% erc20: erc20.address, maxTradeVolume: fp('1e6'), - oracleTimeout: oracleTimeout(chainId, bn('86400')), // 24 hr + oracleTimeout: '86400', // 24 hr targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.013'), delayUntilDefault: bn('86400'), diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_aave_v3_usdc.ts b/scripts/deployment/phase2-assets/collaterals/deploy_aave_v3_usdc.ts index 2d56f9d3f9..2d4eb8112d 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_aave_v3_usdc.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_aave_v3_usdc.ts @@ -13,7 +13,7 @@ import { } from '../../common' import { bn, fp } from '#/common/numbers' import { AaveV3FiatCollateral } from '../../../../typechain' -import { priceTimeout, revenueHiding, oracleTimeout } from '../../utils' +import { priceTimeout, revenueHiding } from '../../utils' // This file specifically deploys Aave V3 USDC collateral @@ -68,7 +68,7 @@ async function main() { ) /******** Deploy Aave V3 USDC collateral plugin **************************/ - const usdcOracleTimeout = 86400 // 24 hr + const usdcOracleTimeout = '86400' // 24 hr const usdcOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% const CollateralFactory = await ethers.getContractFactory('AaveV3FiatCollateral') @@ -79,7 +79,7 @@ async function main() { oracleError: usdcOracleError, erc20: erc20.address, maxTradeVolume: fp('1e6'), - oracleTimeout: oracleTimeout(chainId, usdcOracleTimeout), + oracleTimeout: usdcOracleTimeout, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(usdcOracleError), delayUntilDefault: bn('86400'), diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_cbeth_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_cbeth_collateral.ts index eab6850157..18984099fe 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_cbeth_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_cbeth_collateral.ts @@ -12,7 +12,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { priceTimeout, oracleTimeout, combinedError } from '../../utils' +import { priceTimeout, combinedError } from '../../utils' import { CBEthCollateral, CBEthCollateralL2, @@ -62,14 +62,14 @@ async function main() { oracleError: oracleError.toString(), // 0.5% & 2%, erc20: networkConfig[chainId].tokens.cbETH!, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr, + oracleTimeout: '3600', // 1 hr, targetName: hre.ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0.02').add(oracleError).toString(), // ~4.5% delayUntilDefault: bn('86400').toString(), // 24h }, fp('1e-4').toString(), // revenueHiding = 0.01% networkConfig[chainId].chainlinkFeeds.cbETH!, // refPerTokChainlinkFeed - oracleTimeout(chainId, '86400').toString() // refPerTokChainlinkTimeout + '86400' // refPerTokChainlinkTimeout ) await collateral.deployed() await (await collateral.refresh()).wait() @@ -89,16 +89,16 @@ async function main() { oracleError: oracleError.toString(), // 0.15% & 0.5%, erc20: networkConfig[chainId].tokens.cbETH!, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '1200').toString(), // 20 min + oracleTimeout: '1200', // 20 min targetName: hre.ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0.02').add(oracleError).toString(), // ~2.5% delayUntilDefault: bn('86400').toString(), // 24h }, fp('1e-4').toString(), // revenueHiding = 0.01% networkConfig[chainId].chainlinkFeeds.cbETH!, // refPerTokChainlinkFeed - oracleTimeout(chainId, '86400').toString(), // refPerTokChainlinkTimeout + '86400', // refPerTokChainlinkTimeout networkConfig[chainId].chainlinkFeeds.cbETHETHexr!, // exchangeRateChainlinkFeed - oracleTimeout(chainId, '86400').toString() // exchangeRateChainlinkTimeout + '86400' // exchangeRateChainlinkTimeout ) await collateral.deployed() await (await collateral.refresh()).wait() diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_compound_v2_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_compound_v2_collateral.ts index d328a311dd..89b2464c55 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_compound_v2_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_compound_v2_collateral.ts @@ -12,8 +12,8 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { combinedError, priceTimeout, oracleTimeout, revenueHiding } from '../../utils' -import { ICollateral, ATokenMock, StaticATokenLM } from '../../../../typechain' +import { combinedError, priceTimeout, revenueHiding } from '../../utils' +import { ICollateral } from '../../../../typechain' async function main() { // ==== Read Configuration ==== @@ -71,7 +71,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% cToken: cDaiVault.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -109,7 +109,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% cToken: cUsdcVault.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -147,7 +147,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% cToken: cUsdtVault.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -185,7 +185,7 @@ async function main() { oracleError: fp('0.01').toString(), // 1% cToken: cUsdpVault.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.02').toString(), // 2% delayUntilDefault: bn('86400').toString(), // 24h @@ -224,8 +224,8 @@ async function main() { combinedOracleError: combinedBTCWBTCError.toString(), cToken: cWBTCVault.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr - targetUnitOracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '86400', // 24 hr + targetUnitOracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('BTC'), defaultThreshold: fp('0.01').add(combinedBTCWBTCError).toString(), // ~3.5% delayUntilDefault: bn('86400').toString(), // 24h @@ -265,7 +265,7 @@ async function main() { oracleError: fp('0.005').toString(), // 0.5% cToken: cETHVault.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('ETH'), revenueHiding: revenueHiding.toString(), referenceERC20Decimals: '18', diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_convex_rToken_metapool_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_convex_rToken_metapool_plugin.ts index dae7875b30..b382962e58 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_convex_rToken_metapool_plugin.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_convex_rToken_metapool_plugin.ts @@ -14,7 +14,7 @@ import { fileExists, } from '../../common' import { CurveStableRTokenMetapoolCollateral } from '../../../../typechain' -import { revenueHiding, oracleTimeout } from '../../utils' +import { revenueHiding } from '../../utils' import { CurvePoolType, DEFAULT_THRESHOLD, @@ -63,13 +63,10 @@ async function main() { /******** Deploy Convex Stable Metapool for eUSD/fraxBP **************************/ - const CvxMining = await ethers.getContractAt('CvxMining', deployments.cvxMiningLib) const CurveStableCollateralFactory = await hre.ethers.getContractFactory( 'CurveStableRTokenMetapoolCollateral' ) - const ConvexStakingWrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { - libraries: { CvxMining: CvxMining.address }, - }) + const ConvexStakingWrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper') const wPool = await ConvexStakingWrapperFactory.deploy() await wPool.deployed() @@ -87,7 +84,7 @@ async function main() { priceTimeout: PRICE_TIMEOUT, chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: oracleTimeout(chainId, USDC_ORACLE_TIMEOUT), // max of oracleTimeouts + oracleTimeout: USDC_ORACLE_TIMEOUT, // max of oracleTimeouts maxTradeVolume: MAX_TRADE_VOL, defaultThreshold: DEFAULT_THRESHOLD, // 2%: 1% error on FRAX oracle + 1% base defaultThreshold delayUntilDefault: RTOKEN_DELAY_UNTIL_DEFAULT, @@ -98,10 +95,7 @@ async function main() { curvePool: FRAX_BP, poolType: CurvePoolType.Plain, feeds: [[FRAX_USD_FEED], [USDC_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, FRAX_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[FRAX_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT]], oracleErrors: [[FRAX_ORACLE_ERROR], [USDC_ORACLE_ERROR]], lpToken: FRAX_BP_TOKEN, }, diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_metapool_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_metapool_plugin.ts index 72d0f7debe..6727ff25d7 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_metapool_plugin.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_metapool_plugin.ts @@ -13,7 +13,7 @@ import { fileExists, } from '../../common' import { CurveStableMetapoolCollateral } from '../../../../typechain' -import { revenueHiding, oracleTimeout } from '../../utils' +import { revenueHiding } from '../../utils' import { CurvePoolType, DELAY_UNTIL_DEFAULT, @@ -69,13 +69,10 @@ async function main() { /******** Deploy Convex Stable Metapool for MIM/3Pool **************************/ - const CvxMining = await ethers.getContractAt('CvxMining', deployments.cvxMiningLib) const CurveStableCollateralFactory = await hre.ethers.getContractFactory( 'CurveStableMetapoolCollateral' ) - const ConvexStakingWrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { - libraries: { CvxMining: CvxMining.address }, - }) + const ConvexStakingWrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper') const wPool = await ConvexStakingWrapperFactory.deploy() await wPool.deployed() @@ -105,11 +102,7 @@ async function main() { curvePool: THREE_POOL, poolType: CurvePoolType.Plain, feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], lpToken: THREE_POOL_TOKEN, }, diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_plugin.ts index 97a8fc4f2b..abd65d88b6 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_plugin.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_plugin.ts @@ -14,7 +14,7 @@ import { fileExists, } from '../../common' import { CurveStableCollateral } from '../../../../typechain' -import { revenueHiding, oracleTimeout } from '../../utils' +import { revenueHiding } from '../../utils' import { CurvePoolType, DAI_ORACLE_ERROR, @@ -65,11 +65,8 @@ async function main() { /******** Deploy Convex Stable Pool for 3pool **************************/ - const CvxMining = await ethers.getContractAt('CvxMining', deployments.cvxMiningLib) const CurveStableCollateralFactory = await hre.ethers.getContractFactory('CurveStableCollateral') - const ConvexStakingWrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { - libraries: { CvxMining: CvxMining.address }, - }) + const ConvexStakingWrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper') const w3Pool = await ConvexStakingWrapperFactory.deploy() await w3Pool.deployed() @@ -88,7 +85,7 @@ async function main() { priceTimeout: PRICE_TIMEOUT, chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: oracleTimeout(chainId, USDC_ORACLE_TIMEOUT), // max of oracleTimeouts + oracleTimeout: USDC_ORACLE_TIMEOUT, // max of oracleTimeouts maxTradeVolume: MAX_TRADE_VOL, defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -99,11 +96,7 @@ async function main() { curvePool: THREE_POOL, poolType: CurvePoolType.Plain, feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], lpToken: THREE_POOL_TOKEN, } diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_convex_volatile_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_convex_volatile_plugin.ts deleted file mode 100644 index ab71316676..0000000000 --- a/scripts/deployment/phase2-assets/collaterals/deploy_convex_volatile_plugin.ts +++ /dev/null @@ -1,142 +0,0 @@ -import fs from 'fs' -import hre, { ethers } from 'hardhat' -import { getChainId } from '../../../../common/blockchain-utils' -import { networkConfig } from '../../../../common/configuration' -import { bn } from '../../../../common/numbers' -import { expect } from 'chai' -import { CollateralStatus, ONE_ADDRESS } from '../../../../common/constants' -import { - getDeploymentFile, - getAssetCollDeploymentFilename, - IAssetCollDeployments, - IDeployments, - getDeploymentFilename, - fileExists, -} from '../../common' -import { CurveVolatileCollateral } from '../../../../typechain' -import { revenueHiding, oracleTimeout } from '../../utils' -import { - CurvePoolType, - BTC_USD_ORACLE_ERROR, - BTC_ORACLE_TIMEOUT, - BTC_USD_FEED, - DEFAULT_THRESHOLD, - DELAY_UNTIL_DEFAULT, - MAX_TRADE_VOL, - PRICE_TIMEOUT, - TRI_CRYPTO, - TRI_CRYPTO_TOKEN, - TRI_CRYPTO_CVX_POOL_ID, - WBTC_BTC_ORACLE_ERROR, - WETH_ORACLE_TIMEOUT, - WBTC_BTC_FEED, - WBTC_ORACLE_TIMEOUT, - WETH_USD_FEED, - WETH_ORACLE_ERROR, - USDT_ORACLE_ERROR, - USDT_ORACLE_TIMEOUT, - USDT_USD_FEED, -} from '../../../../test/plugins/individual-collateral/curve/constants' - -// This file specifically deploys Convex Volatile Plugin for Tricrypto - -async function main() { - // ==== Read Configuration ==== - const [deployer] = await hre.ethers.getSigners() - - const chainId = await getChainId(hre) - - console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) - with burner account: ${deployer.address}`) - - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - - // Get phase1 deployment - const phase1File = getDeploymentFilename(chainId) - if (!fileExists(phase1File)) { - throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) - } - const deployments = getDeploymentFile(phase1File) - - // Check previous step completed - const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) - const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) - - const deployedCollateral: string[] = [] - - /******** Deploy Convex Volatile Pool for 3pool **************************/ - - const CvxMining = await ethers.getContractAt('CvxMining', deployments.cvxMiningLib) - const CurveVolatileCollateralFactory = await hre.ethers.getContractFactory( - 'CurveVolatileCollateral' - ) - const ConvexStakingWrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { - libraries: { CvxMining: CvxMining.address }, - }) - - const w3Pool = await ConvexStakingWrapperFactory.deploy() - await w3Pool.deployed() - await (await w3Pool.initialize(TRI_CRYPTO_CVX_POOL_ID)).wait() - - console.log( - `Deployed wrapper for Convex Volatile TriCrypto on ${hre.network.name} (${chainId}): ${w3Pool.address} ` - ) - - const collateral = await CurveVolatileCollateralFactory.connect( - deployer - ).deploy( - { - erc20: w3Pool.address, - targetName: ethers.utils.formatBytes32String('TRICRYPTO'), - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero - oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: oracleTimeout(chainId, USDT_ORACLE_TIMEOUT), // max of oracleTimeouts - maxTradeVolume: MAX_TRADE_VOL, - defaultThreshold: DEFAULT_THRESHOLD, - delayUntilDefault: DELAY_UNTIL_DEFAULT, - }, - revenueHiding.toString(), - { - nTokens: 3, - curvePool: TRI_CRYPTO, - poolType: CurvePoolType.Plain, - feeds: [[USDT_USD_FEED], [WBTC_BTC_FEED, BTC_USD_FEED], [WETH_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, WBTC_ORACLE_TIMEOUT), oracleTimeout(chainId, BTC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, WETH_ORACLE_TIMEOUT)], - ], - oracleErrors: [ - [USDT_ORACLE_ERROR], - [WBTC_BTC_ORACLE_ERROR, BTC_USD_ORACLE_ERROR], - [WETH_ORACLE_ERROR], - ], - lpToken: TRI_CRYPTO_TOKEN, - } - ) - await collateral.deployed() - await (await collateral.refresh()).wait() - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - - console.log( - `Deployed Convex Volatile Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` - ) - - assetCollDeployments.collateral.cvxTriCrypto = collateral.address - assetCollDeployments.erc20s.cvxTriCrypto = w3Pool.address - deployedCollateral.push(collateral.address.toString()) - - fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) - - console.log(`Deployed collateral to ${hre.network.name} (${chainId}) - New deployments: ${deployedCollateral} - Deployment file: ${assetCollDeploymentFilename}`) -} - -main().catch((error) => { - console.error(error) - process.exitCode = 1 -}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdbc_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdbc_collateral.ts index 3f4755ff72..21e78893af 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdbc_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdbc_collateral.ts @@ -12,7 +12,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { priceTimeout, oracleTimeout, revenueHiding } from '../../utils' +import { priceTimeout, revenueHiding } from '../../utils' import { CTokenV3Collateral } from '../../../../typechain' import { ContractFactory } from 'ethers' @@ -61,7 +61,7 @@ async function main() { const CTokenV3Factory: ContractFactory = await hre.ethers.getContractFactory('CTokenV3Collateral') - const usdcOracleTimeout = 86400 // 24 hr + const usdcOracleTimeout = '86400' // 24 hr const usdcOracleError = fp('0.003') // 0.3% (Base) const collateral = await CTokenV3Factory.connect(deployer).deploy( @@ -71,7 +71,7 @@ async function main() { oracleError: usdcOracleError.toString(), erc20: erc20.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, usdcOracleTimeout).toString(), // 24h hr, + oracleTimeout: usdcOracleTimeout, // 24h hr, targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(usdcOracleError).toString(), // 1% + 0.3% delayUntilDefault: bn('86400').toString(), // 24h diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts index 873b8fbcc0..a05ac5bbc7 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts @@ -12,7 +12,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { priceTimeout, oracleTimeout, revenueHiding } from '../../utils' +import { priceTimeout, revenueHiding } from '../../utils' import { CTokenV3Collateral } from '../../../../typechain' import { ContractFactory } from 'ethers' @@ -59,7 +59,7 @@ async function main() { const CTokenV3Factory: ContractFactory = await hre.ethers.getContractFactory('CTokenV3Collateral') - const usdcOracleTimeout = 86400 // 24 hr + const usdcOracleTimeout = '86400' // 24 hr const usdcOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% const collateral = await CTokenV3Factory.connect(deployer).deploy( @@ -69,7 +69,7 @@ async function main() { oracleError: usdcOracleError.toString(), erc20: erc20.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, usdcOracleTimeout).toString(), // 24h hr, + oracleTimeout: usdcOracleTimeout, // 24h hr, targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(usdcOracleError).toString(), delayUntilDefault: bn('86400').toString(), // 24h diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_curve_rToken_metapool_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_curve_rToken_metapool_plugin.ts index e886d6d806..c2d760a2f9 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_curve_rToken_metapool_plugin.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_curve_rToken_metapool_plugin.ts @@ -13,7 +13,7 @@ import { fileExists, } from '../../common' import { CurveStableRTokenMetapoolCollateral } from '../../../../typechain' -import { revenueHiding, oracleTimeout } from '../../utils' +import { revenueHiding } from '../../utils' import { CRV, CurvePoolType, @@ -88,7 +88,7 @@ async function main() { priceTimeout: PRICE_TIMEOUT, chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: oracleTimeout(chainId, USDC_ORACLE_TIMEOUT), // max of oracleTimeouts + oracleTimeout: USDC_ORACLE_TIMEOUT, // max of oracleTimeouts maxTradeVolume: MAX_TRADE_VOL, defaultThreshold: DEFAULT_THRESHOLD, // 2%: 1% error on FRAX oracle + 1% base defaultThreshold delayUntilDefault: RTOKEN_DELAY_UNTIL_DEFAULT, @@ -99,10 +99,7 @@ async function main() { curvePool: FRAX_BP, poolType: CurvePoolType.Plain, feeds: [[FRAX_USD_FEED], [USDC_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, FRAX_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[FRAX_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT]], oracleErrors: [[FRAX_ORACLE_ERROR], [USDC_ORACLE_ERROR]], lpToken: FRAX_BP_TOKEN, }, diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_metapool_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_metapool_plugin.ts index f97882d214..e62840d7e1 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_metapool_plugin.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_metapool_plugin.ts @@ -12,7 +12,7 @@ import { fileExists, } from '../../common' import { CurveStableMetapoolCollateral } from '../../../../typechain' -import { revenueHiding, oracleTimeout } from '../../utils' +import { revenueHiding } from '../../utils' import { CRV, CurvePoolType, @@ -105,11 +105,7 @@ async function main() { curvePool: THREE_POOL, poolType: CurvePoolType.Plain, feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], lpToken: THREE_POOL_TOKEN, }, diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_plugin.ts index b2d6462d6c..6b9f415d01 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_plugin.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_plugin.ts @@ -13,7 +13,7 @@ import { fileExists, } from '../../common' import { CurveStableCollateral } from '../../../../typechain' -import { revenueHiding, oracleTimeout } from '../../utils' +import { revenueHiding } from '../../utils' import { CRV, CurvePoolType, @@ -89,7 +89,7 @@ async function main() { priceTimeout: PRICE_TIMEOUT, chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: oracleTimeout(chainId, USDC_ORACLE_TIMEOUT), // max of oracleTimeouts + oracleTimeout: USDC_ORACLE_TIMEOUT, // max of oracleTimeouts maxTradeVolume: MAX_TRADE_VOL, defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -100,11 +100,7 @@ async function main() { curvePool: THREE_POOL, poolType: CurvePoolType.Plain, feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], lpToken: THREE_POOL_TOKEN, } diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_curve_volatile_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_curve_volatile_plugin.ts deleted file mode 100644 index f3b6e3e615..0000000000 --- a/scripts/deployment/phase2-assets/collaterals/deploy_curve_volatile_plugin.ts +++ /dev/null @@ -1,142 +0,0 @@ -import fs from 'fs' -import hre, { ethers } from 'hardhat' -import { getChainId } from '../../../../common/blockchain-utils' -import { networkConfig } from '../../../../common/configuration' -import { bn } from '../../../../common/numbers' -import { expect } from 'chai' -import { CollateralStatus, ONE_ADDRESS } from '../../../../common/constants' -import { - getDeploymentFile, - getAssetCollDeploymentFilename, - IAssetCollDeployments, - getDeploymentFilename, - fileExists, -} from '../../common' -import { CurveVolatileCollateral } from '../../../../typechain' -import { revenueHiding, oracleTimeout } from '../../utils' -import { - CRV, - CurvePoolType, - BTC_USD_ORACLE_ERROR, - BTC_ORACLE_TIMEOUT, - BTC_USD_FEED, - DEFAULT_THRESHOLD, - DELAY_UNTIL_DEFAULT, - MAX_TRADE_VOL, - PRICE_TIMEOUT, - TRI_CRYPTO, - TRI_CRYPTO_TOKEN, - TRI_CRYPTO_GAUGE, - WBTC_BTC_ORACLE_ERROR, - WETH_ORACLE_TIMEOUT, - WBTC_BTC_FEED, - WBTC_ORACLE_TIMEOUT, - WETH_USD_FEED, - WETH_ORACLE_ERROR, - USDT_ORACLE_ERROR, - USDT_ORACLE_TIMEOUT, - USDT_USD_FEED, -} from '../../../../test/plugins/individual-collateral/curve/constants' - -// Deploy Curve Volatile Plugin for Tricrypto - -async function main() { - // ==== Read Configuration ==== - const [deployer] = await hre.ethers.getSigners() - - const chainId = await getChainId(hre) - - console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) - with burner account: ${deployer.address}`) - - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - - // Get phase1 deployment - const phase1File = getDeploymentFilename(chainId) - if (!fileExists(phase1File)) { - throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) - } - - // Check previous step completed - const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) - const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) - - const deployedCollateral: string[] = [] - - /******** Deploy Curve Volatile Pool for 3pool **************************/ - - const CurveVolatileCollateralFactory = await hre.ethers.getContractFactory( - 'CurveVolatileCollateral' - ) - const CurveStakingWrapperFactory = await ethers.getContractFactory('CurveGaugeWrapper') - const w3Pool = await CurveStakingWrapperFactory.deploy( - TRI_CRYPTO_TOKEN, - 'Wrapped Curve.fi USD-BTC-ETH', - 'wcrv3crypto', - CRV, - TRI_CRYPTO_GAUGE - ) - await w3Pool.deployed() - - console.log( - `Deployed wrapper for Curve Volatile TriCrypto on ${hre.network.name} (${chainId}): ${w3Pool.address} ` - ) - - const collateral = await CurveVolatileCollateralFactory.connect( - deployer - ).deploy( - { - erc20: w3Pool.address, - targetName: ethers.utils.formatBytes32String('TRICRYPTO'), - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero - oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: oracleTimeout(chainId, USDT_ORACLE_TIMEOUT), // max of oracleTimeouts - maxTradeVolume: MAX_TRADE_VOL, - defaultThreshold: DEFAULT_THRESHOLD, - delayUntilDefault: DELAY_UNTIL_DEFAULT, - }, - revenueHiding.toString(), - { - nTokens: 3, - curvePool: TRI_CRYPTO, - poolType: CurvePoolType.Plain, - feeds: [[USDT_USD_FEED], [WBTC_BTC_FEED, BTC_USD_FEED], [WETH_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, WBTC_ORACLE_TIMEOUT), oracleTimeout(chainId, BTC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, WETH_ORACLE_TIMEOUT)], - ], - oracleErrors: [ - [USDT_ORACLE_ERROR], - [WBTC_BTC_ORACLE_ERROR, BTC_USD_ORACLE_ERROR], - [WETH_ORACLE_ERROR], - ], - lpToken: TRI_CRYPTO_TOKEN, - } - ) - await collateral.deployed() - await (await collateral.refresh()).wait() - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - - console.log( - `Deployed Curve Volatile Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` - ) - - assetCollDeployments.collateral.crvTriCrypto = collateral.address - assetCollDeployments.erc20s.crvTriCrypto = w3Pool.address - deployedCollateral.push(collateral.address.toString()) - - fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) - - console.log(`Deployed collateral to ${hre.network.name} (${chainId}) - New deployments: ${deployedCollateral} - Deployment file: ${assetCollDeploymentFilename}`) -} - -main().catch((error) => { - console.error(error) - process.exitCode = 1 -}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_dsr_sdai.ts b/scripts/deployment/phase2-assets/collaterals/deploy_dsr_sdai.ts index 26ab8341ac..aa6e840436 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_dsr_sdai.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_dsr_sdai.ts @@ -13,7 +13,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { priceTimeout, oracleTimeout } from '../../utils' +import { priceTimeout } from '../../utils' import { SDaiCollateral } from '../../../../typechain' import { ContractFactory } from 'ethers' @@ -54,12 +54,12 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% erc20: networkConfig[chainId].tokens.sDAI, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h }, - fp('1e-6').toString(), // revenueHiding = 0.0001% + bn(0), // does not require revenue hiding POT ) await collateral.deployed() diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_flux_finance_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_flux_finance_collateral.ts index af7258ec4c..6acbcccf8f 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_flux_finance_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_flux_finance_collateral.ts @@ -12,7 +12,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { priceTimeout, oracleTimeout, revenueHiding } from '../../utils' +import { priceTimeout, revenueHiding } from '../../utils' import { ICollateral } from '../../../../typechain' async function main() { @@ -49,7 +49,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% cToken: fUsdc.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -74,7 +74,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% cToken: fUsdt.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -99,7 +99,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% cToken: fDai.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -124,7 +124,7 @@ async function main() { oracleError: fp('0.01').toString(), // 1% cToken: fFrax.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.02').toString(), // 2% delayUntilDefault: bn('86400').toString(), // 24h diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_lido_wsteth_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_lido_wsteth_collateral.ts index bc6a8b160a..30884eae2d 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_lido_wsteth_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_lido_wsteth_collateral.ts @@ -12,7 +12,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { priceTimeout, oracleTimeout } from '../../utils' +import { priceTimeout } from '../../utils' import { LidoStakedEthCollateral } from '../../../../typechain' import { ContractFactory } from 'ethers' @@ -79,14 +79,14 @@ async function main() { oracleError: fp('0.01').toString(), // 1%: only for stETHUSD feed erc20: networkConfig[chainId].tokens.wstETH, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr, + oracleTimeout: '3600', // 1 hr, targetName: hre.ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0.025').toString(), // 2.5% = 2% + 0.5% stethEth feed oracleError delayUntilDefault: bn('86400').toString(), // 24h }, fp('1e-4').toString(), // revenueHiding = 0.01% stethEthOracleAddress, // targetPerRefChainlinkFeed - oracleTimeout(chainId, '86400').toString() // targetPerRefChainlinkTimeout + '86400' // targetPerRefChainlinkTimeout ) await collateral.deployed() await (await collateral.refresh()).wait() diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_morpho_aavev2_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_morpho_aavev2_plugin.ts index 535b4fd9f1..cb659b1889 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_morpho_aavev2_plugin.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_morpho_aavev2_plugin.ts @@ -11,7 +11,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { priceTimeout, oracleTimeout, combinedError } from '../../utils' +import { priceTimeout, combinedError } from '../../utils' async function main() { // ==== Read Configuration ==== @@ -46,11 +46,9 @@ async function main() { const MorphoTokenisedDepositFactory = await ethers.getContractFactory( 'MorphoAaveV2TokenisedDeposit' ) - const maUSDT = await MorphoTokenisedDepositFactory.deploy({ morphoController: networkConfig[chainId].MORPHO_AAVE_CONTROLLER!, morphoLens: networkConfig[chainId].MORPHO_AAVE_LENS!, - rewardsDistributor: networkConfig[chainId].MORPHO_REWARDS_DISTRIBUTOR!, underlyingERC20: networkConfig[chainId].tokens.USDT!, poolToken: networkConfig[chainId].tokens.aUSDT!, rewardToken: networkConfig[chainId].tokens.MORPHO!, @@ -59,7 +57,6 @@ async function main() { const maUSDC = await MorphoTokenisedDepositFactory.deploy({ morphoController: networkConfig[chainId].MORPHO_AAVE_CONTROLLER!, morphoLens: networkConfig[chainId].MORPHO_AAVE_LENS!, - rewardsDistributor: networkConfig[chainId].MORPHO_REWARDS_DISTRIBUTOR!, underlyingERC20: networkConfig[chainId].tokens.USDC!, poolToken: networkConfig[chainId].tokens.aUSDC!, rewardToken: networkConfig[chainId].tokens.MORPHO!, @@ -68,7 +65,6 @@ async function main() { const maDAI = await MorphoTokenisedDepositFactory.deploy({ morphoController: networkConfig[chainId].MORPHO_AAVE_CONTROLLER!, morphoLens: networkConfig[chainId].MORPHO_AAVE_LENS!, - rewardsDistributor: networkConfig[chainId].MORPHO_REWARDS_DISTRIBUTOR!, underlyingERC20: networkConfig[chainId].tokens.DAI!, poolToken: networkConfig[chainId].tokens.aDAI!, rewardToken: networkConfig[chainId].tokens.MORPHO!, @@ -77,7 +73,6 @@ async function main() { const maWBTC = await MorphoTokenisedDepositFactory.deploy({ morphoController: networkConfig[chainId].MORPHO_AAVE_CONTROLLER!, morphoLens: networkConfig[chainId].MORPHO_AAVE_LENS!, - rewardsDistributor: networkConfig[chainId].MORPHO_REWARDS_DISTRIBUTOR!, underlyingERC20: networkConfig[chainId].tokens.WBTC!, poolToken: networkConfig[chainId].tokens.aWBTC!, rewardToken: networkConfig[chainId].tokens.MORPHO!, @@ -86,7 +81,6 @@ async function main() { const maWETH = await MorphoTokenisedDepositFactory.deploy({ morphoController: networkConfig[chainId].MORPHO_AAVE_CONTROLLER!, morphoLens: networkConfig[chainId].MORPHO_AAVE_LENS!, - rewardsDistributor: networkConfig[chainId].MORPHO_REWARDS_DISTRIBUTOR!, underlyingERC20: networkConfig[chainId].tokens.WETH!, poolToken: networkConfig[chainId].tokens.aWETH!, rewardToken: networkConfig[chainId].tokens.MORPHO!, @@ -95,7 +89,6 @@ async function main() { const maStETH = await MorphoTokenisedDepositFactory.deploy({ morphoController: networkConfig[chainId].MORPHO_AAVE_CONTROLLER!, morphoLens: networkConfig[chainId].MORPHO_AAVE_LENS!, - rewardsDistributor: networkConfig[chainId].MORPHO_REWARDS_DISTRIBUTOR!, underlyingERC20: networkConfig[chainId].tokens.stETH!, poolToken: networkConfig[chainId].tokens.astETH!, rewardToken: networkConfig[chainId].tokens.MORPHO!, @@ -127,7 +120,7 @@ async function main() { priceTimeout: priceTimeout.toString(), oracleError: stablesOracleError.toString(), maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 1 hr + oracleTimeout: '86400', // 24h targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: stablesOracleError.add(fp('0.01')), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -165,6 +158,7 @@ async function main() { const collateral = await FiatCollateralFactory.connect(deployer).deploy( { ...baseStableConfig, + oracleTimeout: '3600', // 1 hr chainlinkFeed: networkConfig[chainId].chainlinkFeeds.DAI!, erc20: maDAI.address, }, @@ -185,16 +179,16 @@ async function main() { priceTimeout: priceTimeout, oracleError: combinedBTCWBTCError, maxTradeVolume: fp('1e6'), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600'), // 1 hr + oracleTimeout: '86400', // 24 hr targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: fp('0.01').add(combinedBTCWBTCError), // ~3.5% delayUntilDefault: bn('86400'), // 24h - chainlinkFeed: networkConfig[chainId].chainlinkFeeds.BTC!, // {UoA/target} + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.WBTC!, // {target/ref} erc20: maWBTC.address, }, revenueHiding, - networkConfig[chainId].chainlinkFeeds.WBTC!, // {target/ref} - oracleTimeout(chainId, '86400').toString() // 1 hr + networkConfig[chainId].chainlinkFeeds.BTC!, // {UoA/target} + '3600' // 1 hr ) assetCollDeployments.collateral.maWBTC = collateral.address deployedCollateral.push(collateral.address.toString()) @@ -208,7 +202,7 @@ async function main() { priceTimeout: priceTimeout, oracleError: fp('0.005'), maxTradeVolume: fp('1e6'), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600'), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0'), // 0% -- no soft default for self-referential collateral delayUntilDefault: bn('86400'), // 24h @@ -237,16 +231,16 @@ async function main() { priceTimeout: priceTimeout, oracleError: combinedOracleErrors, maxTradeVolume: fp('1e6'), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600'), // 1 hr + oracleTimeout: '86400', // 24 hr targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0.01').add(combinedOracleErrors), // ~1.5% delayUntilDefault: bn('86400'), // 24h - chainlinkFeed: networkConfig[chainId].chainlinkFeeds.ETH!, // {UoA/target} + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.stETHETH!, // {target/ref} erc20: maStETH.address, }, revenueHiding, - networkConfig[chainId].chainlinkFeeds.stETHETH!, // {target/ref} - oracleTimeout(chainId, '86400').toString() // 1 hr + networkConfig[chainId].chainlinkFeeds.ETH!, // {UoA/target} + '3600' // 1 hr ) assetCollDeployments.collateral.maStETH = collateral.address deployedCollateral.push(collateral.address.toString()) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_rocket_pool_reth_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_rocket_pool_reth_collateral.ts index 7f8b308993..d90520b97a 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_rocket_pool_reth_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_rocket_pool_reth_collateral.ts @@ -12,7 +12,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { priceTimeout, oracleTimeout, combinedError } from '../../utils' +import { priceTimeout, combinedError } from '../../utils' import { RethCollateral } from '../../../../typechain' import { ContractFactory } from 'ethers' @@ -70,14 +70,14 @@ async function main() { oracleError: oracleError.toString(), // 0.5% & 2% erc20: networkConfig[chainId].tokens.rETH, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr, + oracleTimeout: '3600', // 1 hr, targetName: hre.ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0.02').add(oracleError).toString(), // ~4.5% delayUntilDefault: bn('86400').toString(), // 24h }, fp('1e-4').toString(), // revenueHiding = 0.01% rethOracleAddress, // refPerTokChainlinkFeed - oracleTimeout(chainId, '86400').toString() // refPerTokChainlinkTimeout + '86400' // refPerTokChainlinkTimeout ) await collateral.deployed() await (await collateral.refresh()).wait() diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_sfrax.ts b/scripts/deployment/phase2-assets/collaterals/deploy_sfrax.ts index 1510377f20..600505d84e 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_sfrax.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_sfrax.ts @@ -12,7 +12,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { priceTimeout, oracleTimeout } from '../../utils' +import { priceTimeout } from '../../utils' import { SFraxCollateral } from '../../../../typechain' import { ContractFactory } from 'ethers' @@ -53,7 +53,7 @@ async function main() { oracleError: fp('0.01').toString(), // 1% erc20: networkConfig[chainId].tokens.sFRAX, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.02').toString(), // 2% = 1% oracleError + 1% buffer delayUntilDefault: bn('86400').toString(), // 24h diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_stargate_usdc_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_stargate_usdc_collateral.ts index 5db4436ea9..dfd837767f 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_stargate_usdc_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_stargate_usdc_collateral.ts @@ -12,17 +12,12 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { revenueHiding, priceTimeout, oracleTimeout } from '../../utils' +import { revenueHiding, priceTimeout } from '../../utils' import { StargatePoolFiatCollateral, StargatePoolFiatCollateral__factory, } from '../../../../typechain' import { ContractFactory } from 'ethers' - -import { - STAKING_CONTRACT, - SUSDC, -} from '../../../../test/plugins/individual-collateral/stargate/constants' import { useEnv } from '#/utils/env' async function main() { @@ -51,8 +46,10 @@ async function main() { /******** Deploy Stargate USDC Wrapper **************************/ - const WrapperFactory: ContractFactory = await hre.ethers.getContractFactory('StargateRewardableWrapper') - let chainIdKey = useEnv('FORK_NETWORK', 'mainnet') == 'mainnet' ? '1' : '8453' + const WrapperFactory: ContractFactory = await hre.ethers.getContractFactory( + 'StargateRewardableWrapper' + ) + const chainIdKey = useEnv('FORK_NETWORK', 'mainnet') == 'mainnet' ? '1' : '8453' let USDC_NAME = 'USDC' let name = 'Wrapped Stargate USDC' let symbol = 'wsgUSDC' @@ -93,7 +90,7 @@ async function main() { oracleError: oracleError.toString(), erc20: erc20.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainIdKey, '86400').toString(), // 24h hr, + oracleTimeout: '86400', // 24h hr, targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(oracleError).toString(), delayUntilDefault: bn('86400').toString(), // 24h @@ -104,7 +101,9 @@ async function main() { await (await collateral.refresh()).wait() expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - console.log(`Deployed Stargate ${USDC_NAME} to ${hre.network.name} (${chainIdKey}): ${collateral.address}`) + console.log( + `Deployed Stargate ${USDC_NAME} to ${hre.network.name} (${chainIdKey}): ${collateral.address}` + ) if (chainIdKey == '8453') { assetCollDeployments.collateral.wsgUSDbC = collateral.address diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_stargate_usdt_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_stargate_usdt_collateral.ts index 8a43556a52..4ac4e4c6c8 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_stargate_usdt_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_stargate_usdt_collateral.ts @@ -12,7 +12,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { revenueHiding, priceTimeout, oracleTimeout } from '../../utils' +import { revenueHiding, priceTimeout } from '../../utils' import { StargatePoolFiatCollateral, StargatePoolFiatCollateral__factory, @@ -77,7 +77,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25%, erc20: erc20.address, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24h hr, + oracleTimeout: '86400', // 24h hr, targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_yearn_v2_curve_usdc.ts b/scripts/deployment/phase2-assets/collaterals/deploy_yearn_v2_curve_usdc.ts new file mode 100644 index 0000000000..e8f9f1f154 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_yearn_v2_curve_usdc.ts @@ -0,0 +1,104 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { bn, fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { priceTimeout } from '../../utils' +import { YearnV2CurveFiatCollateral } from '../../../../typechain' +import { ContractFactory } from 'ethers' +import { + PRICE_PER_SHARE_HELPER, + YVUSDC_LP_TOKEN, +} from '../../../../test/plugins/individual-collateral/yearnv2/constants' + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy Yearn V2 Curve Fiat Collateral - yvCurveUSDCcrvUSD **************************/ + + const YearnV2CurveCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'YearnV2CurveFiatCollateral' + ) + + const collateral = await YearnV2CurveCollateralFactory.connect( + deployer + ).deploy( + { + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDC, // not used but can't be empty + oracleError: fp('0.0025').toString(), // not used but can't be empty + erc20: networkConfig[chainId].tokens.yvCurveUSDCcrvUSD, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '86400', // 24hr -- max of all oracleTimeouts + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.015').toString(), // 1.5% = max oracleError + 1% + delayUntilDefault: bn('86400').toString(), // 24h + }, + fp('1e-6').toString(), // revenueHiding = 0.0001%, low since underlying curve pool should be up-only + { + nTokens: '2', + curvePool: YVUSDC_LP_TOKEN, + poolType: '0', + feeds: [ + [networkConfig[chainId].chainlinkFeeds.USDC], + [networkConfig[chainId].chainlinkFeeds.crvUSD], + ], + oracleTimeouts: [['86400'], ['86400']], + oracleErrors: [[fp('0.0025').toString()], [fp('0.005').toString()]], + lpToken: YVUSDC_LP_TOKEN, + }, + PRICE_PER_SHARE_HELPER + ) + await collateral.deployed() + + console.log( + `Deployed Yearn Curve yvUSDCcrvUSD to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.yvCurveUSDCcrvUSD = collateral.address + assetCollDeployments.erc20s.yvCurveUSDCcrvUSD = networkConfig[chainId].tokens.yvCurveUSDCcrvUSD + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_yearn_v2_curve_usdp.ts b/scripts/deployment/phase2-assets/collaterals/deploy_yearn_v2_curve_usdp.ts new file mode 100644 index 0000000000..cbb2c89cc0 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_yearn_v2_curve_usdp.ts @@ -0,0 +1,104 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { bn, fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { priceTimeout } from '../../utils' +import { YearnV2CurveFiatCollateral } from '../../../../typechain' +import { ContractFactory } from 'ethers' +import { + PRICE_PER_SHARE_HELPER, + YVUSDP_LP_TOKEN, +} from '../../../../test/plugins/individual-collateral/yearnv2/constants' + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy Yearn V2 Curve Fiat Collateral - yvCurveUSDPcrvUSD **************************/ + + const YearnV2CurveCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'YearnV2CurveFiatCollateral' + ) + + const collateral = await YearnV2CurveCollateralFactory.connect( + deployer + ).deploy( + { + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDP, // not used but can't be empty + oracleError: fp('0.0025').toString(), // not used but can't be empty + erc20: networkConfig[chainId].tokens.yvCurveUSDPcrvUSD, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '86400', // 24hr -- max of all oracleTimeouts + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.02').toString(), // 2% = max oracleError + 1% + delayUntilDefault: bn('86400').toString(), // 24h + }, + fp('1e-6').toString(), // revenueHiding = 0.0001%, low since underlying curve pool should be up-only + { + nTokens: '2', + curvePool: YVUSDP_LP_TOKEN, + poolType: '0', + feeds: [ + [networkConfig[chainId].chainlinkFeeds.USDP], + [networkConfig[chainId].chainlinkFeeds.crvUSD], + ], + oracleTimeouts: [['3600'], ['86400']], + oracleErrors: [[fp('0.01').toString()], [fp('0.005').toString()]], + lpToken: YVUSDP_LP_TOKEN, + }, + PRICE_PER_SHARE_HELPER + ) + await collateral.deployed() + + console.log( + `Deployed Yearn Curve yvUSDPcrvUSD to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.yvCurveUSDPcrvUSD = collateral.address + assetCollDeployments.erc20s.yvCurveUSDPcrvUSD = networkConfig[chainId].tokens.yvCurveUSDPcrvUSD + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/utils.ts b/scripts/deployment/utils.ts index 84dad2be5e..a8c2083e09 100644 --- a/scripts/deployment/utils.ts +++ b/scripts/deployment/utils.ts @@ -2,7 +2,7 @@ import hre, { tenderly } from 'hardhat' import * as readline from 'readline' import axios from 'axios' import { exec } from 'child_process' -import { BigNumber, BigNumberish } from 'ethers' +import { BigNumber } from 'ethers' import { bn, fp } from '../../common/numbers' import { IComponents, baseL2Chains } from '../../common/configuration' import { isValidContract } from '../../common/blockchain-utils' @@ -13,13 +13,6 @@ export const priceTimeout = bn('604800') // 1 week export const revenueHiding = fp('1e-6') // 1 part in a million -export const longOracleTimeout = bn('4294967296') - -// Returns the base plus 1 minute -export const oracleTimeout = (chainId: string, base: BigNumberish) => { - return chainId == '1' || chainId == '8453' ? bn('60').add(base) : longOracleTimeout -} - export const combinedError = (x: BigNumber, y: BigNumber): BigNumber => { return fp('1').add(x).mul(fp('1').add(y)).div(fp('1')).sub(fp('1')) } diff --git a/scripts/exhaustive-tests/run-1.sh b/scripts/exhaustive-tests/run-1.sh index fbad597e14..bf214a6ba0 100644 --- a/scripts/exhaustive-tests/run-1.sh +++ b/scripts/exhaustive-tests/run-1.sh @@ -1,3 +1,3 @@ -echo "Running RToken & Furnace exhaustive tests for commit hash: " +echo "Running Broker, RToken, & Furnace exhaustive tests for commit hash: " git rev-parse HEAD; -NODE_OPTIONS=--max-old-space-size=30000 EXTREME=1 SLOW=1 PROTO_IMPL=1 npx hardhat test test/RTokenExtremes.test.ts test/Furnace.test.ts; +NODE_OPTIONS=--max-old-space-size=30000 EXTREME=1 SLOW=1 PROTO_IMPL=1 npx hardhat test test/RTokenExtremes.test.ts test/Broker.test.ts test/Furnace.test.ts; diff --git a/scripts/verification/6_verify_collateral.ts b/scripts/verification/6_verify_collateral.ts index f857f28877..fbcfe84d23 100644 --- a/scripts/verification/6_verify_collateral.ts +++ b/scripts/verification/6_verify_collateral.ts @@ -8,13 +8,7 @@ import { getAssetCollDeploymentFilename, IAssetCollDeployments, } from '../deployment/common' -import { - combinedError, - priceTimeout, - oracleTimeout, - revenueHiding, - verifyContract, -} from '../deployment/utils' +import { combinedError, priceTimeout, revenueHiding, verifyContract } from '../deployment/utils' import { ATokenMock, ATokenFiatCollateral, ICToken, CTokenFiatCollateral } from '../../typechain' let deployments: IAssetCollDeployments @@ -47,7 +41,7 @@ async function main() { oracleError: daiOracleError.toString(), erc20: networkConfig[chainId].tokens.DAI, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, daiOracleTimeout).toString(), + oracleTimeout: daiOracleTimeout, targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(daiOracleError).toString(), delayUntilDefault: bn('86400').toString(), // 24h @@ -71,7 +65,7 @@ async function main() { oracleError: usdcOracleError.toString(), erc20: networkConfig[chainId].tokens.USDbC, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, usdcOracleTimeout).toString(), + oracleTimeout: usdcOracleTimeout, targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(usdcOracleError).toString(), delayUntilDefault: bn('86400').toString(), // 24h @@ -99,7 +93,7 @@ async function main() { 'Static ' + (await aToken.name()), 's' + (await aToken.symbol()), ], - 'contracts/plugins/assets/aave/StaticATokenLM.sol:StaticATokenLM' + 'contracts/plugins/assets/aave/vendor/StaticATokenLM.sol:StaticATokenLM' ) /******** Verify ATokenFiatCollateral - aDAI **************************/ await verifyContract( @@ -112,7 +106,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% erc20: await aTokenCollateral.erc20(), maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -151,7 +145,7 @@ async function main() { oracleError: fp('0.0025').toString(), // 0.25% erc20: deployments.erc20s.cDAI, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -176,13 +170,13 @@ async function main() { oracleError: combinedBTCWBTCError.toString(), erc20: deployments.erc20s.cWBTC, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr + oracleTimeout: '86400', // 24 hr targetName: hre.ethers.utils.formatBytes32String('BTC'), defaultThreshold: fp('0.01').add(combinedBTCWBTCError).toString(), // ~3.5% delayUntilDefault: bn('86400').toString(), // 24h }, networkConfig[chainId].chainlinkFeeds.BTC, - oracleTimeout(chainId, '3600').toString(), + '3600', revenueHiding.toString(), ], 'contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol:CTokenNonFiatCollateral' @@ -198,7 +192,7 @@ async function main() { oracleError: fp('0.005').toString(), // 0.5% erc20: deployments.erc20s.cETH, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('ETH'), defaultThreshold: '0', delayUntilDefault: '0', @@ -219,13 +213,13 @@ async function main() { oracleError: combinedBTCWBTCError.toString(), erc20: networkConfig[chainId].tokens.WBTC, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), + oracleTimeout: '86400', // 24h targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: fp('0.01').add(combinedBTCWBTCError).toString(), // ~3.5% delayUntilDefault: bn('86400').toString(), // 24h }, networkConfig[chainId].chainlinkFeeds.BTC, - oracleTimeout(chainId, '3600').toString(), + '3600', ], 'contracts/plugins/assets/NonFiatCollateral.sol:NonFiatCollateral' ) @@ -244,7 +238,7 @@ async function main() { oracleError: ethOracleError.toString(), // 0.5% erc20: networkConfig[chainId].tokens.WETH, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, ethOracleTimeout).toString(), + oracleTimeout: ethOracleTimeout, targetName: hre.ethers.utils.formatBytes32String('ETH'), defaultThreshold: '0', delayUntilDefault: '0', @@ -264,13 +258,13 @@ async function main() { oracleError: fp('0.02').toString(), // 2% erc20: networkConfig[chainId].tokens.EURT, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), + oracleTimeout: '86400', // 24hr targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: fp('0.03').toString(), // 3% delayUntilDefault: bn('86400').toString(), // 24h }, networkConfig[chainId].chainlinkFeeds.EUR, - oracleTimeout(chainId, '86400').toString(), + '86400', ], 'contracts/plugins/assets/EURFiatCollateral.sol:EURFiatCollateral' ) diff --git a/scripts/verification/collateral-plugins/verify_aave_v3_usdbc.ts b/scripts/verification/collateral-plugins/verify_aave_v3_usdbc.ts index edb092d1af..3a373573dc 100644 --- a/scripts/verification/collateral-plugins/verify_aave_v3_usdbc.ts +++ b/scripts/verification/collateral-plugins/verify_aave_v3_usdbc.ts @@ -7,7 +7,7 @@ import { IAssetCollDeployments, } from '../../deployment/common' import { fp, bn } from '../../../common/numbers' -import { priceTimeout, oracleTimeout, verifyContract, revenueHiding } from '../../deployment/utils' +import { priceTimeout, verifyContract, revenueHiding } from '../../deployment/utils' let deployments: IAssetCollDeployments @@ -54,7 +54,7 @@ async function main() { priceTimeout: priceTimeout.toString(), chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDC!, oracleError: fp('0.003').toString(), // 3% - oracleTimeout: oracleTimeout(chainId, bn('86400')).toString(), // 24 hr + oracleTimeout: '86400', // 24 hr maxTradeVolume: fp('1e6').toString(), defaultThreshold: fp('0.013').toString(), delayUntilDefault: bn('86400').toString(), diff --git a/scripts/verification/collateral-plugins/verify_aave_v3_usdc.ts b/scripts/verification/collateral-plugins/verify_aave_v3_usdc.ts index 1486c37cab..3ffce9a0a5 100644 --- a/scripts/verification/collateral-plugins/verify_aave_v3_usdc.ts +++ b/scripts/verification/collateral-plugins/verify_aave_v3_usdc.ts @@ -7,7 +7,7 @@ import { IAssetCollDeployments, } from '../../deployment/common' import { fp, bn } from '../../../common/numbers' -import { priceTimeout, oracleTimeout, verifyContract, revenueHiding } from '../../deployment/utils' +import { priceTimeout, verifyContract, revenueHiding } from '../../deployment/utils' let deployments: IAssetCollDeployments @@ -39,7 +39,7 @@ async function main() { ) /******** Verify Aave V3 USDC plugin **************************/ - const usdcOracleTimeout = 86400 // 24 hr + const usdcOracleTimeout = '86400' // 24 hr const usdcOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% await verifyContract( @@ -52,7 +52,7 @@ async function main() { priceTimeout: priceTimeout.toString(), chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDC!, oracleError: usdcOracleError.toString(), - oracleTimeout: oracleTimeout(chainId, usdcOracleTimeout).toString(), // 24 hr + oracleTimeout: usdcOracleTimeout, // 24 hr maxTradeVolume: fp('1e6').toString(), defaultThreshold: fp('0.01').add(usdcOracleError).toString(), delayUntilDefault: bn('86400').toString(), diff --git a/scripts/verification/collateral-plugins/verify_cbeth.ts b/scripts/verification/collateral-plugins/verify_cbeth.ts index 4e58ad88d5..9b52d6323b 100644 --- a/scripts/verification/collateral-plugins/verify_cbeth.ts +++ b/scripts/verification/collateral-plugins/verify_cbeth.ts @@ -7,7 +7,7 @@ import { getAssetCollDeploymentFilename, IAssetCollDeployments, } from '../../deployment/common' -import { priceTimeout, oracleTimeout, verifyContract, combinedError } from '../../deployment/utils' +import { priceTimeout, verifyContract, combinedError } from '../../deployment/utils' let deployments: IAssetCollDeployments @@ -40,14 +40,14 @@ async function main() { oracleError: oracleError.toString(), // 0.5% & 2% erc20: networkConfig[chainId].tokens.cbETH!, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: hre.ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0.02').add(oracleError).toString(), // ~4.5% delayUntilDefault: bn('86400').toString(), // 24h }, fp('1e-4'), // revenueHiding = 0.01% networkConfig[chainId].chainlinkFeeds.cbETH!, // refPerTokChainlinkFeed - oracleTimeout(chainId, '86400').toString(), // refPerTokChainlinkTimeout + '86400', // refPerTokChainlinkTimeout ], 'contracts/plugins/assets/cbeth/CBETHCollateral.sol:CBEthCollateral' ) @@ -63,16 +63,16 @@ async function main() { oracleError: oracleError.toString(), // 0.15% & 0.5%, erc20: networkConfig[chainId].tokens.cbETH!, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '1200').toString(), // 20 min + oracleTimeout: '1200', // 20 min targetName: hre.ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0.02').add(oracleError).toString(), // ~2.5% delayUntilDefault: bn('86400').toString(), // 24h }, fp('1e-4'), // revenueHiding = 0.01% networkConfig[chainId].chainlinkFeeds.cbETH!, // refPerTokChainlinkFeed - oracleTimeout(chainId, '86400').toString(), // refPerTokChainlinkTimeout + '86400', // refPerTokChainlinkTimeout networkConfig[chainId].chainlinkFeeds.cbETHETHexr!, // exchangeRateChainlinkFeed - oracleTimeout(chainId, '86400').toString(), // exchangeRateChainlinkTimeout + '86400', // exchangeRateChainlinkTimeout ], 'contracts/plugins/assets/cbeth/CBETHCollateralL2.sol:CBEthCollateralL2' ) diff --git a/scripts/verification/collateral-plugins/verify_convex_stable.ts b/scripts/verification/collateral-plugins/verify_convex_stable.ts index 3c22ae2557..127ef39143 100644 --- a/scripts/verification/collateral-plugins/verify_convex_stable.ts +++ b/scripts/verification/collateral-plugins/verify_convex_stable.ts @@ -11,7 +11,7 @@ import { IDeployments, } from '../../deployment/common' import { verifyContract } from '../../deployment/utils' -import { revenueHiding, oracleTimeout } from '../../deployment/utils' +import { revenueHiding } from '../../deployment/utils' import { CurvePoolType, DAI_ORACLE_ERROR, @@ -61,17 +61,7 @@ async function main() { chainId, await w3PoolCollateral.erc20(), [], - 'contracts/plugins/assets/curve/cvx/vendor/ConvexStakingWrapper.sol:ConvexStakingWrapper', - { CvxMining: coreDeployments.cvxMiningLib } - ) - - /******** Verify CvxMining Lib **************************/ - - await verifyContract( - chainId, - coreDeployments.cvxMiningLib, - [], - 'contracts/plugins/assets/curve/cvx/vendor/CvxMining.sol:CvxMining' + 'contracts/plugins/assets/curve/cvx/vendor/ConvexStakingWrapper.sol:ConvexStakingWrapper' ) /******** Verify 3Pool plugin **************************/ @@ -85,7 +75,7 @@ async function main() { priceTimeout: PRICE_TIMEOUT, chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: oracleTimeout(chainId, USDC_ORACLE_TIMEOUT), // max of oracleTimeouts + oracleTimeout: USDC_ORACLE_TIMEOUT, // max of oracleTimeouts maxTradeVolume: MAX_TRADE_VOL, defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -96,11 +86,7 @@ async function main() { curvePool: THREE_POOL, poolType: CurvePoolType.Plain, feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], lpToken: THREE_POOL_TOKEN, }, diff --git a/scripts/verification/collateral-plugins/verify_convex_stable_metapool.ts b/scripts/verification/collateral-plugins/verify_convex_stable_metapool.ts index 440f8854b2..fedb2d418a 100644 --- a/scripts/verification/collateral-plugins/verify_convex_stable_metapool.ts +++ b/scripts/verification/collateral-plugins/verify_convex_stable_metapool.ts @@ -7,7 +7,7 @@ import { IAssetCollDeployments, } from '../../deployment/common' import { verifyContract } from '../../deployment/utils' -import { revenueHiding, oracleTimeout } from '../../deployment/utils' +import { revenueHiding } from '../../deployment/utils' import { CurvePoolType, DAI_ORACLE_ERROR, @@ -75,11 +75,7 @@ async function main() { curvePool: THREE_POOL, poolType: CurvePoolType.Plain, feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], lpToken: THREE_POOL_TOKEN, }, diff --git a/scripts/verification/collateral-plugins/verify_convex_stable_rtoken_metapool.ts b/scripts/verification/collateral-plugins/verify_convex_stable_rtoken_metapool.ts index 771f6bc767..400c23d10e 100644 --- a/scripts/verification/collateral-plugins/verify_convex_stable_rtoken_metapool.ts +++ b/scripts/verification/collateral-plugins/verify_convex_stable_rtoken_metapool.ts @@ -9,7 +9,7 @@ import { IAssetCollDeployments, } from '../../deployment/common' import { verifyContract } from '../../deployment/utils' -import { revenueHiding, oracleTimeout } from '../../deployment/utils' +import { revenueHiding } from '../../deployment/utils' import { CurvePoolType, DEFAULT_THRESHOLD, @@ -59,7 +59,7 @@ async function main() { priceTimeout: PRICE_TIMEOUT, chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: oracleTimeout(chainId, USDC_ORACLE_TIMEOUT), + oracleTimeout: USDC_ORACLE_TIMEOUT, maxTradeVolume: MAX_TRADE_VOL, defaultThreshold: DEFAULT_THRESHOLD, // 2%: 1% error on FRAX oracle + 1% base defaultThreshold delayUntilDefault: RTOKEN_DELAY_UNTIL_DEFAULT, @@ -70,10 +70,7 @@ async function main() { curvePool: FRAX_BP, poolType: CurvePoolType.Plain, feeds: [[FRAX_USD_FEED], [USDC_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, FRAX_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[FRAX_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT]], oracleErrors: [[FRAX_ORACLE_ERROR], [USDC_ORACLE_ERROR]], lpToken: FRAX_BP_TOKEN, }, diff --git a/scripts/verification/collateral-plugins/verify_convex_volatile.ts b/scripts/verification/collateral-plugins/verify_convex_volatile.ts deleted file mode 100644 index 8c48da0e56..0000000000 --- a/scripts/verification/collateral-plugins/verify_convex_volatile.ts +++ /dev/null @@ -1,98 +0,0 @@ -import hre, { ethers } from 'hardhat' -import { getChainId } from '../../../common/blockchain-utils' -import { developmentChains, networkConfig } from '../../../common/configuration' -import { bn } from '../../../common/numbers' -import { ONE_ADDRESS } from '../../../common/constants' -import { - getDeploymentFile, - getAssetCollDeploymentFilename, - IAssetCollDeployments, -} from '../../deployment/common' -import { verifyContract } from '../../deployment/utils' -import { revenueHiding, oracleTimeout } from '../../deployment/utils' -import { - CurvePoolType, - BTC_USD_ORACLE_ERROR, - BTC_ORACLE_TIMEOUT, - BTC_USD_FEED, - DEFAULT_THRESHOLD, - DELAY_UNTIL_DEFAULT, - MAX_TRADE_VOL, - PRICE_TIMEOUT, - TRI_CRYPTO, - TRI_CRYPTO_TOKEN, - WBTC_BTC_ORACLE_ERROR, - WETH_ORACLE_TIMEOUT, - WBTC_BTC_FEED, - WBTC_ORACLE_TIMEOUT, - WETH_USD_FEED, - WETH_ORACLE_ERROR, - USDT_ORACLE_ERROR, - USDT_ORACLE_TIMEOUT, - USDT_USD_FEED, -} from '../../../test/plugins/individual-collateral/curve/constants' - -let deployments: IAssetCollDeployments - -async function main() { - // ********** Read config ********** - const chainId = await getChainId(hre) - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - - if (developmentChains.includes(hre.network.name)) { - throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) - } - - const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) - deployments = getDeploymentFile(assetCollDeploymentFilename) - - const wTriCrypto = await ethers.getContractAt( - 'CurveVolatileCollateral', - deployments.collateral.cvxTriCrypto as string - ) - - /******** Verify TriCrypto plugin **************************/ - await verifyContract( - chainId, - deployments.collateral.cvxTriCrypto, - [ - { - erc20: await wTriCrypto.erc20(), - targetName: ethers.utils.formatBytes32String('TRICRYPTO'), - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero - oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: bn('1'), // unused but cannot be zero - maxTradeVolume: MAX_TRADE_VOL, - defaultThreshold: DEFAULT_THRESHOLD, - delayUntilDefault: DELAY_UNTIL_DEFAULT, - }, - revenueHiding.toString(), - { - nTokens: 3, - curvePool: TRI_CRYPTO, - poolType: CurvePoolType.Plain, - feeds: [[USDT_USD_FEED], [WBTC_BTC_FEED, BTC_USD_FEED], [WETH_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, WBTC_ORACLE_TIMEOUT), oracleTimeout(chainId, BTC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, WETH_ORACLE_TIMEOUT)], - ], - oracleErrors: [ - [USDT_ORACLE_ERROR], - [WBTC_BTC_ORACLE_ERROR, BTC_USD_ORACLE_ERROR], - [WETH_ORACLE_ERROR], - ], - lpToken: TRI_CRYPTO_TOKEN, - }, - ], - 'contracts/plugins/assets/convex/CurveVolatileCollateral.sol:CurveVolatileCollateral' - ) -} - -main().catch((error) => { - console.error(error) - process.exitCode = 1 -}) diff --git a/scripts/verification/collateral-plugins/verify_curve_stable.ts b/scripts/verification/collateral-plugins/verify_curve_stable.ts index ce1120f618..3f4b66190a 100644 --- a/scripts/verification/collateral-plugins/verify_curve_stable.ts +++ b/scripts/verification/collateral-plugins/verify_curve_stable.ts @@ -9,7 +9,7 @@ import { IAssetCollDeployments, } from '../../deployment/common' import { verifyContract } from '../../deployment/utils' -import { revenueHiding, oracleTimeout } from '../../deployment/utils' +import { revenueHiding } from '../../deployment/utils' import { CRV, CurvePoolType, @@ -72,7 +72,7 @@ async function main() { priceTimeout: PRICE_TIMEOUT, chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: oracleTimeout(chainId, USDC_ORACLE_TIMEOUT), // max of oracleTimeouts + oracleTimeout: USDC_ORACLE_TIMEOUT, // max of oracleTimeouts maxTradeVolume: MAX_TRADE_VOL, defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -83,11 +83,7 @@ async function main() { curvePool: THREE_POOL, poolType: CurvePoolType.Plain, feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], lpToken: THREE_POOL_TOKEN, }, diff --git a/scripts/verification/collateral-plugins/verify_curve_stable_metapool.ts b/scripts/verification/collateral-plugins/verify_curve_stable_metapool.ts index 60be29f1e0..e1b433bbd5 100644 --- a/scripts/verification/collateral-plugins/verify_curve_stable_metapool.ts +++ b/scripts/verification/collateral-plugins/verify_curve_stable_metapool.ts @@ -7,7 +7,7 @@ import { IAssetCollDeployments, } from '../../deployment/common' import { verifyContract } from '../../deployment/utils' -import { revenueHiding, oracleTimeout } from '../../deployment/utils' +import { revenueHiding } from '../../deployment/utils' import { CurvePoolType, DAI_ORACLE_ERROR, @@ -75,11 +75,7 @@ async function main() { curvePool: THREE_POOL, poolType: CurvePoolType.Plain, feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], lpToken: THREE_POOL_TOKEN, }, diff --git a/scripts/verification/collateral-plugins/verify_curve_stable_rtoken_metapool.ts b/scripts/verification/collateral-plugins/verify_curve_stable_rtoken_metapool.ts index a48df02d1f..43d2172f10 100644 --- a/scripts/verification/collateral-plugins/verify_curve_stable_rtoken_metapool.ts +++ b/scripts/verification/collateral-plugins/verify_curve_stable_rtoken_metapool.ts @@ -9,7 +9,7 @@ import { IAssetCollDeployments, } from '../../deployment/common' import { verifyContract } from '../../deployment/utils' -import { revenueHiding, oracleTimeout } from '../../deployment/utils' +import { revenueHiding } from '../../deployment/utils' import { CurvePoolType, DEFAULT_THRESHOLD, @@ -59,7 +59,7 @@ async function main() { priceTimeout: PRICE_TIMEOUT, chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: oracleTimeout(chainId, USDC_ORACLE_TIMEOUT), // max of oracleTimeouts + oracleTimeout: USDC_ORACLE_TIMEOUT, // max of oracleTimeouts maxTradeVolume: MAX_TRADE_VOL, defaultThreshold: DEFAULT_THRESHOLD, // 2%: 1% error on FRAX oracle + 1% base defaultThreshold delayUntilDefault: RTOKEN_DELAY_UNTIL_DEFAULT, @@ -70,10 +70,7 @@ async function main() { curvePool: FRAX_BP, poolType: CurvePoolType.Plain, feeds: [[FRAX_USD_FEED], [USDC_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, FRAX_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - ], + oracleTimeouts: [[FRAX_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT]], oracleErrors: [[FRAX_ORACLE_ERROR], [USDC_ORACLE_ERROR]], lpToken: FRAX_BP_TOKEN, }, diff --git a/scripts/verification/collateral-plugins/verify_curve_volatile.ts b/scripts/verification/collateral-plugins/verify_curve_volatile.ts deleted file mode 100644 index 2f5c53b2c1..0000000000 --- a/scripts/verification/collateral-plugins/verify_curve_volatile.ts +++ /dev/null @@ -1,98 +0,0 @@ -import hre, { ethers } from 'hardhat' -import { getChainId } from '../../../common/blockchain-utils' -import { developmentChains, networkConfig } from '../../../common/configuration' -import { bn } from '../../../common/numbers' -import { ONE_ADDRESS } from '../../../common/constants' -import { - getDeploymentFile, - getAssetCollDeploymentFilename, - IAssetCollDeployments, -} from '../../deployment/common' -import { verifyContract } from '../../deployment/utils' -import { revenueHiding, oracleTimeout } from '../../deployment/utils' -import { - CurvePoolType, - BTC_USD_ORACLE_ERROR, - BTC_ORACLE_TIMEOUT, - BTC_USD_FEED, - DEFAULT_THRESHOLD, - DELAY_UNTIL_DEFAULT, - MAX_TRADE_VOL, - PRICE_TIMEOUT, - TRI_CRYPTO, - TRI_CRYPTO_TOKEN, - WBTC_BTC_ORACLE_ERROR, - WETH_ORACLE_TIMEOUT, - WBTC_BTC_FEED, - WBTC_ORACLE_TIMEOUT, - WETH_USD_FEED, - WETH_ORACLE_ERROR, - USDT_ORACLE_ERROR, - USDT_ORACLE_TIMEOUT, - USDT_USD_FEED, -} from '../../../test/plugins/individual-collateral/curve/constants' - -let deployments: IAssetCollDeployments - -async function main() { - // ********** Read config ********** - const chainId = await getChainId(hre) - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - - if (developmentChains.includes(hre.network.name)) { - throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) - } - - const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) - deployments = getDeploymentFile(assetCollDeploymentFilename) - - const wTriCrypto = await ethers.getContractAt( - 'CurveVolatileCollateral', - deployments.collateral.crvTriCrypto as string - ) - - /******** Verify TriCrypto plugin **************************/ - await verifyContract( - chainId, - deployments.collateral.crvTriCrypto, - [ - { - erc20: await wTriCrypto.erc20(), - targetName: ethers.utils.formatBytes32String('TRICRYPTO'), - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero - oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: bn('1'), // unused but cannot be zero - maxTradeVolume: MAX_TRADE_VOL, - defaultThreshold: DEFAULT_THRESHOLD, - delayUntilDefault: DELAY_UNTIL_DEFAULT, - }, - revenueHiding.toString(), - { - nTokens: 3, - curvePool: TRI_CRYPTO, - poolType: CurvePoolType.Plain, - feeds: [[USDT_USD_FEED], [WBTC_BTC_FEED, BTC_USD_FEED], [WETH_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, WBTC_ORACLE_TIMEOUT), oracleTimeout(chainId, BTC_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, WETH_ORACLE_TIMEOUT)], - ], - oracleErrors: [ - [USDT_ORACLE_ERROR], - [WBTC_BTC_ORACLE_ERROR, BTC_USD_ORACLE_ERROR], - [WETH_ORACLE_ERROR], - ], - lpToken: TRI_CRYPTO_TOKEN, - }, - ], - 'contracts/plugins/assets/convex/CurveVolatileCollateral.sol:CurveVolatileCollateral' - ) -} - -main().catch((error) => { - console.error(error) - process.exitCode = 1 -}) diff --git a/scripts/verification/collateral-plugins/verify_cusdbcv3.ts b/scripts/verification/collateral-plugins/verify_cusdbcv3.ts index c3a6cb314e..d0eb672ef2 100644 --- a/scripts/verification/collateral-plugins/verify_cusdbcv3.ts +++ b/scripts/verification/collateral-plugins/verify_cusdbcv3.ts @@ -7,7 +7,7 @@ import { getAssetCollDeploymentFilename, IAssetCollDeployments, } from '../../deployment/common' -import { priceTimeout, oracleTimeout, verifyContract, revenueHiding } from '../../deployment/utils' +import { priceTimeout, verifyContract, revenueHiding } from '../../deployment/utils' let deployments: IAssetCollDeployments @@ -50,7 +50,7 @@ async function main() { /******** Verify Collateral - wcUSDbCv3 **************************/ - const usdcOracleTimeout = 86400 // 24 hr + const usdcOracleTimeout = '86400' // 24 hr const usdcOracleError = fp('0.003') // 0.3% (Base) await verifyContract( @@ -63,7 +63,7 @@ async function main() { oracleError: usdcOracleError.toString(), erc20: await collateral.erc20(), maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, usdcOracleTimeout).toString(), // 24h hr, + oracleTimeout: usdcOracleTimeout, // 24h hr, targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(usdcOracleError).toString(), // 1% + 0.3% delayUntilDefault: bn('86400').toString(), // 24h diff --git a/scripts/verification/collateral-plugins/verify_cusdcv3.ts b/scripts/verification/collateral-plugins/verify_cusdcv3.ts index 62c1389289..09a6eceb34 100644 --- a/scripts/verification/collateral-plugins/verify_cusdcv3.ts +++ b/scripts/verification/collateral-plugins/verify_cusdcv3.ts @@ -7,7 +7,7 @@ import { getAssetCollDeploymentFilename, IAssetCollDeployments, } from '../../deployment/common' -import { priceTimeout, oracleTimeout, verifyContract, revenueHiding } from '../../deployment/utils' +import { priceTimeout, verifyContract, revenueHiding } from '../../deployment/utils' let deployments: IAssetCollDeployments @@ -45,7 +45,7 @@ async function main() { /******** Verify Collateral - wcUSDCv3 **************************/ - const usdcOracleTimeout = 86400 // 24 hr + const usdcOracleTimeout = '86400' // 24 hr const usdcOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% await verifyContract( @@ -58,7 +58,7 @@ async function main() { oracleError: usdcOracleError.toString(), erc20: await collateral.erc20(), maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, usdcOracleTimeout).toString(), // 24h hr, + oracleTimeout: usdcOracleTimeout, // 24h hr, targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01').add(usdcOracleError).toString(), delayUntilDefault: bn('86400').toString(), // 24h diff --git a/scripts/verification/collateral-plugins/verify_morpho.ts b/scripts/verification/collateral-plugins/verify_morpho.ts index ba7658f5af..4f9e6d832b 100644 --- a/scripts/verification/collateral-plugins/verify_morpho.ts +++ b/scripts/verification/collateral-plugins/verify_morpho.ts @@ -7,13 +7,7 @@ import { getAssetCollDeploymentFilename, IAssetCollDeployments, } from '../../deployment/common' -import { - combinedError, - priceTimeout, - oracleTimeout, - verifyContract, - revenueHiding, -} from '../../deployment/utils' +import { combinedError, priceTimeout, verifyContract, revenueHiding } from '../../deployment/utils' let deployments: IAssetCollDeployments @@ -64,7 +58,7 @@ async function main() { priceTimeout: priceTimeout.toString(), oracleError: fp('0.0025').toString(), // 0.25% maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 1 hr + oracleTimeout: '86400', // 1 hr targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0025').add(fp('0.01')).toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h @@ -92,7 +86,7 @@ async function main() { priceTimeout: priceTimeout.toString(), oracleError: combinedBTCWBTCError.toString(), // 0.25% maxTradeVolume: fp('1e6'), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600'), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: fp('0.01').add(combinedBTCWBTCError), // ~3.5% delayUntilDefault: bn('86400'), // 24h @@ -101,7 +95,7 @@ async function main() { }, revenueHiding, networkConfig[chainId].chainlinkFeeds.WBTC!, - oracleTimeout(chainId, '86400').toString(), // 1 hr + '86400', // 1 hr ], 'contracts/plugins/assets/morpho-aave/MorphoNonFiatCollateral.sol:MorphoNonFiatCollateral' ) @@ -121,7 +115,7 @@ async function main() { priceTimeout: priceTimeout, oracleError: fp('0.005'), maxTradeVolume: fp('1e6'), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600'), // 1 hr + oracleTimeout: '3600', // 1 hr targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0'), // 0% -- no soft default for self-referential collateral delayUntilDefault: bn('86400'), // 24h diff --git a/scripts/verification/collateral-plugins/verify_reth.ts b/scripts/verification/collateral-plugins/verify_reth.ts index 324a081859..077cc76e0d 100644 --- a/scripts/verification/collateral-plugins/verify_reth.ts +++ b/scripts/verification/collateral-plugins/verify_reth.ts @@ -7,7 +7,7 @@ import { getAssetCollDeploymentFilename, IAssetCollDeployments, } from '../../deployment/common' -import { priceTimeout, oracleTimeout, verifyContract, combinedError } from '../../deployment/utils' +import { priceTimeout, verifyContract, combinedError } from '../../deployment/utils' let deployments: IAssetCollDeployments @@ -37,14 +37,14 @@ async function main() { oracleError: oracleError.toString(), // 0.5% & 2%, erc20: networkConfig[chainId].tokens.rETH, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr, + oracleTimeout: '3600', // 1 hr, targetName: hre.ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0.02').add(oracleError).toString(), // ~4.5% delayUntilDefault: bn('86400').toString(), // 24h }, fp('1e-4'), // revenueHiding = 0.01% networkConfig[chainId].chainlinkFeeds.rETH, // refPerTokChainlinkFeed - oracleTimeout(chainId, '86400').toString(), // refPerTokChainlinkTimeout + '86400', // refPerTokChainlinkTimeout ], 'contracts/plugins/assets/rocket-eth/RethCollateral.sol:RethCollateral' ) diff --git a/scripts/verification/collateral-plugins/verify_sdai.ts b/scripts/verification/collateral-plugins/verify_sdai.ts index e5d9290c39..393c6264b3 100644 --- a/scripts/verification/collateral-plugins/verify_sdai.ts +++ b/scripts/verification/collateral-plugins/verify_sdai.ts @@ -42,7 +42,7 @@ async function main() { defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h }, - fp('1e-6').toString(), // revenueHiding = 0.0001% + bn(0), POT, ], 'contracts/plugins/assets/dsr/SDaiCollateral.sol:SDaiCollateral' diff --git a/scripts/verification/collateral-plugins/verify_wsteth.ts b/scripts/verification/collateral-plugins/verify_wsteth.ts index c0b73b2fb0..b84c9aad57 100644 --- a/scripts/verification/collateral-plugins/verify_wsteth.ts +++ b/scripts/verification/collateral-plugins/verify_wsteth.ts @@ -7,7 +7,7 @@ import { getAssetCollDeploymentFilename, IAssetCollDeployments, } from '../../deployment/common' -import { priceTimeout, oracleTimeout, verifyContract } from '../../deployment/utils' +import { priceTimeout, verifyContract } from '../../deployment/utils' let deployments: IAssetCollDeployments @@ -38,14 +38,14 @@ async function main() { oracleError: fp('0.01').toString(), // 1%: only for stETHUSD feed erc20: networkConfig[chainId].tokens.wstETH, maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr, + oracleTimeout: '3600', // 1 hr, targetName: hre.ethers.utils.formatBytes32String('ETH'), defaultThreshold: fp('0.025').toString(), // 2.5% = 2% + 0.5% stethETH feed oracleError delayUntilDefault: bn('86400').toString(), // 24h }, fp('1e-4'), // revenueHiding = 0.01% networkConfig[chainId].chainlinkFeeds.stETHETH, // targetPerRefChainlinkFeed - oracleTimeout(chainId, '86400').toString(), // targetPerRefChainlinkTimeout + '86400', // targetPerRefChainlinkTimeout ], 'contracts/plugins/assets/lido/LidoStakedEthCollateral.sol:LidoStakedEthCollateral' ) diff --git a/scripts/verification/collateral-plugins/verify_yearn_v2_curve_usdc.ts b/scripts/verification/collateral-plugins/verify_yearn_v2_curve_usdc.ts new file mode 100644 index 0000000000..7505cfdb85 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_yearn_v2_curve_usdc.ts @@ -0,0 +1,73 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { fp, bn } from '../../../common/numbers' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { priceTimeout, oracleTimeout, verifyContract } from '../../deployment/utils' +import { + PRICE_PER_SHARE_HELPER, + YVUSDC_LP_TOKEN, +} from '../../../test/plugins/individual-collateral/yearnv2/constants' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + /******** Verify yvCurveUSDCcrvUSD **************************/ + await verifyContract( + chainId, + deployments.collateral.yvCurveUSDCcrvUSD, + [ + { + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDC, // not used but can't be empty + oracleError: fp('0.0025').toString(), // not used but can't be empty + erc20: networkConfig[chainId].tokens.yvCurveUSDCcrvUSD, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24hr -- max of all oracleTimeouts + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.015').toString(), // 1.5% = max oracleError + 1% + delayUntilDefault: bn('86400').toString(), // 24h + }, + fp('1e-6').toString(), // revenueHiding = 0.0001%, low since underlying curve pool should be up-only + { + nTokens: '2', + curvePool: YVUSDC_LP_TOKEN, + poolType: '0', + feeds: [ + networkConfig[chainId].chainlinkFeeds.USDC, + networkConfig[chainId].chainlinkFeeds.crvUSD, + ], + oracleTimeouts: [ + oracleTimeout(chainId, '86400').toString(), + oracleTimeout(chainId, '86400').toString(), + ], + oracleErrors: [fp('0.0025').toString(), fp('0.005').toString()], + lpToken: YVUSDC_LP_TOKEN, + }, + PRICE_PER_SHARE_HELPER, + ], + 'contracts/plugins/assets/yearnv2/YearnV2CurveFiatCollateral.sol:YearnV2CurveFiatCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verify_etherscan.ts b/scripts/verify_etherscan.ts index a0a69c2281..dd200b6248 100644 --- a/scripts/verify_etherscan.ts +++ b/scripts/verify_etherscan.ts @@ -62,6 +62,8 @@ async function main() { 'collateral-plugins/verify_sdai.ts', 'collateral-plugins/verify_morpho.ts', 'collateral-plugins/verify_aave_v3_usdc.ts', + 'collateral-plugins/verify_yearn_v2_curve_usdc.ts', + 'collateral-plugins/verify_yearn_v2_curve_usdp.ts', 'collateral-plugins/verify_sfrax.ts' ) } else if (chainId == '8453' || chainId == '84531') { diff --git a/tasks/deployment/deploy-facade-monitor.ts b/tasks/deployment/deploy-facade-monitor.ts new file mode 100644 index 0000000000..290a77f647 --- /dev/null +++ b/tasks/deployment/deploy-facade-monitor.ts @@ -0,0 +1,107 @@ +import { getChainId } from '../../common/blockchain-utils' +import { task, types } from 'hardhat/config' +import { FacadeMonitor } from '../../typechain' +import { developmentChains, networkConfig, IMonitorParams } from '../../common/configuration' +import { ZERO_ADDRESS } from '../../common/constants' +import { ContractFactory } from 'ethers' + +let facadeMonitor: FacadeMonitor + +task( + 'deploy-facade-monitor', + 'Deploys the FacadeMonitor implementation and proxy (if its not an upgrade)' +) + .addParam('upgrade', 'Set to true if this is for a later upgrade', false, types.boolean) + .addOptionalParam('owner', 'The address that will own the FacadeMonitor', '', types.string) + .addOptionalParam('noOutput', 'Suppress output', false, types.boolean) + .setAction(async (params, hre) => { + const [wallet] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + // ********** Read config ********** + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (!params.upgrade) { + if (!params.owner) { + throw new Error( + `An --owner must be specified for the initial deployment to ${hre.network.name}` + ) + } + } + + if (!params.noOutput) { + console.log( + `Deploying FacadeMonitor to ${hre.network.name} (${chainId}) with burner account ${wallet.address}` + ) + } + + // Setup Monitor Params + const monitorParams: IMonitorParams = { + AAVE_V2_DATA_PROVIDER_ADDR: networkConfig[chainId].AAVE_DATA_PROVIDER ?? ZERO_ADDRESS, + } + + // Deploy FacadeMonitor + const FacadeMonitorFactory: ContractFactory = await hre.ethers.getContractFactory( + 'FacadeMonitor' + ) + const facadeMonitorImplAddr = (await hre.upgrades.deployImplementation(FacadeMonitorFactory, { + kind: 'uups', + constructorArgs: [monitorParams], + })) as string + + if (!params.noOutput) { + console.log( + `Deployed FacadeMonitor (Implementation) to ${hre.network.name} (${chainId}): ${facadeMonitorImplAddr}` + ) + } + + if (!params.upgrade) { + facadeMonitor = await hre.upgrades.deployProxy( + FacadeMonitorFactory, + [params.owner], + { + kind: 'uups', + initializer: 'init', + constructorArgs: [monitorParams], + } + ) + + if (!params.noOutput) { + console.log( + `Deployed FacadeMonitor (Proxy) to ${hre.network.name} (${chainId}): ${facadeMonitor.address}` + ) + } + } + // Verify if its not a development chain + if (!developmentChains.includes(hre.network.name)) { + // Uncomment to verify + if (!params.noOutput) { + console.log('sleeping 30s') + } + + // Sleep to ensure API is in sync with chain + await new Promise((r) => setTimeout(r, 30000)) // 30s + + if (!params.noOutput) { + console.log('verifying') + } + + /** ******************** Verify FacadeMonitor ****************************************/ + console.time('Verifying FacadeMonitor Implementation') + await hre.run('verify:verify', { + address: facadeMonitorImplAddr, + constructorArguments: [monitorParams], + contract: 'contracts/facade/FacadeMonitor.sol:FacadeMonitor', + }) + console.timeEnd('Verifying FacadeMonitor Implementation') + + if (!params.noOutput) { + console.log('verified') + } + } + + return { facadeMonitor: facadeMonitor ? facadeMonitor.address : 'N/A', facadeMonitorImplAddr } + }) diff --git a/tasks/index.ts b/tasks/index.ts index 4f167da7a5..b1a9df3b56 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -16,6 +16,7 @@ import './deployment/mock/deploy-mock-aave' import './deployment/mock/deploy-mock-wbtc' import './deployment/mock/deploy-mock-easyauction' import './deployment/create-deployer-registry' +import './deployment/deploy-facade-monitor' import './deployment/empty-wallet' import './deployment/cancel-tx' import './deployment/sign-msg' diff --git a/test/Broker.test.ts b/test/Broker.test.ts index ff345cabd4..8ef43c0721 100644 --- a/test/Broker.test.ts +++ b/test/Broker.test.ts @@ -42,7 +42,7 @@ import { Implementation, IMPLEMENTATION, ORACLE_ERROR, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, SLOW, } from './fixtures' @@ -54,7 +54,7 @@ import { getLatestBlockTimestamp, getLatestBlockNumber, } from './utils/time' -import { ITradeRequest } from './utils/trades' +import { ITradeRequest, disableBatchTrade, disableDutchTrade } from './utils/trades' import { useEnv } from '#/utils/env' import { parseUnits } from 'ethers/lib/utils' @@ -132,30 +132,6 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { prices = { sellLow: fp('1'), sellHigh: fp('1'), buyLow: fp('1'), buyHigh: fp('1') } }) - const disableBatchTrade = async () => { - if (IMPLEMENTATION == Implementation.P1) { - const slot = await getStorageAt(broker.address, 205) - await setStorageAt( - broker.address, - 205, - slot.replace(slot.slice(2, 14), '1'.padStart(12, '0')) - ) - } else { - const slot = await getStorageAt(broker.address, 56) - await setStorageAt(broker.address, 56, slot.replace(slot.slice(2, 42), '1'.padStart(40, '0'))) - } - expect(await broker.batchTradeDisabled()).to.equal(true) - } - - const disableDutchTrade = async (erc20: string) => { - const mappingSlot = IMPLEMENTATION == Implementation.P1 ? bn('208') : bn('57') - const p = mappingSlot.toHexString().slice(2).padStart(64, '0') - const key = erc20.slice(2).padStart(64, '0') - const slot = ethers.utils.keccak256('0x' + key + p) - await setStorageAt(broker.address, slot, '0x' + '1'.padStart(64, '0')) - expect(await broker.dutchTradeDisabled(erc20)).to.equal(true) - } - describe('Deployment', () => { it('Should setup Broker correctly', async () => { expect(await broker.gnosis()).to.equal(gnosis.address) @@ -412,7 +388,7 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { expect(await broker.dutchTradeDisabled(token0.address)).to.equal(false) // Disable batch trade manually - await disableBatchTrade() + await disableBatchTrade(broker) expect(await broker.batchTradeDisabled()).to.equal(true) // Enable batch trade with owner @@ -425,7 +401,7 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { expect(await broker.dutchTradeDisabled(token0.address)).to.equal(false) // Disable dutch trade manually - await disableDutchTrade(token0.address) + await disableDutchTrade(broker, token0.address) expect(await broker.dutchTradeDisabled(token0.address)).to.equal(true) // Enable dutch trade with owner @@ -444,7 +420,7 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { describe('Trade Management', () => { it('Should not allow to open Batch trade if Disabled', async () => { // Disable Broker Batch Auctions - await disableBatchTrade() + await disableBatchTrade(broker) const tradeRequest: ITradeRequest = { sell: collateral0.address, @@ -473,12 +449,13 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { await token0.connect(bmSigner).approve(broker.address, tradeRequest.sellAmount) // Should succeed in callStatic + await assetRegistry.refresh() await broker .connect(bmSigner) .callStatic.openTrade(TradeKind.DUTCH_AUCTION, tradeRequest, prices) // Disable Broker Dutch Auctions for token0 - await disableDutchTrade(token0.address) + await disableDutchTrade(broker, token0.address) // Dutch Auction openTrade should fail now await expect( @@ -491,12 +468,13 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { .withArgs(token0.address, true, false) // Should succeed in callStatic + await assetRegistry.refresh() await broker .connect(bmSigner) .callStatic.openTrade(TradeKind.DUTCH_AUCTION, tradeRequest, prices) // Disable Broker Dutch Auctions for token1 - await disableDutchTrade(token1.address) + await disableDutchTrade(broker, token1.address) // Dutch Auction openTrade should fail now await expect( @@ -572,28 +550,6 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { // Check nothing changed expect(await broker.batchTradeDisabled()).to.equal(false) }) - - it('Should not allow to report violation if paused or frozen', async () => { - // Check not disabled - expect(await broker.batchTradeDisabled()).to.equal(false) - - await main.connect(owner).pauseTrading() - - await expect(broker.connect(addr1).reportViolation()).to.be.revertedWith( - 'frozen or trading paused' - ) - - await main.connect(owner).unpauseTrading() - - await main.connect(owner).freezeShort() - - await expect(broker.connect(addr1).reportViolation()).to.be.revertedWith( - 'frozen or trading paused' - ) - - // Check nothing changed - expect(await broker.batchTradeDisabled()).to.equal(false) - }) }) describe('Trades', () => { @@ -1271,7 +1227,7 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { oracleError: ORACLE_ERROR, erc20: token0.address, maxTradeVolume: bn(500), - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1436,7 +1392,7 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { oracleError: bn('1'), // minimize erc20: sellTok.address, maxTradeVolume: MAX_UINT192, - oracleTimeout: MAX_UINT48, + oracleTimeout: MAX_UINT48.sub(300), targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01'), // shouldn't matter delayUntilDefault: bn('604800'), // shouldn't matter @@ -1448,7 +1404,7 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { oracleError: bn('1'), // minimize erc20: buyTok.address, maxTradeVolume: MAX_UINT192, - oracleTimeout: MAX_UINT48, + oracleTimeout: MAX_UINT48.sub(300), targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01'), // shouldn't matter delayUntilDefault: bn('604800'), // shouldn't matter @@ -1660,6 +1616,14 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { let TradeFactory: ContractFactory let newTrade: DutchTrade + // Increment `lastSave` in storage slot 1 + const incrementLastSave = async (addr: string) => { + const asArray = ethers.utils.arrayify(await getStorageAt(addr, 1)) + asArray[7] = asArray[7] + 1 // increment least significant byte of lastSave + const asHex = ethers.utils.hexlify(asArray) + await setStorageAt(addr, 1, asHex) + } + beforeEach(async () => { amount = bn('100e18') @@ -1687,6 +1651,9 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { // Backing Manager await whileImpersonating(backingManager.address, async (bmSigner) => { await token0.connect(bmSigner).approve(broker.address, amount) + await assetRegistry.refresh() + await incrementLastSave(tradeRequest.sell) + await incrementLastSave(tradeRequest.buy) await snapshotGasCost( broker.connect(bmSigner).openTrade(TradeKind.DUTCH_AUCTION, tradeRequest, prices) ) @@ -1695,6 +1662,9 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { // RSR Trader await whileImpersonating(rsrTrader.address, async (rsrSigner) => { await token0.connect(rsrSigner).approve(broker.address, amount) + await assetRegistry.refresh() + await incrementLastSave(tradeRequest.sell) + await incrementLastSave(tradeRequest.buy) await snapshotGasCost( broker.connect(rsrSigner).openTrade(TradeKind.DUTCH_AUCTION, tradeRequest, prices) ) @@ -1703,6 +1673,9 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { // RToken Trader await whileImpersonating(rTokenTrader.address, async (rtokSigner) => { await token0.connect(rtokSigner).approve(broker.address, amount) + await assetRegistry.refresh() + await incrementLastSave(tradeRequest.sell) + await incrementLastSave(tradeRequest.buy) await snapshotGasCost( broker.connect(rtokSigner).openTrade(TradeKind.DUTCH_AUCTION, tradeRequest, prices) ) diff --git a/test/Facade.test.ts b/test/Facade.test.ts index 207263b706..f20428e2a4 100644 --- a/test/Facade.test.ts +++ b/test/Facade.test.ts @@ -3,11 +3,13 @@ import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' import { BigNumber, ContractFactory } from 'ethers' -import { ethers } from 'hardhat' +import { ethers, upgrades } from 'hardhat' import { expectEvents } from '../common/events' -import { IConfig } from '#/common/configuration' +import { IConfig, IMonitorParams } from '#/common/configuration' import { bn, fp } from '../common/numbers' import { setOraclePrice } from './utils/oracles' +import { disableBatchTrade, disableDutchTrade } from './utils/trades' +import { whileImpersonating } from './utils/impersonation' import { Asset, BackingManagerP1, @@ -18,6 +20,8 @@ import { CTokenWrapperMock, ERC20Mock, FacadeAct, + FacadeMonitor, + FacadeMonitorV2, FacadeRead, FacadeTest, MockV3Aggregator, @@ -45,9 +49,17 @@ import { IMPLEMENTATION, defaultFixture, ORACLE_ERROR, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, } from './fixtures' import { advanceBlocks, getLatestBlockTimestamp, setNextBlockTimestamp } from './utils/time' -import { CollateralStatus, TradeKind, MAX_UINT256, ZERO_ADDRESS } from '#/common/constants' +import { + CollateralStatus, + TradeKind, + MAX_UINT256, + ONE_PERIOD, + ZERO_ADDRESS, +} from '#/common/constants' import { expectTrade } from './utils/trades' import { mintCollaterals } from './utils/tokens' @@ -55,7 +67,7 @@ const describeP1 = IMPLEMENTATION == Implementation.P1 ? describe : describe.ski const itP1 = IMPLEMENTATION == Implementation.P1 ? it : it.skip -describe('FacadeRead + FacadeAct contracts', () => { +describe('FacadeRead + FacadeAct + FacadeMonitor contracts', () => { let owner: SignerWithAddress let addr1: SignerWithAddress let addr2: SignerWithAddress @@ -83,6 +95,7 @@ describe('FacadeRead + FacadeAct contracts', () => { let facade: FacadeRead let facadeTest: FacadeTest let facadeAct: FacadeAct + let facadeMonitor: FacadeMonitor // Main let rToken: TestIRToken @@ -125,6 +138,7 @@ describe('FacadeRead + FacadeAct contracts', () => { facade, facadeAct, facadeTest, + facadeMonitor, rToken, main, basketHandler, @@ -270,7 +284,7 @@ describe('FacadeRead + FacadeAct contracts', () => { it('Should handle UNPRICED when returning issuable quantities', async () => { // Set unpriced assets, should return UoA = 0 - await setOraclePrice(tokenAsset.address, MAX_UINT256.div(2).sub(1)) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) const [toks, quantities, uoas] = await facade.callStatic.issue(rToken.address, issueAmount) expect(toks.length).to.equal(4) expect(toks[0]).to.equal(token.address) @@ -283,9 +297,9 @@ describe('FacadeRead + FacadeAct contracts', () => { expect(quantities[2]).to.equal(issueAmount.div(4)) expect(quantities[3]).to.equal(issueAmount.div(4).mul(50).div(bn('1e10'))) expect(uoas.length).to.equal(4) - // Three assets are unpriced + // Assets are unpriced expect(uoas[0]).to.equal(0) - expect(uoas[1]).to.equal(issueAmount.div(4)) + expect(uoas[1]).to.equal(0) expect(uoas[2]).to.equal(0) expect(uoas[3]).to.equal(0) }) @@ -481,6 +495,10 @@ describe('FacadeRead + FacadeAct contracts', () => { // Set price to 0 await setOraclePrice(rsrAsset.address, bn(0)) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) + await setOraclePrice(tokenAsset.address, bn('1e8')) + await setOraclePrice(usdcAsset.address, bn('1e8')) + await assetRegistry.refresh() const [backing2, overCollateralization2] = await facade.callStatic.backingOverview( rToken.address @@ -505,7 +523,10 @@ describe('FacadeRead + FacadeAct contracts', () => { expect(backing).to.equal(fp('1')) expect(overCollateralization).to.equal(fp('0.5')) - await setOraclePrice(rsrAsset.address, MAX_UINT256.div(2).sub(1)) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) + await setOraclePrice(tokenAsset.address, bn('1e8')) + await setOraclePrice(usdcAsset.address, bn('1e8')) + await assetRegistry.refresh() ;[backing, overCollateralization] = await facade.callStatic.backingOverview(rToken.address) // Check values - Fully collateralized and no over-collateralization @@ -551,98 +572,98 @@ describe('FacadeRead + FacadeAct contracts', () => { }) it('Should return revenue + chain into FacadeAct.runRevenueAuctions', async () => { - const traders = [rTokenTrader, rsrTrader] - const initialPrice = await usdcAsset.price() + // Set low to 0 == revenueOverview() should not revert + const minTradeVolume = await rsrTrader.minTradeVolume() + const auctionLength = await broker.dutchAuctionLength() + const tokenSurplus = bn('0.5e18') + await token.connect(addr1).transfer(rsrTrader.address, tokenSurplus) - // Set lotLow to 0 == revenueOverview() should not revert await setOraclePrice(usdcAsset.address, bn('0')) - await usdcAsset.refresh() - for (let traderIndex = 0; traderIndex < traders.length; traderIndex++) { - const trader = traders[traderIndex] - - const minTradeVolume = await trader.minTradeVolume() - const auctionLength = await broker.dutchAuctionLength() - const tokenSurplus = bn('0.5e18') - await token.connect(addr1).transfer(trader.address, tokenSurplus) - - const [lotLow] = await usdcAsset.lotPrice() - expect(lotLow).to.equal(initialPrice[0]) - - // revenue - let [erc20s, canStart, surpluses, minTradeAmounts] = - await facadeAct.callStatic.revenueOverview(trader.address) - expect(erc20s.length).to.equal(8) // should be full set of registered ERC20s - - const erc20sToStart = [] - for (let i = 0; i < 8; i++) { - if (erc20s[i] == token.address) { - erc20sToStart.push(erc20s[i]) - expect(canStart[i]).to.equal(true) - expect(surpluses[i]).to.equal(tokenSurplus) - } else { - expect(canStart[i]).to.equal(false) - expect(surpluses[i]).to.equal(0) - } - const asset = await ethers.getContractAt('IAsset', await assetRegistry.toAsset(erc20s[i])) - const [low] = await asset.lotPrice() - expect(minTradeAmounts[i]).to.equal( - low.gt(0) ? minTradeVolume.mul(bn('10').pow(await asset.erc20Decimals())).div(low) : 0 - ) // 1% oracleError - } + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) + await setOraclePrice(tokenAsset.address, bn('1e8')) + await setOraclePrice(rsrAsset.address, bn('1e8')) + await assetRegistry.refresh() - // Run revenue auctions via multicall - const funcSig = ethers.utils.id('runRevenueAuctions(address,address[],address[],uint8[])') - const args = ethers.utils.defaultAbiCoder.encode( - ['address', 'address[]', 'address[]', 'uint8[]'], - [trader.address, [], erc20sToStart, [TradeKind.DUTCH_AUCTION]] - ) - const data = funcSig.substring(0, 10) + args.slice(2) - await expect(facadeAct.multicall([data])).to.emit(trader, 'TradeStarted') - - // Another call to revenueOverview should not propose any auction - ;[erc20s, canStart, surpluses, minTradeAmounts] = - await facadeAct.callStatic.revenueOverview(trader.address) - expect(canStart).to.eql(Array(8).fill(false)) - - // Nothing should be settleable - expect((await facade.auctionsSettleable(trader.address)).length).to.equal(0) - - // Advance time till auction is over - await advanceBlocks(2 + auctionLength / 12) - - // Now should be settleable - const settleable = await facade.auctionsSettleable(trader.address) - expect(settleable.length).to.equal(1) - expect(settleable[0]).to.equal(token.address) - - // Another call to revenueOverview should settle and propose new auction - ;[erc20s, canStart, surpluses, minTradeAmounts] = - await facadeAct.callStatic.revenueOverview(trader.address) - - // Should repeat the same auctions - for (let i = 0; i < 8; i++) { - if (erc20s[i] == token.address) { - expect(canStart[i]).to.equal(true) - expect(surpluses[i]).to.equal(tokenSurplus) - } else { - expect(canStart[i]).to.equal(false) - expect(surpluses[i]).to.equal(0) - } + const [low] = await usdcAsset.price() + expect(low).to.equal(0) + + // revenue + let [erc20s, canStart, surpluses, minTradeAmounts] = + await facadeAct.callStatic.revenueOverview(rsrTrader.address) + expect(erc20s.length).to.equal(8) // should be full set of registered ERC20s + + const erc20sToStart = [] + for (let i = 0; i < 8; i++) { + if (erc20s[i] == token.address) { + erc20sToStart.push(erc20s[i]) + expect(canStart[i]).to.equal(true) + expect(surpluses[i]).to.equal(tokenSurplus) + } else { + expect(canStart[i]).to.equal(false) + expect(surpluses[i]).to.equal(0) } + const asset = await ethers.getContractAt('IAsset', await assetRegistry.toAsset(erc20s[i])) + const [low] = await asset.price() + expect(minTradeAmounts[i]).to.equal( + low.gt(0) ? minTradeVolume.mul(bn('10').pow(await asset.erc20Decimals())).div(low) : 0 + ) // 1% oracleError + } + + // Run revenue auctions via multicall + const funcSig = ethers.utils.id('runRevenueAuctions(address,address[],address[],uint8[])') + const args = ethers.utils.defaultAbiCoder.encode( + ['address', 'address[]', 'address[]', 'uint8[]'], + [rsrTrader.address, [], erc20sToStart, [TradeKind.DUTCH_AUCTION]] + ) + const data = funcSig.substring(0, 10) + args.slice(2) + await expect(facadeAct.multicall([data])).to.emit(rsrTrader, 'TradeStarted') - // Settle and start new auction - await facadeAct.runRevenueAuctions(trader.address, erc20sToStart, erc20sToStart, [ - TradeKind.DUTCH_AUCTION, - ]) + // Another call to revenueOverview should not propose any auction + ;[erc20s, canStart, surpluses, minTradeAmounts] = await facadeAct.callStatic.revenueOverview( + rsrTrader.address + ) + expect(canStart).to.eql(Array(8).fill(false)) - // Send additional revenues - await token.connect(addr1).transfer(trader.address, tokenSurplus) + // Nothing should be settleable + expect((await facade.auctionsSettleable(rsrTrader.address)).length).to.equal(0) + + // Advance time till auction is over + await advanceBlocks(2 + auctionLength / 12) - // Call revenueOverview, cannot open new auctions - ;[erc20s, canStart, surpluses, minTradeAmounts] = - await facadeAct.callStatic.revenueOverview(trader.address) - expect(canStart).to.eql(Array(8).fill(false)) + // Now should be settleable + const settleable = await facade.auctionsSettleable(rsrTrader.address) + expect(settleable.length).to.equal(1) + expect(settleable[0]).to.equal(token.address) + + // Another call to revenueOverview should settle and propose new auction + ;[erc20s, canStart, surpluses, minTradeAmounts] = await facadeAct.callStatic.revenueOverview( + rsrTrader.address + ) + + // Should repeat the same auctions + for (let i = 0; i < 8; i++) { + if (erc20s[i] == token.address) { + expect(canStart[i]).to.equal(true) + expect(surpluses[i]).to.equal(tokenSurplus) + } else { + expect(canStart[i]).to.equal(false) + expect(surpluses[i]).to.equal(0) + } } + + // Settle and start new auction + await facadeAct.runRevenueAuctions(rsrTrader.address, erc20sToStart, erc20sToStart, [ + TradeKind.DUTCH_AUCTION, + ]) + + // Send additional revenues + await token.connect(addr1).transfer(rsrTrader.address, tokenSurplus) + + // Call revenueOverview, cannot open new auctions + ;[erc20s, canStart, surpluses, minTradeAmounts] = await facadeAct.callStatic.revenueOverview( + rsrTrader.address + ) + expect(canStart).to.eql(Array(8).fill(false)) }) itP1('Should handle invalid versions when running revenueOverview', async () => { @@ -892,7 +913,11 @@ describe('FacadeRead + FacadeAct contracts', () => { ) // set price of dai to 0 await chainlinkFeed.updateAnswer(0) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) + await setOraclePrice(usdcAsset.address, bn('1e8')) + await assetRegistry.refresh() await main.connect(owner).pauseTrading() + const [erc20s, breakdown, targets] = await facade.callStatic.basketBreakdown(rToken.address) expect(erc20s.length).to.equal(4) expect(breakdown.length).to.equal(4) @@ -941,16 +966,24 @@ describe('FacadeRead + FacadeAct contracts', () => { }) it('Should return pending unstakings', async () => { - const unstakeAmount = bn('10000e18') - await rsr.connect(owner).mint(addr1.address, unstakeAmount.mul(10)) + // Bump draftEra by seizing RSR when the withdrawal queue is empty + await rsr.connect(owner).mint(stRSRP1.address, 1) + await whileImpersonating(backingManager.address, async (signer) => { + await stRSRP1.connect(signer).seizeRSR(1) + }) + const draftEra = await stRSRP1.getDraftEra() + expect(draftEra).to.equal(2) // Stake + const unstakeAmount = bn('10000e18') + await rsr.connect(owner).mint(addr1.address, unstakeAmount.mul(10)) await rsr.connect(addr1).approve(stRSR.address, unstakeAmount.mul(10)) await stRSRP1.connect(addr1).stake(unstakeAmount.mul(10)) + await stRSRP1.connect(addr1).unstake(unstakeAmount) await stRSRP1.connect(addr1).unstake(unstakeAmount.add(1)) - const pendings = await facade.pendingUnstakings(rToken.address, addr1.address) + const pendings = await facade.pendingUnstakings(rToken.address, draftEra, addr1.address) expect(pendings.length).to.eql(2) expect(pendings[0][0]).to.eql(bn(0)) // index expect(pendings[0][2]).to.eql(unstakeAmount) // amount @@ -1034,6 +1067,339 @@ describe('FacadeRead + FacadeAct contracts', () => { } }) + describe('FacadeMonitor', () => { + const monitorParams: IMonitorParams = { + AAVE_V2_DATA_PROVIDER_ADDR: ZERO_ADDRESS, + } + + beforeEach(async () => { + // Mint Tokens + initialBal = bn('10000000000e18') + await token.connect(owner).mint(addr1.address, initialBal) + await usdc.connect(owner).mint(addr1.address, initialBal) + await aToken.connect(owner).mint(addr1.address, initialBal) + await cTokenVault.connect(owner).mint(addr1.address, initialBal) + + // Provide approvals + await token.connect(addr1).approve(rToken.address, initialBal) + await usdc.connect(addr1).approve(rToken.address, initialBal) + await aToken.connect(addr1).approve(rToken.address, initialBal) + await cTokenVault.connect(addr1).approve(rToken.address, initialBal) + }) + + it('should return batch auctions disabled correctly', async () => { + expect(await facadeMonitor.batchAuctionsDisabled(rToken.address)).to.equal(false) + + // Disable Broker Batch Auctions + await disableBatchTrade(broker) + + expect(await facadeMonitor.batchAuctionsDisabled(rToken.address)).to.equal(true) + }) + + it('should return dutch auctions disabled correctly', async () => { + expect(await facadeMonitor.dutchAuctionsDisabled(rToken.address)).to.equal(false) + + // Disable Broker Dutch Auctions for token0 + await disableDutchTrade(broker, token.address) + + expect(await facadeMonitor.dutchAuctionsDisabled(rToken.address)).to.equal(true) + }) + + it('should return issuance available', async () => { + expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) // no supply + + // Issue some RTokens (1%) + const issueAmount = bn('10000e18') + + // Issue rTokens (1%) + await rToken.connect(addr1).issue(issueAmount) + + // check throttles updated + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('0.99')) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Issue additional rTokens (another 1%) + await rToken.connect(addr1).issue(issueAmount) + + // Should be 2% down minus some recharging + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.be.closeTo( + fp('0.98'), + fp('0.001') + ) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Advance time significantly + await advanceTime(10000000) + + // Check new issuance available - fully recharged + expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Issuance #2 - Consume all throttle + const issueAmount2: BigNumber = config.issuanceThrottle.amtRate + await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + Number(ONE_PERIOD)) + await rToken.connect(addr1).issue(issueAmount2) + + // Check new issuance available - all consumed + expect(await rToken.issuanceAvailable()).to.equal(bn(0)) + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(bn(0)) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + }) + + it('should return redemption available', async () => { + const issueAmount = bn('100000e18') + + // Decrease redemption allowed amount + const redeemThrottleParams = { amtRate: issueAmount.div(2), pctRate: fp('0.1') } // 50K + await rToken.connect(owner).setRedemptionThrottleParams(redeemThrottleParams) + + // Check with no supply + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) + expect(await rToken.redemptionAvailable()).to.equal(bn(0)) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Issue some RTokens + await rToken.connect(addr1).issue(issueAmount) + + // check throttles - redemption still fully available + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('0.9')) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Redeem RTokens (50% of throttle) + await rToken.connect(addr1).redeem(issueAmount.div(4)) + + // check throttle - redemption allowed decreased to 50% + expect(await rToken.redemptionAvailable()).to.equal(issueAmount.div(4)) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('0.5')) + + // Advance time significantly + await advanceTime(10000000) + + // Check redemption available - fully recharged + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Redemption #2 - Consume all throttle + await rToken.connect(addr1).redeem(issueAmount.div(2)) + + // Check new redemption available - all consumed + expect(await rToken.redemptionAvailable()).to.equal(bn(0)) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(bn(0)) + }) + + it('Should handle issuance/redemption throttles correctly, using percent', async function () { + // Full issuance available. Nothing to redeem + expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.redemptionAvailable()).to.equal(bn(0)) + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Issue full throttle + const issueAmount1: BigNumber = config.issuanceThrottle.amtRate + await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + Number(ONE_PERIOD)) + await rToken.connect(addr1).issue(issueAmount1) + + // Check redemption throttles updated + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(bn(0)) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Advance time significantly + await advanceTime(1000000000) + + // Check new issuance available - fully recharged + expect(await rToken.issuanceAvailable()).to.equal(config.issuanceThrottle.amtRate) + expect(await rToken.redemptionAvailable()).to.equal(issueAmount1) + + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Issuance #2 - Full throttle again - will be processed + const issueAmount2: BigNumber = config.issuanceThrottle.amtRate + await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + Number(ONE_PERIOD)) + await rToken.connect(addr1).issue(issueAmount2) + + // Check new issuance available - all consumed + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(bn(0)) + + // Check redemption throttle updated - fixed in max (does not exceed) + expect(await rToken.redemptionAvailable()).to.equal(config.redemptionThrottle.amtRate) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Set issuance throttle to percent only + const issuanceThrottleParams = { amtRate: fp('1'), pctRate: fp('0.1') } // 10% + await rToken.connect(owner).setIssuanceThrottleParams(issuanceThrottleParams) + + // Advance time significantly + await advanceTime(1000000000) + + // Check new issuance available - 10% of supply (2 M) = 200K + const supplyThrottle = bn('200000e18') + expect(await rToken.issuanceAvailable()).to.equal(supplyThrottle) + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) + + // Check redemption throttle unchanged + expect(await rToken.redemptionAvailable()).to.equal(config.redemptionThrottle.amtRate) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Issuance #3 - Should be allowed, does not exceed supply restriction + const issueAmount3: BigNumber = bn('100000e18') + await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + Number(ONE_PERIOD)) + await rToken.connect(addr1).issue(issueAmount3) + + // Check issuance throttle updated - Previous issuances recharged + expect(await rToken.issuanceAvailable()).to.equal(supplyThrottle.sub(issueAmount3)) + + // Hourly Limit: 210K (10% of total supply of 2.1 M) + // Available: 100 K / 201K (~ 0.47619) + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.be.closeTo( + fp('0.476'), + fp('0.001') + ) + + // Check redemption throttle unchanged + expect(await rToken.redemptionAvailable()).to.equal(config.redemptionThrottle.amtRate) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Check all issuances are confirmed + expect(await rToken.balanceOf(addr1.address)).to.equal( + issueAmount1.add(issueAmount2).add(issueAmount3) + ) + + // Advance time, issuance will recharge a bit + await advanceTime(100) + + // Now 50% of hourly limit available (~105.8K / 210 K) + expect(await rToken.issuanceAvailable()).to.be.closeTo(fp('105800'), fp('100')) + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.be.closeTo( + fp('0.5'), + fp('0.01') + ) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + const issueAmount4: BigNumber = fp('105800') + // Issuance #4 - almost all available + await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + Number(ONE_PERIOD)) + await rToken.connect(addr1).issue(issueAmount4) + + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.be.closeTo( + fp('0.003'), + fp('0.001') + ) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Advance time significantly to fully recharge + await advanceTime(1000000000) + + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Check redemptions + // Set redemption throttle to percent only + const redemptionThrottleParams = { amtRate: fp('1'), pctRate: fp('0.1') } // 10% + await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) + + const totalSupply = await rToken.totalSupply() + expect(await rToken.redemptionAvailable()).to.equal(totalSupply.div(10)) // 10% + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Redeem half of the available throttle + await rToken.connect(addr1).redeem(totalSupply.div(10).div(2)) + + // About 52% now used of redemption throttle + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.be.closeTo( + fp('0.52'), + fp('0.01') + ) + + // Advance time significantly to fully recharge + await advanceTime(1000000000) + + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(fp('1')) + + // Redeem all remaining + await rToken.connect(addr1).redeem(await rToken.redemptionAvailable()) + + // Check all consumed + expect(await facadeMonitor.issuanceAvailable(rToken.address)).to.equal(fp('1')) + expect(await rToken.redemptionAvailable()).to.equal(bn(0)) + expect(await facadeMonitor.redemptionAvailable(rToken.address)).to.equal(bn(0)) + }) + + it('Should not allow empty owner on initialization', async () => { + const FacadeMonitorFactory: ContractFactory = await ethers.getContractFactory('FacadeMonitor') + + const newFacadeMonitor = await upgrades.deployProxy(FacadeMonitorFactory, [], { + constructorArgs: [monitorParams], + kind: 'uups', + }) + + await expect(newFacadeMonitor.init(ZERO_ADDRESS)).to.be.revertedWith('invalid owner address') + }) + + it('Should allow owner to transfer ownership', async () => { + expect(await facadeMonitor.owner()).to.equal(owner.address) + + // Attempt to transfer ownership with another account + await expect( + facadeMonitor.connect(addr1).transferOwnership(addr1.address) + ).to.be.revertedWith('Ownable: caller is not the owner') + + // Owner remains the same + expect(await facadeMonitor.owner()).to.equal(owner.address) + + // Transfer ownership with owner + await expect(facadeMonitor.connect(owner).transferOwnership(addr1.address)) + .to.emit(facadeMonitor, 'OwnershipTransferred') + .withArgs(owner.address, addr1.address) + + // Owner changed + expect(await facadeMonitor.owner()).to.equal(addr1.address) + }) + + it('Should only allow owner to upgrade', async () => { + const FacadeMonitorV2Factory: ContractFactory = await ethers.getContractFactory( + 'FacadeMonitorV2' + ) + const facadeMonitorV2 = await FacadeMonitorV2Factory.deploy(monitorParams) + + await expect( + facadeMonitor.connect(addr1).upgradeTo(facadeMonitorV2.address) + ).to.be.revertedWith('Ownable: caller is not the owner') + await expect(facadeMonitor.connect(owner).upgradeTo(facadeMonitorV2.address)).to.not.be + .reverted + }) + + it('Should upgrade correctly', async () => { + // Upgrading + const FacadeMonitorV2Factory: ContractFactory = await ethers.getContractFactory( + 'FacadeMonitorV2' + ) + const facadeMonitorV2: FacadeMonitorV2 = await upgrades.upgradeProxy( + facadeMonitor.address, + FacadeMonitorV2Factory, + { + constructorArgs: [monitorParams], + } + ) + + // Check address is maintained + expect(facadeMonitorV2.address).to.equal(facadeMonitor.address) + + // Check state is preserved + expect(await facadeMonitorV2.owner()).to.equal(owner.address) + + // Check new version is implemented + expect(await facadeMonitorV2.version()).to.equal('2.0.0') + + expect(await facadeMonitorV2.newValue()).to.equal(0) + await facadeMonitorV2.connect(owner).setNewValue(bn(1000)) + expect(await facadeMonitorV2.newValue()).to.equal(bn(1000)) + }) + }) + // P1 only describeP1('FacadeAct', () => { let issueAmount: BigNumber @@ -1139,10 +1505,7 @@ describe('FacadeRead + FacadeAct contracts', () => { expect((await facade.auctionsSettleable(rsrTrader.address)).length).to.equal(0) // Advance time till auction ended - await advanceBlocks(2 + auctionLength / 12) - - // Settleable now - expect((await facade.auctionsSettleable(rsrTrader.address)).length).to.equal(1) + await advanceBlocks(1 + auctionLength / 12) // Settle and start new auction - Will retry await expectEvents( @@ -1167,18 +1530,6 @@ describe('FacadeRead + FacadeAct contracts', () => { }, ] ) - - // Nothing should be settleable - expect((await facade.auctionsSettleable(rsrTrader.address)).length).to.equal(0) - - // Advance time till auction ended - await advanceBlocks(2 + auctionLength / 12) - - // Settleable now - expect((await facade.auctionsSettleable(rsrTrader.address)).length).to.equal(1) - - // Should not revert, even when not starting new auctions - await facadeAct.runRevenueAuctions(rsrTrader.address, [token.address], [], []) }) it('Should handle other versions when running revenue auctions', async () => { diff --git a/test/FacadeWrite.test.ts b/test/FacadeWrite.test.ts index 97210ce749..9176c71ac0 100644 --- a/test/FacadeWrite.test.ts +++ b/test/FacadeWrite.test.ts @@ -195,8 +195,8 @@ describe('FacadeWrite contract', () => { // Set governance params govParams = { - votingDelay: bn(5), // 5 blocks - votingPeriod: bn(100), // 100 blocks + votingDelay: bn(7200), // 1 day + votingPeriod: bn(21600), // 3 days proposalThresholdAsMicroPercent: bn(1e6), // 1% quorumPercent: bn(4), // 4% timelockDelay: bn(60 * 60 * 24), // 1 day diff --git a/test/Furnace.test.ts b/test/Furnace.test.ts index 15776210be..84d16f32c7 100644 --- a/test/Furnace.test.ts +++ b/test/Furnace.test.ts @@ -204,9 +204,9 @@ describe(`FurnaceP${IMPLEMENTATION} contract`, () => { await furnace.connect(addr1).melt() }) - it('Should not melt if frozen #fast', async () => { + it('Should melt if frozen #fast', async () => { await main.connect(owner).freezeShort() - await expect(furnace.connect(addr1).melt()).to.be.revertedWith('frozen') + await furnace.connect(addr1).melt() }) it('Should not melt any funds in the initial block #fast', async () => { @@ -450,40 +450,57 @@ describe(`FurnaceP${IMPLEMENTATION} contract`, () => { it('Regression test -- C4 June 2023 Issue #29', async () => { // https://github.com/code-423n4/2023-06-reserve-findings/issues/29 + const firstRatio = fp('1e-6') + const secondRatio = fp('1e-4') + const mintAmount = fp('100') + + // Set ratio to something cleaner + await expect(furnace.connect(owner).setRatio(firstRatio)) + .to.emit(furnace, 'RatioSet') + .withArgs(config.rewardRatio, firstRatio) + // Transfer to Furnace and do first melt - await rToken.connect(addr1).transfer(furnace.address, bn('10e18')) + await rToken.connect(addr1).transfer(furnace.address, mintAmount) await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + Number(ONE_PERIOD)) await furnace.melt() // Should have updated lastPayout + lastPayoutBal expect(await furnace.lastPayout()).to.be.closeTo(await getLatestBlockTimestamp(), 12) expect(await furnace.lastPayout()).to.be.lte(await getLatestBlockTimestamp()) - expect(await furnace.lastPayoutBal()).to.equal(bn('10e18')) + expect(await furnace.lastPayoutBal()).to.equal(mintAmount) - // Advance 99 periods -- should melt at old ratio - await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + 99 * Number(ONE_PERIOD)) + // Advance 100 periods -- should melt at old ratio + await setNextBlockTimestamp( + Number(await getLatestBlockTimestamp()) + 100 * Number(ONE_PERIOD) + ) - // Freeze and change ratio + // Freeze and change ratio (melting as a pre-step) await main.connect(owner).freezeForever() - const maxRatio = bn('1e14') - await expect(furnace.connect(owner).setRatio(maxRatio)) + await expect(furnace.connect(owner).setRatio(secondRatio)) .to.emit(furnace, 'RatioSet') - .withArgs(config.rewardRatio, maxRatio) + .withArgs(firstRatio, secondRatio) - // Should have updated lastPayout + lastPayoutBal + // Should have melted expect(await furnace.lastPayout()).to.be.closeTo(await getLatestBlockTimestamp(), 12) expect(await furnace.lastPayout()).to.be.lte(await getLatestBlockTimestamp()) - expect(await furnace.lastPayoutBal()).to.equal(bn('10e18')) // no change + expect(await furnace.lastPayoutBal()).to.eq(fp('99.990000494983830300')) - // Unfreeze and advance 1 period + // Unfreeze and advance 100 periods await main.connect(owner).unfreeze() - await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + Number(ONE_PERIOD)) + await setNextBlockTimestamp( + Number(await getLatestBlockTimestamp()) + 100 * Number(ONE_PERIOD) + ) await expect(furnace.melt()).to.emit(rToken, 'Melted') - // Should have updated lastPayout + lastPayoutBal + // Should have updated lastPayout + lastPayoutBal and melted at new ratio expect(await furnace.lastPayout()).to.be.closeTo(await getLatestBlockTimestamp(), 12) expect(await furnace.lastPayout()).to.be.lte(await getLatestBlockTimestamp()) - expect(await furnace.lastPayoutBal()).to.equal(bn('9.999e18')) + expect(await furnace.lastPayoutBal()).to.equal(fp('98.995033865808581644')) + // if the ratio were not increased 100x, this would be more like 99.980001989868666200 + + // Total supply should have decreased by the cumulative melted amount + expect(await rToken.totalSupply()).to.equal(mintAmount.add(await furnace.lastPayoutBal())) + expect(await rToken.basketsNeeded()).to.equal(mintAmount.mul(2)) }) }) diff --git a/test/Governance.test.ts b/test/Governance.test.ts index 83dffb413a..53b7f7d2f1 100644 --- a/test/Governance.test.ts +++ b/test/Governance.test.ts @@ -59,8 +59,8 @@ describeP1(`Governance - P${IMPLEMENTATION}`, () => { let initialBal: BigNumber const MIN_DELAY = 7 * 60 * 60 * 24 // 7 days - const VOTING_DELAY = 5 // 5 blocks - const VOTING_PERIOD = 100 // 100 blocks + const VOTING_DELAY = 7200 // 1 day (in blocks) + const VOTING_PERIOD = 21600 // 3 days (in blocks) const PROPOSAL_THRESHOLD = 1e6 // 1% const QUORUM_PERCENTAGE = 4 // 4% @@ -306,13 +306,39 @@ describeP1(`Governance - P${IMPLEMENTATION}`, () => { expect(await governor.supportsInterface(interfaceID._hex)).to.equal(true) }) + + it('Should perform validations on votingDelay at deployment', async () => { + // Attempt to deploy with 0 voting delay + await expect( + GovernorFactory.deploy( + stRSRVotes.address, + timelock.address, + bn(0), + VOTING_PERIOD, + PROPOSAL_THRESHOLD, + QUORUM_PERCENTAGE + ) + ).to.be.revertedWith('invalid votingDelay') + + // Attempt to deploy with voting delay below minium (1 day) + await expect( + GovernorFactory.deploy( + stRSRVotes.address, + timelock.address, + bn(2000), // less than 1 day + VOTING_PERIOD, + PROPOSAL_THRESHOLD, + QUORUM_PERCENTAGE + ) + ).to.be.revertedWith('invalid votingDelay') + }) }) describe('Proposals', () => { // Proposal details const newValue: BigNumber = bn('360') - const proposalDescription = 'Proposal #1 - Update Trading Delay to 360' - const proposalDescHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(proposalDescription)) + let proposalDescription = 'Proposal #1 - Update Trading Delay to 360' + let proposalDescHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(proposalDescription)) let encodedFunctionCall: string let stkAmt1: BigNumber let stkAmt2: BigNumber @@ -873,5 +899,143 @@ describeP1(`Governance - P${IMPLEMENTATION}`, () => { // Check role was granted expect(await main.hasRole(SHORT_FREEZER, other.address)).to.equal(true) }) + + it('Should allow to update GovernorSettings via governance', async () => { + // Attempt to update if not governance + await expect(governor.setVotingDelay(bn(14400))).to.be.revertedWith( + 'Governor: onlyGovernance' + ) + + // Attempt to update without governance process in place + await whileImpersonating(timelock.address, async (signer) => { + await expect(governor.connect(signer).setVotingDelay(bn(14400))).to.be.reverted + }) + + // Update votingDelay via proposal + encodedFunctionCall = governor.interface.encodeFunctionData('setVotingDelay', [ + VOTING_DELAY * 2, + ]) + proposalDescription = 'Proposal #2 - Update Voting Delay to double' + proposalDescHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(proposalDescription)) + + // Check current value + expect(await governor.votingDelay()).to.equal(VOTING_DELAY) + + // Propose + const proposeTx = await governor + .connect(addr1) + .propose([governor.address], [0], [encodedFunctionCall], proposalDescription) + + const proposeReceipt = await proposeTx.wait(1) + const proposalId = proposeReceipt.events![0].args!.proposalId + + // Check proposal state + expect(await governor.state(proposalId)).to.equal(ProposalState.Pending) + + // Advance time to start voting + await advanceBlocks(VOTING_DELAY + 1) + + // Check proposal state + expect(await governor.state(proposalId)).to.equal(ProposalState.Active) + + const voteWay = 1 // for + + // vote + await governor.connect(addr1).castVote(proposalId, voteWay) + await advanceBlocks(1) + + // Advance time till voting is complete + await advanceBlocks(VOTING_PERIOD + 1) + + // Finished voting - Check proposal state + expect(await governor.state(proposalId)).to.equal(ProposalState.Succeeded) + + // Queue propoal + await governor + .connect(addr1) + .queue([governor.address], [0], [encodedFunctionCall], proposalDescHash) + + // Check proposal state + expect(await governor.state(proposalId)).to.equal(ProposalState.Queued) + + // Advance time required by timelock + await advanceTime(MIN_DELAY + 1) + await advanceBlocks(1) + + // Execute + await governor + .connect(addr1) + .execute([governor.address], [0], [encodedFunctionCall], proposalDescHash) + + // Check proposal state + expect(await governor.state(proposalId)).to.equal(ProposalState.Executed) + + // Check value was updated + expect(await governor.votingDelay()).to.equal(VOTING_DELAY * 2) + }) + + it('Should perform validations on votingDelay when updating', async () => { + // Update via proposal - Invalid value + encodedFunctionCall = governor.interface.encodeFunctionData('setVotingDelay', [bn(7100)]) + proposalDescription = 'Proposal #2 - Update Voting Delay to invalid' + proposalDescHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(proposalDescription)) + + // Check current value + expect(await governor.votingDelay()).to.equal(VOTING_DELAY) + + // Propose + const proposeTx = await governor + .connect(addr1) + .propose([governor.address], [0], [encodedFunctionCall], proposalDescription) + + const proposeReceipt = await proposeTx.wait(1) + const proposalId = proposeReceipt.events![0].args!.proposalId + + // Check proposal state + expect(await governor.state(proposalId)).to.equal(ProposalState.Pending) + + // Advance time to start voting + await advanceBlocks(VOTING_DELAY + 1) + + // Check proposal state + expect(await governor.state(proposalId)).to.equal(ProposalState.Active) + + const voteWay = 1 // for + + // vote + await governor.connect(addr1).castVote(proposalId, voteWay) + await advanceBlocks(1) + + // Advance time till voting is complete + await advanceBlocks(VOTING_PERIOD + 1) + + // Finished voting - Check proposal state + expect(await governor.state(proposalId)).to.equal(ProposalState.Succeeded) + + // Queue propoal + await governor + .connect(addr1) + .queue([governor.address], [0], [encodedFunctionCall], proposalDescHash) + + // Check proposal state + expect(await governor.state(proposalId)).to.equal(ProposalState.Queued) + + // Advance time required by timelock + await advanceTime(MIN_DELAY + 1) + await advanceBlocks(1) + + // Execute + await expect( + governor + .connect(addr1) + .execute([governor.address], [0], [encodedFunctionCall], proposalDescHash) + ).to.be.revertedWith('TimelockController: underlying transaction reverted') + + // Check proposal state, still queued + expect(await governor.state(proposalId)).to.equal(ProposalState.Queued) + + // Check value was not updated + expect(await governor.votingDelay()).to.equal(VOTING_DELAY) + }) }) }) diff --git a/test/Main.test.ts b/test/Main.test.ts index 788d4eba4b..9d00c3b0a6 100644 --- a/test/Main.test.ts +++ b/test/Main.test.ts @@ -70,6 +70,8 @@ import { Implementation, IMPLEMENTATION, ORACLE_ERROR, + ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, REVENUE_HIDING, } from './fixtures' @@ -1180,7 +1182,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: newToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: await collateral0.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: await ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral0.delayUntilDefault(), @@ -1202,7 +1204,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: newToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: await collateral0.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: await ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral0.delayUntilDefault(), @@ -1228,7 +1230,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: await gasGuzzlingColl.erc20(), maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: await collateral0.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: await ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral0.delayUntilDefault(), @@ -1627,7 +1629,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: erc20s[5].address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: await collateral2.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral2.delayUntilDefault(), @@ -1723,7 +1725,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: eurToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: await collateral0.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral1.delayUntilDefault(), @@ -1946,7 +1948,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: await collateral0.erc20(), maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: await collateral0.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('NEW_TARGET'), defaultThreshold: fp('0.01'), delayUntilDefault: await collateral0.delayUntilDefault(), @@ -2756,8 +2758,10 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { // Check BU price -- 1/4 of the basket has lost half its value await expectPrice(basketHandler.address, fp('0.875'), ORACLE_ERROR, true) - // Set collateral1 price to invalid value that should produce [0, FIX_MAX] - await setOraclePrice(collateral1.address, MAX_UINT192) + // Set collateral1 price to [0, FIX_MAX] + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) + await setOraclePrice(collateral0.address, bn('1e8')) + await assetRegistry.refresh() // Check BU price -- 1/4 of the basket has lost all its value const asset = await ethers.getContractAt('Asset', basketHandler.address) @@ -2799,7 +2803,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: await collateral2.erc20(), maxTradeVolume: await collateral2.maxTradeVolume(), - oracleTimeout: await collateral2.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral2.delayUntilDefault(), @@ -2812,6 +2816,8 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { // Set price = 0, which hits 3 of our 4 collateral in the basket await setOraclePrice(newColl2.address, bn('0')) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) + await setOraclePrice(collateral1.address, bn('1e8')) // Check status and price again const p = await basketHandler.price() @@ -2832,7 +2838,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: await collateral2.erc20(), maxTradeVolume: await collateral2.maxTradeVolume(), - oracleTimeout: await collateral2.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral2.delayUntilDefault(), @@ -2840,17 +2846,9 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { REVENUE_HIDING ) await assetRegistry.connect(owner).swapRegistered(newColl.address) - await setOraclePrice(newColl.address, MAX_UINT192) // overflow - await expectUnpriced(newColl.address) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) await newColl.setTargetPerRef(1) - await freshBasketHandler.setPrimeBasket([await newColl.erc20()], [fp('1000')]) - await freshBasketHandler.refreshBasket() - - // Expect [something > 0, FIX_MAX] - const bh = await ethers.getContractAt('Asset', basketHandler.address) - const [lowPrice, highPrice] = await bh.price() - expect(lowPrice).to.be.gt(0) - expect(highPrice).to.equal(MAX_UINT192) + await expectUnpriced(basketHandler.address) }) it('Should handle overflow in price calculation and return [FIX_MAX, FIX_MAX] - case 1', async () => { @@ -2865,7 +2863,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: await collateral2.erc20(), maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: await collateral2.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral2.delayUntilDefault(), @@ -2907,35 +2905,6 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { expect(highPrice).to.equal(MAX_UINT192) }) - it('Should distinguish between price/lotPrice', async () => { - // Set basket with single collateral - await basketHandler.connect(owner).setPrimeBasket([token0.address], [fp('1')]) - await basketHandler.refreshBasket() - - await collateral0.refresh() - const [low, high] = await collateral0.price() - await setOraclePrice(collateral0.address, MAX_UINT256.div(2)) // oracle error - - // lotPrice() should begin at 100% - let [lowPrice, highPrice] = await basketHandler.price() - let [lotLowPrice, lotHighPrice] = await basketHandler.lotPrice() - expect(lowPrice).to.equal(0) - expect(highPrice).to.equal(MAX_UINT192) - expect(lotLowPrice).to.be.eq(low) - expect(lotHighPrice).to.be.eq(high) - - // Advance time past 100% period -- lotPrice() should begin to fall - await advanceTime(await collateral0.oracleTimeout()) - ;[lowPrice, highPrice] = await basketHandler.price() - ;[lotLowPrice, lotHighPrice] = await basketHandler.lotPrice() - expect(lowPrice).to.equal(0) - expect(highPrice).to.equal(MAX_UINT192) - expect(lotLowPrice).to.be.closeTo(low, low.div(bn('1e5'))) // small decay expected - expect(lotLowPrice).to.be.lt(low) - expect(lotHighPrice).to.be.closeTo(high, high.div(bn('1e5'))) // small decay expected - expect(lotHighPrice).to.be.lt(high) - }) - it('Should disable basket on asset deregistration + return quantities correctly', async () => { // Check values expect(await facadeTest.wholeBasketsHeldBy(rToken.address, addr1.address)).to.equal( @@ -3116,7 +3085,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: await collateral2.erc20(), maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: await collateral2.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral2.delayUntilDefault(), @@ -3160,7 +3129,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: await collateral2.erc20(), maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: await collateral2.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral2.delayUntilDefault(), @@ -3181,6 +3150,15 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { await expectPrice(basketHandler.address, fp('0.75'), ORACLE_ERROR, true) }) + it('lotPrice (deprecated) is equal to price()', async () => { + const lotPrice = await basketHandler.lotPrice() + const price = await basketHandler.price() + expect(price.length).to.equal(2) + expect(lotPrice.length).to.equal(price.length) + expect(lotPrice[0]).to.equal(price[0]) + expect(lotPrice[1]).to.equal(price[1]) + }) + it('Should not put backup tokens with different targetName in the basket', async () => { // Swap out collateral for bad target name const CollFactory = await ethers.getContractFactory('FiatCollateral') @@ -3190,7 +3168,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: token0.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: await collateral0.oracleTimeout(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: await ethers.utils.formatBytes32String('NEW TARGET'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral0.delayUntilDefault(), diff --git a/test/RTokenExtremes.test.ts b/test/RTokenExtremes.test.ts index f11c415f6a..f5c8afa994 100644 --- a/test/RTokenExtremes.test.ts +++ b/test/RTokenExtremes.test.ts @@ -21,7 +21,7 @@ import { IMPLEMENTATION, ORACLE_ERROR, SLOW, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, defaultFixtureNoBasket, } from './fixtures' @@ -66,7 +66,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { oracleError: ORACLE_ERROR, erc20: erc20.address, maxTradeVolume: fp('1e36'), - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01'), delayUntilDefault: bn(86400), @@ -155,7 +155,6 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { // Recharge throttle await advanceTime(3600) - await advanceTime(await basketHandler.warmupPeriod()) // ==== Issue the "initial" rtoken supply to owner diff --git a/test/Recollateralization.test.ts b/test/Recollateralization.test.ts index e806d4b316..83d0b04af6 100644 --- a/test/Recollateralization.test.ts +++ b/test/Recollateralization.test.ts @@ -51,6 +51,7 @@ import { IMPLEMENTATION, ORACLE_ERROR, ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, } from './fixtures' import snapshotGasCost from './utils/snapshotGasCost' @@ -643,7 +644,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR, erc20: token1.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await collateral1.delayUntilDefault(), @@ -656,7 +657,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR, erc20: backupToken1.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await backupCollateral1.delayUntilDefault(), @@ -1015,9 +1016,6 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { }) it('Should not recollateralize when switching basket if all assets are UNPRICED', async () => { - // Set price to use lot price - await setOraclePrice(collateral0.address, MAX_UINT256.div(2)) - // Setup prime basket await basketHandler.connect(owner).setPrimeBasket([token1.address], [fp('1')]) @@ -1029,7 +1027,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { // Advance time post warmup period - temporary IFFY->SOUND await advanceTime(Number(config.warmupPeriod) + 1) - // Set to sell price = 0 + // Set all assets to UNPRICED await advanceTime(Number(ORACLE_TIMEOUT.add(PRICE_TIMEOUT))) // Check state remains SOUND @@ -1188,8 +1186,8 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { await expectRTokenPrice(rTokenAsset.address, fp('1'), ORACLE_ERROR) }) - it('Should recollateralize correctly when switching basket - Using lot price', async () => { - // Set price to unpriced (will use lotPrice to size trade) + it('Should recollateralize correctly when switching basket', async () => { + // Set oracle value out-of-range await setOraclePrice(collateral0.address, MAX_UINT256.div(2)) // Setup prime basket @@ -1218,7 +1216,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { await toMinBuyAmt(sellAmt, fp('1'), fp('1')), 6 ).add(1) - // since within oracleTimeout lotPrice() should still be at 100% of original price + // since within oracleTimeout, price() should still be at 100% of original price await expect(facadeTest.runAuctionsForAllTraders(rToken.address)) .to.emit(backingManager, 'TradeStarted') @@ -1352,11 +1350,10 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { expect(await token0.balanceOf(backingManager.address)).to.equal(remainder) expect(await token1.balanceOf(backingManager.address)).to.equal(0) - // Check price in USD of the current RToken -- no backing currently - const rTokenPrice = remainder.mul(BN_SCALE_FACTOR).div(issueAmount).add(2) // no RSR + // Check price in USD of the current RToken -- should track backing out on auction await expectRTokenPrice( rTokenAsset.address, - rTokenPrice, + fp('1'), ORACLE_ERROR, await backingManager.maxTradeSlippage(), config.minTradeVolume.mul((await assetRegistry.erc20s()).length) @@ -1476,11 +1473,10 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { expect(await token0.balanceOf(backingManager.address)).to.equal(remainder) expect(await token1.balanceOf(backingManager.address)).to.equal(0) - // Check price in USD of the current RToken -- no backing currently - const rTokenPrice = remainder.mul(BN_SCALE_FACTOR).div(issueAmount).add(2) // no RSR + // Check price in USD of the current RToken -- should track balances out on trade await expectRTokenPrice( rTokenAsset.address, - rTokenPrice, + fp('1'), ORACLE_ERROR, await backingManager.maxTradeSlippage(), config.minTradeVolume.mul((await assetRegistry.erc20s()).length) @@ -1610,11 +1606,10 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { expect(await token0.balanceOf(backingManager.address)).to.equal(remainder) expect(await token1.balanceOf(backingManager.address)).to.equal(0) - // Check price in USD of the current RToken -- no backing currently - const rTokenPrice = remainder.mul(BN_SCALE_FACTOR).div(issueAmount).add(2) // no RSR + // Check price in USD of the current RToken -- backing is tracked while out on trade await expectRTokenPrice( rTokenAsset.address, - rTokenPrice, + fp('1'), ORACLE_ERROR, await backingManager.maxTradeSlippage(), config.minTradeVolume.mul((await assetRegistry.erc20s()).length) @@ -2148,7 +2143,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR, erc20: token0.address, maxTradeVolume: fp('25'), - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: await backupCollateral1.delayUntilDefault(), @@ -2212,7 +2207,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { ).div(2) const sellAmt = sellAmtBeforeSlippage .mul(BN_SCALE_FACTOR) - .div(BN_SCALE_FACTOR.add(ORACLE_ERROR)) + .div(BN_SCALE_FACTOR.sub(ORACLE_ERROR)) const minBuyAmt = await toMinBuyAmt(sellAmt, fp('0.5'), fp('1')) await expect(facadeTest.runAuctionsForAllTraders(rToken.address)) @@ -2255,64 +2250,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { await advanceTime(config.batchAuctionLength.add(100).toString()) // Run auctions - will end current, and will open a new auction for the same amount - await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ - { - contract: backingManager, - name: 'TradeSettled', - args: [anyValue, token0.address, backupToken1.address, sellAmt, minBuyAmt], - emitted: true, - }, - { - contract: backingManager, - name: 'TradeStarted', - args: [anyValue, token0.address, backupToken1.address, sellAmt, minBuyAmt], - emitted: true, - }, - ]) - const leftoverSellAmt = issueAmount.sub(sellAmt.mul(2)) - - // Check new auction - // Token0 -> Backup Token Auction - await expectTrade(backingManager, { - sell: token0.address, - buy: backupToken1.address, - endTime: (await getLatestBlockTimestamp()) + Number(config.batchAuctionLength), - externalId: bn('1'), - }) - - // Check state - expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) - expect(await basketHandler.fullyCollateralized()).to.equal(false) - expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.equal( - minBuyAmt.add(leftoverSellAmt.div(2)) - ) - expect(await token0.balanceOf(backingManager.address)).to.equal(leftoverSellAmt) - expect(await backupToken1.balanceOf(backingManager.address)).to.equal(minBuyAmt) - expect(await rToken.totalSupply()).to.equal(issueAmount) - - // Check price in USD of the current RToken - await expectRTokenPrice(rTokenAsset.address, fp('1'), ORACLE_ERROR) - - // Perform Mock Bids (addr1 has balance) - // Pay at worst-case price - await backupToken1.connect(addr1).approve(gnosis.address, minBuyAmt) - await gnosis.placeBid(1, { - bidder: addr1.address, - sellAmount: sellAmt, - buyAmount: minBuyAmt, - }) - - // Advance time till auction ended - await advanceTime(config.batchAuctionLength.add(100).toString()) - - // Check staking situation remains unchanged - expect(await rsr.balanceOf(stRSR.address)).to.equal(stakeAmount) - expect(await stRSR.balanceOf(addr1.address)).to.equal(stakeAmount) - - // Advance time till auction ended - await advanceTime(config.batchAuctionLength.add(100).toString()) - - // Run auctions - will end current, and will open a new auction for the same amount + const leftoverSellAmt = issueAmount.sub(sellAmt) const leftoverMinBuyAmt = await toMinBuyAmt(leftoverSellAmt, fp('0.5'), fp('1')) await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ { @@ -2341,17 +2279,14 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { sell: token0.address, buy: backupToken1.address, endTime: (await getLatestBlockTimestamp()) + Number(config.batchAuctionLength), - externalId: bn('2'), + externalId: bn('1'), }) // Check state expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) expect(await basketHandler.fullyCollateralized()).to.equal(false) - expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.equal( - minBuyAmt.mul(2) - ) expect(await token0.balanceOf(backingManager.address)).to.equal(0) - expect(await backupToken1.balanceOf(backingManager.address)).to.equal(minBuyAmt.mul(2)) + expect(await backupToken1.balanceOf(backingManager.address)).to.equal(minBuyAmt) expect(await rToken.totalSupply()).to.equal(issueAmount) // Check price in USD of the current RToken @@ -2360,7 +2295,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { // Perform Mock Bids (addr1 has balance) // Pay at worst-case price await backupToken1.connect(addr1).approve(gnosis.address, minBuyAmt) - await gnosis.placeBid(2, { + await gnosis.placeBid(1, { bidder: addr1.address, sellAmount: leftoverSellAmt, buyAmount: leftoverMinBuyAmt, @@ -2373,11 +2308,11 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { expect(await rsr.balanceOf(stRSR.address)).to.equal(stakeAmount) expect(await stRSR.balanceOf(addr1.address)).to.equal(stakeAmount) - // End current auction, should start a new one to sell RSR for collateral - // ~51e18 Tokens left to buy - Sets Buy amount as independent value - const buyAmtBidRSR: BigNumber = issueAmount - .sub(minBuyAmt.mul(2).add(leftoverMinBuyAmt)) - .add(1) + // Advance time till auction ended + await advanceTime(config.batchAuctionLength.add(100).toString()) + + // Run auctions - will end current, and will open a new auction for the same amount + const buyAmtBidRSR: BigNumber = issueAmount.sub(minBuyAmt.add(leftoverMinBuyAmt)).add(1) await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ { contract: backingManager, @@ -2407,7 +2342,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { sell: rsr.address, buy: backupToken1.address, endTime: auctionTimestamp + Number(config.batchAuctionLength), - externalId: bn('3'), + externalId: bn('2'), }) const t = await getTrade(backingManager, rsr.address) @@ -2418,11 +2353,11 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) expect(await basketHandler.fullyCollateralized()).to.equal(false) expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.equal( - minBuyAmt.mul(2).add(leftoverMinBuyAmt) + minBuyAmt.add(leftoverMinBuyAmt) ) expect(await token0.balanceOf(backingManager.address)).to.equal(0) expect(await backupToken1.balanceOf(backingManager.address)).to.equal( - minBuyAmt.mul(2).add(leftoverMinBuyAmt) + minBuyAmt.add(leftoverMinBuyAmt) ) expect(await rToken.totalSupply()).to.equal(issueAmount) @@ -2435,7 +2370,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { // Perform Mock Bids for RSR (addr1 has balance) // Pay at worst-case price await backupToken1.connect(addr1).approve(gnosis.address, buyAmtBidRSR) - await gnosis.placeBid(3, { + await gnosis.placeBid(2, { bidder: addr1.address, sellAmount: sellAmtRSR, buyAmount: buyAmtBidRSR, @@ -3311,7 +3246,6 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { collateral1.address, collateral0.address, issueAmount, - config.minTradeVolume, config.maxTradeSlippage ), bn('1e12') @@ -3375,7 +3309,6 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { collateral0.address, collateral1.address, issueAmount, - config.minTradeVolume, config.maxTradeSlippage ), bn('1e12') // decimals @@ -4528,10 +4461,10 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { }, ]) - // Check price in USD of the current RToken - capital out on auction + // Check price in USD of the current RToken - should track the capital out on auction await expectRTokenPrice( rTokenAsset.address, - fp('0.5'), + fp('0.625'), ORACLE_ERROR, await backingManager.maxTradeSlippage(), config.minTradeVolume.mul((await assetRegistry.erc20s()).length) diff --git a/test/Revenues.test.ts b/test/Revenues.test.ts index 4277386931..3858ba4290 100644 --- a/test/Revenues.test.ts +++ b/test/Revenues.test.ts @@ -13,6 +13,7 @@ import { CollateralStatus, TradeKind, MAX_UINT192, + ONE_PERIOD, } from '../common/constants' import { expectEvents } from '../common/events' import { bn, divCeil, fp, near } from '../common/numbers' @@ -59,6 +60,7 @@ import { REVENUE_HIDING, ORACLE_ERROR, ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, } from './fixtures' import { expectRTokenPrice, setOraclePrice } from './utils/oracles' @@ -554,7 +556,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { ) }) - it('Should forward RSR revenue directly to StRSR', async () => { + it('Should forward RSR revenue directly to StRSR and call payoutRewards()', async () => { const amount = bn('2000e18') await rsr.connect(owner).mint(backingManager.address, amount) expect(await rsr.balanceOf(backingManager.address)).to.equal(amount) @@ -562,20 +564,36 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(await rsr.balanceOf(rsrTrader.address)).to.equal(0) expect(await rsr.balanceOf(rTokenTrader.address)).to.equal(0) - await expect(backingManager.forwardRevenue([rsr.address])).to.emit(rsr, 'Transfer') + // Advance to the end of noop period + await advanceTime(Number(ONE_PERIOD)) + + await expectEvents(backingManager.forwardRevenue([rsr.address]), [ + { + contract: rsr, + name: 'Transfer', + args: [backingManager.address, stRSR.address, amount], + emitted: true, + }, + { + contract: stRSR, + name: 'RewardsPaid', + emitted: true, + }, + ]) + expect(await rsr.balanceOf(backingManager.address)).to.equal(0) expect(await rsr.balanceOf(stRSR.address)).to.equal(amount) expect(await rsr.balanceOf(rsrTrader.address)).to.equal(0) expect(await rsr.balanceOf(rTokenTrader.address)).to.equal(0) }) - it('Should launch revenue auction at lotPrice if UNPRICED', async () => { - // After oracleTimeout the lotPrice should be the original price still + it('Should launch revenue auction if UNPRICED', async () => { + // After oracleTimeout it should still launch auction for RToken await advanceTime(ORACLE_TIMEOUT.toString()) await rsr.connect(addr1).transfer(rTokenTrader.address, issueAmount) await rTokenTrader.callStatic.manageTokens([rsr.address], [TradeKind.BATCH_AUCTION]) - // After oracleTimeout the lotPrice should be the original price still + // After priceTimeout it should not buy RToken await advanceTime(PRICE_TIMEOUT.toString()) await rsr.connect(addr1).transfer(rTokenTrader.address, issueAmount) await expect( @@ -651,9 +669,113 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(await rsr.balanceOf(stRSR.address)).to.be.closeTo(expectedAmount, 100) }) + it('Should distribute tokenToBuy before updating distribution', async () => { + // Check initial status + const [rTokenTotal, rsrTotal] = await distributor.totals() + expect(rsrTotal).equal(bn(60)) + expect(rTokenTotal).equal(bn(40)) + + // Set some balance of token-to-buy in traders + const issueAmount = bn('100e18') + + // RSR Trader + const stRSRBal = await rsr.balanceOf(stRSR.address) + await rsr.connect(owner).mint(rsrTrader.address, issueAmount) + + // RToken Trader + const rTokenBal = await rToken.balanceOf(furnace.address) + await rToken.connect(addr1).issueTo(rTokenTrader.address, issueAmount) + + // Update distributions with owner - Set f = 1 + await distributor + .connect(owner) + .setDistribution(FURNACE_DEST, { rTokenDist: bn(0), rsrDist: bn(0) }) + + // Check tokens were transferred from Traders + const expectedAmountRSR = stRSRBal.add(issueAmount) + expect(await rsr.balanceOf(stRSR.address)).to.be.closeTo(expectedAmountRSR, 100) + expect(await rsr.balanceOf(rsrTrader.address)).to.be.closeTo(bn(0), 100) + + const expectedAmountRToken = rTokenBal.add(issueAmount) + expect(await rToken.balanceOf(furnace.address)).to.be.closeTo(expectedAmountRToken, 100) + expect(await rsr.balanceOf(rTokenTrader.address)).to.be.closeTo(bn(0), 100) + + // Check updated distributions + const [newRTokenTotal, newRsrTotal] = await distributor.totals() + expect(newRsrTotal).equal(bn(60)) + expect(newRTokenTotal).equal(bn(0)) + }) + + it('Should avoid zero transfers when distributing tokenToBuy', async () => { + // Distribute with no balance + await expect(rsrTrader.distributeTokenToBuy()).to.be.revertedWith('nothing to distribute') + expect(await rsr.balanceOf(stRSR.address)).to.equal(bn(0)) + + // Small amount which ends in zero distribution due to rounding + await rsr.connect(owner).mint(rsrTrader.address, bn(1)) + await expect(rsrTrader.distributeTokenToBuy()).to.be.revertedWith('nothing to distribute') + expect(await rsr.balanceOf(stRSR.address)).to.equal(bn(0)) + }) + + it('Should account rewards when distributing tokenToBuy', async () => { + // 1. StRSR.payoutRewards() + const stRSRBal = await rsr.balanceOf(stRSR.address) + await rsr.connect(owner).mint(rsrTrader.address, issueAmount) + await advanceTime(Number(ONE_PERIOD)) + await expect(rsrTrader.distributeTokenToBuy()).to.emit(stRSR, 'RewardsPaid') + const expectedAmountStRSR = stRSRBal.add(issueAmount) + expect(await rsr.balanceOf(stRSR.address)).to.be.closeTo(expectedAmountStRSR, 100) + + // 2. Furnace.melt() + // Transfer RTokens to Furnace (to trigger melting later) + const hndAmt: BigNumber = bn('10e18') + await rToken.connect(addr1).transfer(furnace.address, hndAmt) + await advanceTime(Number(ONE_PERIOD)) + await furnace.melt() + + // Transfer and distribute tokens in Trader (will melt) + await advanceTime(Number(ONE_PERIOD)) + await rToken.connect(addr1).transfer(rTokenTrader.address, hndAmt) + await expect(rTokenTrader.distributeTokenToBuy()).to.emit(rToken, 'Melted') + const expectedAmountFurnace = hndAmt.mul(2) + expect(await rToken.balanceOf(furnace.address)).to.be.closeTo( + expectedAmountFurnace, + expectedAmountFurnace.div(1000) + ) // within 0.1% + }) + + it('Should update distribution even if distributeTokenToBuy() reverts', async () => { + // Check initial status + const [rTokenTotal, rsrTotal] = await distributor.totals() + expect(rsrTotal).equal(bn(60)) + expect(rTokenTotal).equal(bn(40)) + + // Set some balance of token-to-buy in RSR trader + const issueAmount = bn('100e18') + const stRSRBal = await rsr.balanceOf(stRSR.address) + await rsr.connect(owner).mint(rsrTrader.address, issueAmount) + + // Pause the system, makes distributeTokenToBuy() revert + await main.connect(owner).pauseTrading() + await expect(rsrTrader.distributeTokenToBuy()).to.be.reverted + + // Update distributions with owner - Set f = 1 + await distributor + .connect(owner) + .setDistribution(FURNACE_DEST, { rTokenDist: bn(0), rsrDist: bn(0) }) + + // Check no tokens were transferred + expect(await rsr.balanceOf(stRSR.address)).to.equal(stRSRBal) + expect(await rsr.balanceOf(rsrTrader.address)).to.equal(issueAmount) + + // Check updated distributions + const [newRTokenTotal, newRsrTotal] = await distributor.totals() + expect(newRsrTotal).equal(bn(60)) + expect(newRTokenTotal).equal(bn(0)) + }) + it('Should return tokens to BackingManager correctly - rsrTrader.returnTokens()', async () => { // Mint tokens - await rsr.connect(owner).mint(rsrTrader.address, issueAmount) await token0.connect(owner).mint(rsrTrader.address, issueAmount.add(1)) await token1.connect(owner).mint(rsrTrader.address, issueAmount.add(2)) @@ -676,6 +798,9 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { ).to.be.revertedWith('rsrTotal > 0') await distributor.setDistribution(STRSR_DEST, { rTokenDist: bn('0'), rsrDist: bn('0') }) + // Mint RSR + await rsr.connect(owner).mint(rsrTrader.address, issueAmount) + // Should fail for unregistered token await assetRegistry.connect(owner).unregister(collateral1.address) await expect( @@ -981,6 +1106,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { minBuyAmtRToken.div(bn('1e15')) ) }) + it('Should be able to start a dust auction BATCH_AUCTION, if enabled', async () => { const minTrade = bn('1e18') @@ -1036,26 +1162,26 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { it('Should only be able to start a dust auction BATCH_AUCTION (and not DUTCH_AUCTION) if oracle has failed', async () => { const minTrade = bn('1e18') - await rTokenTrader.connect(owner).setMinTradeVolume(minTrade) + await rsrTrader.connect(owner).setMinTradeVolume(minTrade) const dustAmount = bn('1e17') - await token0.connect(addr1).transfer(rTokenTrader.address, dustAmount) + await token0.connect(addr1).transfer(rsrTrader.address, dustAmount) - const p1RevenueTrader = await ethers.getContractAt('RevenueTraderP1', rTokenTrader.address) await setOraclePrice(collateral0.address, bn(0)) await collateral0.refresh() await advanceTime(PRICE_TIMEOUT.add(ORACLE_TIMEOUT).toString()) - await setOraclePrice(collateral1.address, bn('1e8')) + await setOraclePrice(rsrAsset.address, bn('1e8')) - const p = await collateral0.lotPrice() + const p = await collateral0.price() expect(p[0]).to.equal(0) - expect(p[1]).to.equal(0) - await expect( - p1RevenueTrader.manageTokens([token0.address], [TradeKind.DUTCH_AUCTION]) - ).to.revertedWith('bad sell pricing') + expect(p[1]).to.equal(MAX_UINT192) await expect( - p1RevenueTrader.manageTokens([token0.address], [TradeKind.BATCH_AUCTION]) - ).to.emit(rTokenTrader, 'TradeStarted') + rsrTrader.manageTokens([token0.address], [TradeKind.DUTCH_AUCTION]) + ).to.revertedWith('dutch auctions require live prices') + await expect(rsrTrader.manageTokens([token0.address], [TradeKind.BATCH_AUCTION])).to.emit( + rsrTrader, + 'TradeStarted' + ) }) it('Should not launch an auction for 1 qTok', async () => { @@ -1100,7 +1226,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { ORACLE_ERROR, aaveToken.address, bn(606), // 2 qTok auction at $300 (after accounting for price.high) - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) // Set a very high price @@ -1181,7 +1307,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { ORACLE_ERROR, aaveToken.address, MAX_UINT192, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) @@ -1192,7 +1318,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { ORACLE_ERROR, rsr.address, MAX_UINT192, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) @@ -1360,7 +1486,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { ORACLE_ERROR, aaveToken.address, fp('1'), - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) @@ -1411,7 +1537,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(await rToken.balanceOf(furnace.address)).to.equal(0) // Expected values based on Prices between AAVE and RSR = 1 to 1 (for simplification) - const sellAmt: BigNumber = fp('1').mul(100).div(101) // due to oracle error + const sellAmt: BigNumber = fp('1').mul(100).div(99) // due to oracle error const minBuyAmt: BigNumber = await toMinBuyAmt(sellAmt, fp('1'), fp('1')) // Run auctions @@ -1559,7 +1685,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { ORACLE_ERROR, aaveToken.address, fp('1'), - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) @@ -1593,7 +1719,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { // Collect revenue // Expected values based on Prices between AAVE and RToken = 1 (for simplification) - const sellAmt: BigNumber = fp('1').mul(100).div(101) // due to high price setting trade size + const sellAmt: BigNumber = fp('1').mul(100).div(99) // due to high price setting trade size const minBuyAmt: BigNumber = await toMinBuyAmt(sellAmt, fp('1'), fp('1')) await expectEvents(backingManager.claimRewards(), [ @@ -1758,7 +1884,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { ORACLE_ERROR, aaveToken.address, fp('1'), - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) @@ -1791,7 +1917,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { // Collect revenue // Expected values based on Prices between AAVE and RSR/RToken = 1 to 1 (for simplification) - const sellAmt: BigNumber = fp('1').mul(100).div(101) // due to high price setting trade size + const sellAmt: BigNumber = fp('1').mul(100).div(99) // due to high price setting trade size const minBuyAmt: BigNumber = await toMinBuyAmt(sellAmt, fp('1'), fp('1')) const sellAmtRToken: BigNumber = rewardAmountAAVE.mul(20).div(100) // All Rtokens can be sold - 20% of total comp based on f @@ -1969,10 +2095,6 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { it('Should only allow RevenueTraders to call distribute()', async () => { const distAmount: BigNumber = bn('100e18') - // Transfer some RSR to RevenueTraders - await rsr.connect(addr1).transfer(rTokenTrader.address, distAmount) - await rsr.connect(addr1).transfer(rsrTrader.address, distAmount) - // Set f = 1 await expect( distributor @@ -1990,6 +2112,10 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { .to.emit(distributor, 'DistributionSet') .withArgs(STRSR_DEST, bn(0), bn(1)) + // Transfer some RSR to RevenueTraders + await rsr.connect(addr1).transfer(rTokenTrader.address, distAmount) + await rsr.connect(addr1).transfer(rsrTrader.address, distAmount) + // Check funds in RevenueTraders and destinations expect(await rsr.balanceOf(rTokenTrader.address)).to.equal(distAmount) expect(await rsr.balanceOf(rsrTrader.address)).to.equal(distAmount) @@ -2008,52 +2134,365 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { ).to.be.revertedWith('RevenueTraders only') }) - // Should succeed for RevenueTraders - await whileImpersonating(rTokenTrader.address, async (bmSigner) => { - await rsr.connect(bmSigner).approve(distributor.address, distAmount) - await distributor.connect(bmSigner).distribute(rsr.address, distAmount) + // Should succeed for RevenueTraders + await whileImpersonating(rTokenTrader.address, async (bmSigner) => { + await rsr.connect(bmSigner).approve(distributor.address, distAmount) + await distributor.connect(bmSigner).distribute(rsr.address, distAmount) + }) + await whileImpersonating(rsrTrader.address, async (bmSigner) => { + await rsr.connect(bmSigner).approve(distributor.address, distAmount) + await distributor.connect(bmSigner).distribute(rsr.address, distAmount) + }) + + // RSR should be in staking pool + expect(await rsr.balanceOf(stRSR.address)).to.equal(distAmount.mul(2)) + }) + + it('Should revert if no distribution exists for a specific token', async () => { + // Check funds in Backing Manager and destinations + expect(await rsr.balanceOf(backingManager.address)).to.equal(0) + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rToken.balanceOf(furnace.address)).to.equal(0) + + // Set f = 0, avoid dropping tokens + await expect( + distributor + .connect(owner) + .setDistribution(FURNACE_DEST, { rTokenDist: bn(1), rsrDist: bn(0) }) + ) + .to.emit(distributor, 'DistributionSet') + .withArgs(FURNACE_DEST, bn(1), bn(0)) + await expect( + distributor + .connect(owner) + .setDistribution(STRSR_DEST, { rTokenDist: bn(0), rsrDist: bn(0) }) + ) + .to.emit(distributor, 'DistributionSet') + .withArgs(STRSR_DEST, bn(0), bn(0)) + + await whileImpersonating(rTokenTrader.address, async (bmSigner) => { + await expect( + distributor.connect(bmSigner).distribute(rsr.address, bn(100)) + ).to.be.revertedWith('nothing to distribute') + }) + + // Check funds, nothing changed + expect(await rsr.balanceOf(backingManager.address)).to.equal(0) + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rToken.balanceOf(furnace.address)).to.equal(0) + }) + + it('Should not start trades if no distribution defined', async () => { + // Check funds in Backing Manager and destinations + expect(await rsr.balanceOf(backingManager.address)).to.equal(0) + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rToken.balanceOf(furnace.address)).to.equal(0) + + // Set f = 0, avoid dropping tokens + await expect( + distributor + .connect(owner) + .setDistribution(FURNACE_DEST, { rTokenDist: bn(1), rsrDist: bn(0) }) + ) + .to.emit(distributor, 'DistributionSet') + .withArgs(FURNACE_DEST, bn(1), bn(0)) + await expect( + distributor + .connect(owner) + .setDistribution(STRSR_DEST, { rTokenDist: bn(0), rsrDist: bn(0) }) + ) + .to.emit(distributor, 'DistributionSet') + .withArgs(STRSR_DEST, bn(0), bn(0)) + + await expect( + rsrTrader.manageTokens([rsr.address], [TradeKind.BATCH_AUCTION]) + ).to.be.revertedWith('zero distribution') + + // Check funds, nothing changed + expect(await rsr.balanceOf(backingManager.address)).to.equal(0) + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rToken.balanceOf(furnace.address)).to.equal(0) + }) + + it('Should handle no distribution defined when settling trade', async () => { + // Set COMP tokens as reward + rewardAmountCOMP = bn('0.8e18') + + // COMP Rewards + await compoundMock.setRewards(backingManager.address, rewardAmountCOMP) + + // Collect revenue + // Expected values based on Prices between COMP and RSR/RToken = 1 to 1 (for simplification) + const sellAmt: BigNumber = rewardAmountCOMP.mul(60).div(100) // due to f = 60% + const minBuyAmt: BigNumber = await toMinBuyAmt(sellAmt, fp('1'), fp('1')) + + const sellAmtRToken: BigNumber = rewardAmountCOMP.sub(sellAmt) // Remainder + const minBuyAmtRToken: BigNumber = await toMinBuyAmt(sellAmtRToken, fp('1'), fp('1')) + + await expectEvents(backingManager.claimRewards(), [ + { + contract: token3, + name: 'RewardsClaimed', + args: [compToken.address, rewardAmountCOMP], + emitted: true, + }, + { + contract: token2, + name: 'RewardsClaimed', + args: [aaveToken.address, bn(0)], + emitted: true, + }, + ]) + + // Check status of destinations at this point + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rToken.balanceOf(furnace.address)).to.equal(0) + + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: rsrTrader, + name: 'TradeStarted', + args: [anyValue, compToken.address, rsr.address, sellAmt, withinQuad(minBuyAmt)], + emitted: true, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + args: [ + anyValue, + compToken.address, + rToken.address, + sellAmtRToken, + withinQuad(minBuyAmtRToken), + ], + emitted: true, + }, + ]) + + const auctionTimestamp: number = await getLatestBlockTimestamp() + + // Check auctions registered + // COMP -> RSR Auction + await expectTrade(rsrTrader, { + sell: compToken.address, + buy: rsr.address, + endTime: auctionTimestamp + Number(config.batchAuctionLength), + externalId: bn('0'), + }) + + // COMP -> RToken Auction + await expectTrade(rTokenTrader, { + sell: compToken.address, + buy: rToken.address, + endTime: auctionTimestamp + Number(config.batchAuctionLength), + externalId: bn('1'), + }) + + // Check funds in Market + expect(await compToken.balanceOf(gnosis.address)).to.equal(rewardAmountCOMP) + + // Advance time till auction ended + await advanceTime(config.batchAuctionLength.add(100).toString()) + + // Perform Mock Bids for RSR and RToken (addr1 has balance) + await rsr.connect(addr1).approve(gnosis.address, minBuyAmt) + await rToken.connect(addr1).approve(gnosis.address, minBuyAmtRToken) + await gnosis.placeBid(0, { + bidder: addr1.address, + sellAmount: sellAmt, + buyAmount: minBuyAmt, + }) + await gnosis.placeBid(1, { + bidder: addr1.address, + sellAmount: sellAmtRToken, + buyAmount: minBuyAmtRToken, + }) + + // Set no distribution for StRSR + // Set f = 0, avoid dropping tokens + await expect( + distributor + .connect(owner) + .setDistribution(FURNACE_DEST, { rTokenDist: bn(1), rsrDist: bn(0) }) + ) + .to.emit(distributor, 'DistributionSet') + .withArgs(FURNACE_DEST, bn(1), bn(0)) + await expect( + distributor + .connect(owner) + .setDistribution(STRSR_DEST, { rTokenDist: bn(0), rsrDist: bn(0) }) + ) + .to.emit(distributor, 'DistributionSet') + .withArgs(STRSR_DEST, bn(0), bn(0)) + + // Close auctions + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: rsrTrader, + name: 'TradeSettled', + args: [anyValue, compToken.address, rsr.address, sellAmt, minBuyAmt], + emitted: true, + }, + { + contract: rTokenTrader, + name: 'TradeSettled', + args: [anyValue, compToken.address, rToken.address, sellAmtRToken, minBuyAmtRToken], + emitted: true, + }, + { + contract: rsrTrader, + name: 'TradeStarted', + emitted: false, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + emitted: false, + }, + ]) + + // Check balances + // StRSR - Still in trader, was not distributed due to zero distribution + expect(await rsr.balanceOf(rsrTrader.address)).to.equal(minBuyAmt) + expect(await rsr.balanceOf(stRSR.address)).to.equal(bn(0)) + + // Furnace - RTokens transferred to destination + expect(await rToken.balanceOf(rTokenTrader.address)).to.equal(bn(0)) + expect(await rToken.balanceOf(furnace.address)).to.closeTo( + minBuyAmtRToken, + minBuyAmtRToken.div(bn('1e15')) + ) + }) + + it('Should allow to settle trade (and not distribute) even if trading paused or frozen', async () => { + // Set COMP tokens as reward + rewardAmountCOMP = bn('0.8e18') + + // COMP Rewards + await compoundMock.setRewards(backingManager.address, rewardAmountCOMP) + + // Collect revenue + // Expected values based on Prices between COMP and RSR/RToken = 1 to 1 (for simplification) + const sellAmt: BigNumber = rewardAmountCOMP.mul(60).div(100) // due to f = 60% + const minBuyAmt: BigNumber = await toMinBuyAmt(sellAmt, fp('1'), fp('1')) + + const sellAmtRToken: BigNumber = rewardAmountCOMP.sub(sellAmt) // Remainder + const minBuyAmtRToken: BigNumber = await toMinBuyAmt(sellAmtRToken, fp('1'), fp('1')) + + await expectEvents(backingManager.claimRewards(), [ + { + contract: token3, + name: 'RewardsClaimed', + args: [compToken.address, rewardAmountCOMP], + emitted: true, + }, + { + contract: token2, + name: 'RewardsClaimed', + args: [aaveToken.address, bn(0)], + emitted: true, + }, + ]) + + // Check status of destinations at this point + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rToken.balanceOf(furnace.address)).to.equal(0) + + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: rsrTrader, + name: 'TradeStarted', + args: [anyValue, compToken.address, rsr.address, sellAmt, withinQuad(minBuyAmt)], + emitted: true, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + args: [ + anyValue, + compToken.address, + rToken.address, + sellAmtRToken, + withinQuad(minBuyAmtRToken), + ], + emitted: true, + }, + ]) + + const auctionTimestamp: number = await getLatestBlockTimestamp() + + // Check auctions registered + // COMP -> RSR Auction + await expectTrade(rsrTrader, { + sell: compToken.address, + buy: rsr.address, + endTime: auctionTimestamp + Number(config.batchAuctionLength), + externalId: bn('0'), + }) + + // COMP -> RToken Auction + await expectTrade(rTokenTrader, { + sell: compToken.address, + buy: rToken.address, + endTime: auctionTimestamp + Number(config.batchAuctionLength), + externalId: bn('1'), + }) + + // Check funds in Market + expect(await compToken.balanceOf(gnosis.address)).to.equal(rewardAmountCOMP) + + // Advance time till auction ended + await advanceTime(config.batchAuctionLength.add(100).toString()) + + // Perform Mock Bids for RSR and RToken (addr1 has balance) + await rsr.connect(addr1).approve(gnosis.address, minBuyAmt) + await rToken.connect(addr1).approve(gnosis.address, minBuyAmtRToken) + await gnosis.placeBid(0, { + bidder: addr1.address, + sellAmount: sellAmt, + buyAmount: minBuyAmt, }) - await whileImpersonating(rsrTrader.address, async (bmSigner) => { - await rsr.connect(bmSigner).approve(distributor.address, distAmount) - await distributor.connect(bmSigner).distribute(rsr.address, distAmount) + await gnosis.placeBid(1, { + bidder: addr1.address, + sellAmount: sellAmtRToken, + buyAmount: minBuyAmtRToken, }) - // RSR should be in staking pool - expect(await rsr.balanceOf(stRSR.address)).to.equal(distAmount.mul(2)) - }) - - it('Should revert if no distribution exists for a specific token', async () => { - // Check funds in Backing Manager and destinations - expect(await rsr.balanceOf(backingManager.address)).to.equal(0) - expect(await rsr.balanceOf(stRSR.address)).to.equal(0) - expect(await rToken.balanceOf(furnace.address)).to.equal(0) + // Pause Trading + await main.connect(owner).pauseTrading() - // Set f = 0, avoid dropping tokens - await expect( - distributor - .connect(owner) - .setDistribution(FURNACE_DEST, { rTokenDist: bn(1), rsrDist: bn(0) }) - ) - .to.emit(distributor, 'DistributionSet') - .withArgs(FURNACE_DEST, bn(1), bn(0)) - await expect( - distributor - .connect(owner) - .setDistribution(STRSR_DEST, { rTokenDist: bn(0), rsrDist: bn(0) }) - ) - .to.emit(distributor, 'DistributionSet') - .withArgs(STRSR_DEST, bn(0), bn(0)) + // Close auctions + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: rsrTrader, + name: 'TradeSettled', + args: [anyValue, compToken.address, rsr.address, sellAmt, minBuyAmt], + emitted: true, + }, + { + contract: rTokenTrader, + name: 'TradeSettled', + args: [anyValue, compToken.address, rToken.address, sellAmtRToken, minBuyAmtRToken], + emitted: true, + }, + { + contract: rsrTrader, + name: 'TradeStarted', + emitted: false, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + emitted: false, + }, + ]) - await whileImpersonating(rTokenTrader.address, async (bmSigner) => { - await expect( - distributor.connect(bmSigner).distribute(rsr.address, bn(100)) - ).to.be.revertedWith('nothing to distribute') - }) + // Distribution did not occurr, funds are in Traders + expect(await rsr.balanceOf(rsrTrader.address)).to.equal(minBuyAmt) + expect(await rToken.balanceOf(rTokenTrader.address)).to.equal(minBuyAmtRToken) - // Check funds, nothing changed - expect(await rsr.balanceOf(backingManager.address)).to.equal(0) - expect(await rsr.balanceOf(stRSR.address)).to.equal(0) - expect(await rToken.balanceOf(furnace.address)).to.equal(0) + expect(await rsr.balanceOf(stRSR.address)).to.equal(bn(0)) + expect(await rToken.balanceOf(furnace.address)).to.equal(bn(0)) }) it('Should trade even if price for buy token = 0', async () => { @@ -2244,11 +2683,133 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { }, ]) + // Check broker disabled (batch) + expect(await broker.batchTradeDisabled()).to.equal(true) + // Check funds at destinations expect(await rsr.balanceOf(stRSR.address)).to.be.closeTo(minBuyAmt.sub(10), 50) expect(await rToken.balanceOf(furnace.address)).to.be.closeTo(minBuyAmtRToken.sub(10), 50) }) + it('Should report violation even if paused or frozen', async () => { + // This test needs to be in this file and not Broker.test.ts because settleTrade() + // requires the BackingManager _actually_ started the trade + + rewardAmountAAVE = bn('0.5e18') + + // AAVE Rewards + await token2.setRewards(backingManager.address, rewardAmountAAVE) + + // Collect revenue + // Expected values based on Prices between AAVE and RSR/RToken = 1 to 1 (for simplification) + const sellAmt: BigNumber = rewardAmountAAVE.mul(60).div(100) // due to f = 60% + const minBuyAmt: BigNumber = await toMinBuyAmt(sellAmt, fp('1'), fp('1')) + + const sellAmtRToken: BigNumber = rewardAmountAAVE.sub(sellAmt) // Remainder + const minBuyAmtRToken: BigNumber = await toMinBuyAmt(sellAmtRToken, fp('1'), fp('1')) + + // Claim rewards + await facadeTest.claimRewards(rToken.address) + + // Check status of destinations at this point + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rToken.balanceOf(furnace.address)).to.equal(0) + + // Run auctions + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: rsrTrader, + name: 'TradeStarted', + args: [anyValue, aaveToken.address, rsr.address, sellAmt, withinQuad(minBuyAmt)], + emitted: true, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + args: [ + anyValue, + aaveToken.address, + rToken.address, + sellAmtRToken, + withinQuad(minBuyAmtRToken), + ], + emitted: true, + }, + ]) + + // Advance time till auction ended + await advanceTime(config.batchAuctionLength.add(100).toString()) + + // Perform Mock Bids for RSR and RToken (addr1 has balance) + // In order to force deactivation we provide an amount below minBuyAmt, this will represent for our tests an invalid behavior although in a real scenario would retrigger auction + // NOTE: DIFFERENT BEHAVIOR WILL BE OBSERVED ON PRODUCTION GNOSIS AUCTIONS + await rsr.connect(addr1).approve(gnosis.address, minBuyAmt) + await rToken.connect(addr1).approve(gnosis.address, minBuyAmtRToken) + await gnosis.placeBid(0, { + bidder: addr1.address, + sellAmount: sellAmt, + buyAmount: minBuyAmt.sub(10), // Forces in our mock an invalid behavior + }) + await gnosis.placeBid(1, { + bidder: addr1.address, + sellAmount: sellAmtRToken, + buyAmount: minBuyAmtRToken.sub(10), // Forces in our mock an invalid behavior + }) + + // Freeze protocol + await main.connect(owner).freezeShort() + + // Close auctions - Will end trades and also report violation + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: broker, + name: 'BatchTradeDisabledSet', + args: [false, true], + emitted: true, + }, + { + contract: rsrTrader, + name: 'TradeSettled', + args: [anyValue, aaveToken.address, rsr.address, sellAmt, minBuyAmt.sub(10)], + emitted: true, + }, + { + contract: rTokenTrader, + name: 'TradeSettled', + args: [ + anyValue, + aaveToken.address, + rToken.address, + sellAmtRToken, + minBuyAmtRToken.sub(10), + ], + emitted: true, + }, + { + contract: rsrTrader, + name: 'TradeStarted', + emitted: false, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + emitted: false, + }, + ]) + + // Check broker disabled (batch) + expect(await broker.batchTradeDisabled()).to.equal(true) + + // Funds are not distributed if paused or frozen + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rsr.balanceOf(rsrTrader.address)).to.be.closeTo(minBuyAmt.sub(10), 50) + expect(await rToken.balanceOf(furnace.address)).to.equal(0) + expect(await rToken.balanceOf(rTokenTrader.address)).to.be.closeTo( + minBuyAmtRToken.sub(10), + 50 + ) + }) + it('Should not report violation when Dutch Auction clears in geometric phase', async () => { // This test needs to be in this file and not Broker.test.ts because settleTrade() // requires the BackingManager _actually_ started the trade @@ -2827,7 +3388,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR, erc20: token2.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.05'), delayUntilDefault: await collateral2.delayUntilDefault(), @@ -2954,7 +3515,6 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { rTokenAsset.address, collateral0.address, issueAmount, - config.minTradeVolume, config.maxTradeSlippage ) expect(actual).to.be.closeTo(expected, expected.div(bn('1e15'))) @@ -3014,7 +3574,6 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { rTokenAsset.address, collateral0.address, issueAmount, - config.minTradeVolume, config.maxTradeSlippage ) expect(await rTokenTrader.tradesOpen()).to.equal(0) @@ -3791,6 +4350,106 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(await token2.balanceOf(rsrTrader.address)).to.equal(0) expect(await token2.balanceOf(rTokenTrader.address)).to.equal(0) }) + + it('Should handle backingBuffer when minting RTokens from collateral appreciation', async () => { + // Set distribution for RToken only (f=0) + await distributor + .connect(owner) + .setDistribution(FURNACE_DEST, { rTokenDist: bn(1), rsrDist: bn(0) }) + + await distributor + .connect(owner) + .setDistribution(STRSR_DEST, { rTokenDist: bn(0), rsrDist: bn(0) }) + + // Set Backing buffer + const backingBuffer = fp('0.05') + await backingManager.connect(owner).setBackingBuffer(backingBuffer) + + // Issue additional RTokens + const newIssueAmount = bn('900e18') + await rToken.connect(addr1).issue(newIssueAmount) + + // Check Price and Assets value + const totalIssuedAmount = issueAmount.add(newIssueAmount) + await expectRTokenPrice(rTokenAsset.address, fp('1'), ORACLE_ERROR) + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.equal( + totalIssuedAmount + ) + expect(await rToken.totalSupply()).to.equal(totalIssuedAmount) + + // Change redemption rate for AToken and CToken to double + await token2.setExchangeRate(fp('1.10')) + await token3.setExchangeRate(fp('1.10')) + await collateral2.refresh() + await collateral3.refresh() + + // Check Price (unchanged) and Assets value (now 10% higher) + await expectRTokenPrice(rTokenAsset.address, fp('1'), ORACLE_ERROR) + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.equal( + totalIssuedAmount.mul(110).div(100) + ) + expect(await rToken.totalSupply()).to.equal(totalIssuedAmount) + + // Check status of destinations at this point + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rToken.balanceOf(rsrTrader.address)).to.equal(0) + expect(await rToken.balanceOf(furnace.address)).to.equal(0) + + // Set expected minting, based on f = 0.6 + const excessRevenue = totalIssuedAmount + .mul(110) + .div(100) + .mul(BN_SCALE_FACTOR) + .div(fp('1').add(backingBuffer)) + .sub(await rToken.basketsNeeded()) + + // Set expected auction values + const expectedToFurnace = excessRevenue + const currentTotalSupply: BigNumber = await rToken.totalSupply() + const newTotalSupply: BigNumber = currentTotalSupply.add(excessRevenue) + + // Collect revenue and mint new tokens + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: rToken, + name: 'Transfer', + args: [ZERO_ADDRESS, backingManager.address, withinQuad(excessRevenue)], + emitted: true, + }, + { + contract: rsrTrader, + name: 'TradeStarted', + emitted: false, + }, + ]) + + // Check Price (unchanged) and Assets value - Supply has increased 10% + await expectRTokenPrice(rTokenAsset.address, fp('1'), ORACLE_ERROR) + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.equal( + totalIssuedAmount.mul(110).div(100) + ) + expect(await rToken.totalSupply()).to.be.closeTo( + newTotalSupply, + newTotalSupply.mul(5).div(1000) + ) // within 0.5% + + // Check destinations after newly minted tokens + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rToken.balanceOf(rsrTrader.address)).to.equal(0) + expect(await rToken.balanceOf(furnace.address)).to.be.closeTo( + expectedToFurnace, + expectedToFurnace.mul(5).div(1000) + ) + + // Check Price and Assets value - RToken price increases due to melting + const updatedRTokenPrice: BigNumber = newTotalSupply + .mul(BN_SCALE_FACTOR) + .div(await rToken.totalSupply()) + await expectRTokenPrice(rTokenAsset.address, updatedRTokenPrice, ORACLE_ERROR) + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.equal( + totalIssuedAmount.mul(110).div(100) + ) + }) }) context('With simple basket of ATokens and CTokens: no issued RTokens', function () { @@ -3931,7 +4590,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { ORACLE_ERROR, compToken.address, config.rTokenMaxTradeVolume, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) diff --git a/test/ZZStRSR.test.ts b/test/ZZStRSR.test.ts index 927858718f..81629b3426 100644 --- a/test/ZZStRSR.test.ts +++ b/test/ZZStRSR.test.ts @@ -1,5 +1,6 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' import { expect } from 'chai' import { signERC2612Permit } from 'eth-permit' import { BigNumber, ContractFactory } from 'ethers' @@ -536,6 +537,24 @@ describe(`StRSRP${IMPLEMENTATION} contract`, () => { await expect(stRSR.connect(addr1).unstake(0)).to.be.revertedWith('frozen or trading paused') }) + it('Should emit UnstakingStarted event with draftEra -- regression test 01/18/2024', async () => { + const amount: BigNumber = bn('1000e18') + + // Stake + await rsr.connect(addr1).approve(stRSR.address, amount) + await stRSR.connect(addr1).stake(amount) + + // Seize half the RSR, bumping the draftEra because the withdrawal queue is empty + await whileImpersonating(backingManager.address, async (signer) => { + await stRSR.connect(signer).seizeRSR(amount.div(2)) + }) + + // Unstake + await expect(stRSR.connect(addr1).unstake(amount)) + .emit(stRSR, 'UnstakingStarted') + .withArgs(0, 2, addr1.address, amount.div(2), amount, anyValue) + }) + it('Should create Pending withdrawal when unstaking', async () => { const amount: BigNumber = bn('1000e18') @@ -2009,7 +2028,7 @@ describe(`StRSRP${IMPLEMENTATION} contract`, () => { await expect(stRSR.connect(addr1).unstake(one)) .emit(stRSR, 'UnstakingStarted') - .withArgs(0, 1, addr1.address, bn(0), one, availableAt) + .withArgs(0, 2, addr1.address, bn(0), one, availableAt) // Check withdrawal properly registered - Check draft era //await expectWithdrawal(addr1.address, 0, { rsrAmount: bn(1) }) diff --git a/test/__snapshots__/Broker.test.ts.snap b/test/__snapshots__/Broker.test.ts.snap index fc823d852b..634c60aac7 100644 --- a/test/__snapshots__/Broker.test.ts.snap +++ b/test/__snapshots__/Broker.test.ts.snap @@ -2,20 +2,20 @@ exports[`BrokerP1 contract #fast Gas Reporting DutchTrade Initialize Trade 1`] = `251984`; -exports[`BrokerP1 contract #fast Gas Reporting DutchTrade Open Trade 1`] = `361087`; +exports[`BrokerP1 contract #fast Gas Reporting DutchTrade Open Trade 1`] = `366975`; -exports[`BrokerP1 contract #fast Gas Reporting DutchTrade Open Trade 2`] = `363202`; +exports[`BrokerP1 contract #fast Gas Reporting DutchTrade Open Trade 2`] = `369090`; -exports[`BrokerP1 contract #fast Gas Reporting DutchTrade Open Trade 3`] = `365340`; +exports[`BrokerP1 contract #fast Gas Reporting DutchTrade Open Trade 3`] = `371228`; exports[`BrokerP1 contract #fast Gas Reporting DutchTrade Settle Trade 1`] = `63333`; -exports[`BrokerP1 contract #fast Gas Reporting GnosisTrade Initialize Trade 1`] = `451427`; +exports[`BrokerP1 contract #fast Gas Reporting GnosisTrade Initialize Trade 1`] = `453893`; -exports[`BrokerP1 contract #fast Gas Reporting GnosisTrade Open Trade 1`] = `541279`; +exports[`BrokerP1 contract #fast Gas Reporting GnosisTrade Open Trade 1`] = `543745`; -exports[`BrokerP1 contract #fast Gas Reporting GnosisTrade Open Trade 2`] = `529117`; +exports[`BrokerP1 contract #fast Gas Reporting GnosisTrade Open Trade 2`] = `531583`; -exports[`BrokerP1 contract #fast Gas Reporting GnosisTrade Open Trade 3`] = `531255`; +exports[`BrokerP1 contract #fast Gas Reporting GnosisTrade Open Trade 3`] = `533721`; -exports[`BrokerP1 contract #fast Gas Reporting GnosisTrade Settle Trade 1`] = `113056`; +exports[`BrokerP1 contract #fast Gas Reporting GnosisTrade Settle Trade 1`] = `113028`; diff --git a/test/__snapshots__/FacadeWrite.test.ts.snap b/test/__snapshots__/FacadeWrite.test.ts.snap index 848354f559..5318fd2f61 100644 --- a/test/__snapshots__/FacadeWrite.test.ts.snap +++ b/test/__snapshots__/FacadeWrite.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FacadeWrite contract Deployment Process Gas Reporting Phase 1 - RToken Deployment 1`] = `8393668`; +exports[`FacadeWrite contract Deployment Process Gas Reporting Phase 1 - RToken Deployment 1`] = `8330567`; exports[`FacadeWrite contract Deployment Process Gas Reporting Phase 2 - Deploy governance 1`] = `5464253`; diff --git a/test/__snapshots__/Furnace.test.ts.snap b/test/__snapshots__/Furnace.test.ts.snap index af06969a2f..e905f3eec0 100644 --- a/test/__snapshots__/Furnace.test.ts.snap +++ b/test/__snapshots__/Furnace.test.ts.snap @@ -1,35 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FurnaceP1 contract Gas Reporting Melt - A million periods, all at once 1`] = `83931`; +exports[`FurnaceP1 contract Gas Reporting Melt - A million periods, all at once 1`] = `71626`; -exports[`FurnaceP1 contract Gas Reporting Melt - A million periods, all at once 2`] = `89820`; +exports[`FurnaceP1 contract Gas Reporting Melt - A million periods, all at once 2`] = `77515`; -exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 1`] = `83931`; +exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 1`] = `71626`; -exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 2`] = `78303`; +exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 2`] = `65998`; -exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 3`] = `78303`; +exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 3`] = `65998`; -exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 4`] = `78303`; +exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 4`] = `65998`; -exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 5`] = `78303`; +exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 5`] = `65998`; -exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 6`] = `78303`; +exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 6`] = `65998`; -exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 7`] = `78303`; +exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 7`] = `65998`; -exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 8`] = `78303`; +exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 8`] = `65998`; -exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 9`] = `78303`; +exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 9`] = `65998`; -exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 10`] = `78303`; +exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 10`] = `65998`; -exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 11`] = `78303`; +exports[`FurnaceP1 contract Gas Reporting Melt - Many periods, one after the other 11`] = `65998`; -exports[`FurnaceP1 contract Gas Reporting Melt - One period 1`] = `64031`; +exports[`FurnaceP1 contract Gas Reporting Melt - One period 1`] = `51726`; -exports[`FurnaceP1 contract Gas Reporting Melt - One period 2`] = `80663`; +exports[`FurnaceP1 contract Gas Reporting Melt - One period 2`] = `68358`; -exports[`FurnaceP1 contract Gas Reporting Melt - One period 3`] = `78303`; +exports[`FurnaceP1 contract Gas Reporting Melt - One period 3`] = `65998`; -exports[`FurnaceP1 contract Gas Reporting Melt - One period 4`] = `40761`; +exports[`FurnaceP1 contract Gas Reporting Melt - One period 4`] = `28452`; diff --git a/test/__snapshots__/Main.test.ts.snap b/test/__snapshots__/Main.test.ts.snap index 06ba9d68c7..0771900efd 100644 --- a/test/__snapshots__/Main.test.ts.snap +++ b/test/__snapshots__/Main.test.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MainP1 contract Gas Reporting Asset Registry - Refresh 1`] = `357705`; +exports[`MainP1 contract Gas Reporting Asset Registry - Refresh 1`] = `393855`; -exports[`MainP1 contract Gas Reporting Asset Registry - Register Asset 1`] = `195889`; +exports[`MainP1 contract Gas Reporting Asset Registry - Register Asset 1`] = `245356`; -exports[`MainP1 contract Gas Reporting Asset Registry - Register Asset 2`] = `195889`; +exports[`MainP1 contract Gas Reporting Asset Registry - Register Asset 2`] = `245356`; -exports[`MainP1 contract Gas Reporting Asset Registry - Swap Registered Asset 1`] = `167045`; +exports[`MainP1 contract Gas Reporting Asset Registry - Swap Registered Asset 1`] = `224015`; -exports[`MainP1 contract Gas Reporting Asset Registry - Unregister Asset 1`] = `80532`; +exports[`MainP1 contract Gas Reporting Asset Registry - Unregister Asset 1`] = `80510`; -exports[`MainP1 contract Gas Reporting Asset Registry - Unregister Asset 2`] = `70044`; +exports[`MainP1 contract Gas Reporting Asset Registry - Unregister Asset 2`] = `70022`; diff --git a/test/__snapshots__/RToken.test.ts.snap b/test/__snapshots__/RToken.test.ts.snap index f50430c9b4..600063cf88 100644 --- a/test/__snapshots__/RToken.test.ts.snap +++ b/test/__snapshots__/RToken.test.ts.snap @@ -1,10 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RTokenP1 contract Gas Reporting Issuance: within block 1`] = `787453`; +exports[`RTokenP1 contract Gas Reporting Issuance: within block 1`] = `782176`; -exports[`RTokenP1 contract Gas Reporting Issuance: within block 2`] = `614457`; +exports[`RTokenP1 contract Gas Reporting Issuance: within block 2`] = `609176`; -exports[`RTokenP1 contract Gas Reporting Redemption 1`] = `589230`; +exports[`RTokenP1 contract Gas Reporting Redemption 1`] = `583880`; exports[`RTokenP1 contract Gas Reporting Transfer 1`] = `56658`; diff --git a/test/__snapshots__/Recollateralization.test.ts.snap b/test/__snapshots__/Recollateralization.test.ts.snap index 9e0d532f8e..1bcce9471c 100644 --- a/test/__snapshots__/Recollateralization.test.ts.snap +++ b/test/__snapshots__/Recollateralization.test.ts.snap @@ -1,19 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Recollateralization - P1 Gas Reporting rebalance() - DutchTrade 1`] = `1384418`; +exports[`Recollateralization - P1 Gas Reporting rebalance() - DutchTrade 1`] = `1396756`; -exports[`Recollateralization - P1 Gas Reporting rebalance() - DutchTrade 2`] = `1510705`; +exports[`Recollateralization - P1 Gas Reporting rebalance() - DutchTrade 2`] = `1518120`; -exports[`Recollateralization - P1 Gas Reporting rebalance() - DutchTrade 3`] = `747331`; +exports[`Recollateralization - P1 Gas Reporting rebalance() - DutchTrade 3`] = `750910`; -exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 1`] = `1680908`; +exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 1`] = `1715195`; -exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 2`] = `174808`; +exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 2`] = `179696`; -exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 3`] = `1613640`; +exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 3`] = `1657793`; -exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 4`] = `174808`; +exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 4`] = `179696`; -exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 5`] = `1702037`; +exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 5`] = `1733823`; -exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 6`] = `202908`; +exports[`Recollateralization - P1 Gas Reporting rebalance() - GnosisTrade 6`] = `207769`; diff --git a/test/__snapshots__/Revenues.test.ts.snap b/test/__snapshots__/Revenues.test.ts.snap index 81fa8bb746..24037c7f9a 100644 --- a/test/__snapshots__/Revenues.test.ts.snap +++ b/test/__snapshots__/Revenues.test.ts.snap @@ -1,27 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 1`] = `164974`; +exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 1`] = `168005`; -exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 2`] = `165027`; +exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 2`] = `168058`; -exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 3`] = `165027`; +exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 3`] = `168058`; -exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 4`] = `208624`; +exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 4`] = `211655`; -exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 5`] = `229377`; +exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 5`] = `232408`; -exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 6`] = `212277`; +exports[`Revenues - P1 Gas Reporting Claim and Sweep Rewards 6`] = `215308`; -exports[`Revenues - P1 Gas Reporting Selling RToken 1`] = `1008567`; +exports[`Revenues - P1 Gas Reporting Selling RToken 1`] = `1044935`; -exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 1`] = `773918`; +exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 1`] = `820357`; -exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 2`] = `1181227`; +exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 2`] = `1222455`; -exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 3`] = `311446`; +exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 3`] = `368496`; -exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 4`] = `266512`; +exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 4`] = `318685`; -exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 5`] = `739718`; +exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 5`] = `786157`; -exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 6`] = `242306`; +exports[`Revenues - P1 Gas Reporting Settle Trades / Manage Funds 6`] = `285704`; diff --git a/test/__snapshots__/ZZStRSR.test.ts.snap b/test/__snapshots__/ZZStRSR.test.ts.snap index dbc65bb91d..36ee8b72fa 100644 --- a/test/__snapshots__/ZZStRSR.test.ts.snap +++ b/test/__snapshots__/ZZStRSR.test.ts.snap @@ -2,7 +2,7 @@ exports[`StRSRP1 contract Gas Reporting Stake 1`] = `139717`; -exports[`StRSRP1 contract Gas Reporting Stake 2`] = `151759`; +exports[`StRSRP1 contract Gas Reporting Stake 2`] = `134917`; exports[`StRSRP1 contract Gas Reporting Transfer 1`] = `63409`; @@ -14,6 +14,6 @@ exports[`StRSRP1 contract Gas Reporting Unstake 1`] = `222609`; exports[`StRSRP1 contract Gas Reporting Unstake 2`] = `139758`; -exports[`StRSRP1 contract Gas Reporting Withdraw 1`] = `572011`; +exports[`StRSRP1 contract Gas Reporting Withdraw 1`] = `606291`; -exports[`StRSRP1 contract Gas Reporting Withdraw 2`] = `526015`; +exports[`StRSRP1 contract Gas Reporting Withdraw 2`] = `536425`; diff --git a/test/fixtures.ts b/test/fixtures.ts index ff881e60d0..a787359e1b 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -1,11 +1,23 @@ import { ContractFactory } from 'ethers' import { expect } from 'chai' -import hre, { ethers } from 'hardhat' +import hre, { ethers, upgrades } from 'hardhat' import { getChainId } from '../common/blockchain-utils' -import { IConfig, IImplementations, IRevenueShare, networkConfig } from '../common/configuration' +import { + IConfig, + IImplementations, + IMonitorParams, + IRevenueShare, + networkConfig, +} from '../common/configuration' import { expectInReceipt } from '../common/events' import { bn, fp } from '../common/numbers' -import { CollateralStatus, PAUSER, LONG_FREEZER, SHORT_FREEZER } from '../common/constants' +import { + CollateralStatus, + PAUSER, + LONG_FREEZER, + SHORT_FREEZER, + ZERO_ADDRESS, +} from '../common/constants' import { Asset, AssetRegistryP1, @@ -24,6 +36,7 @@ import { DutchTrade, FacadeRead, FacadeAct, + FacadeMonitor, FacadeTest, DistributorP1, FiatCollateral, @@ -71,14 +84,16 @@ export const SLOW = !!useEnv('SLOW') export const PRICE_TIMEOUT = bn('604800') // 1 week -export const ORACLE_TIMEOUT = bn('281474976710655').div(2) // type(uint48).max / 2 +export const ORACLE_TIMEOUT_PRE_BUFFER = bn('281474976710655').div(100) // type(uint48).max / 100 + +export const ORACLE_TIMEOUT = ORACLE_TIMEOUT_PRE_BUFFER.add(300) export const ORACLE_ERROR = fp('0.01') // 1% oracle error export const REVENUE_HIDING = fp('0') // no revenue hiding by default; test individually // This will have to be updated on each release -export const VERSION = '3.0.1' +export const VERSION = '3.1.0' export type Collateral = | FiatCollateral @@ -183,7 +198,7 @@ async function collateralFixture( oracleError: ORACLE_ERROR, erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: defaultThreshold, delayUntilDefault: delayUntilDefault, @@ -203,7 +218,7 @@ async function collateralFixture( oracleError: ORACLE_ERROR, erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: defaultThreshold, delayUntilDefault: delayUntilDefault, @@ -223,7 +238,7 @@ async function collateralFixture( oracleError: ORACLE_ERROR, erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: defaultThreshold, delayUntilDefault: delayUntilDefault, @@ -252,7 +267,7 @@ async function collateralFixture( oracleError: ORACLE_ERROR, erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: defaultThreshold, delayUntilDefault: delayUntilDefault, @@ -280,7 +295,7 @@ async function collateralFixture( oracleError: ORACLE_ERROR, erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: defaultThreshold, delayUntilDefault: delayUntilDefault, @@ -410,6 +425,7 @@ export interface DefaultFixture extends RSRAndCompAaveAndCollateralAndModuleFixt facade: FacadeRead facadeAct: FacadeAct facadeTest: FacadeTest + facadeMonitor: FacadeMonitor broker: TestIBroker rsrTrader: TestIRevenueTrader rTokenTrader: TestIRevenueTrader @@ -466,6 +482,11 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = }, } + // Setup Monitor Params (mock addrs for local deployment) + const monitorParams: IMonitorParams = { + AAVE_V2_DATA_PROVIDER_ADDR: ZERO_ADDRESS, + } + // Deploy TradingLib external library const TradingLibFactory: ContractFactory = await ethers.getContractFactory('TradingLibP0') const tradingLib: TradingLibP0 = await TradingLibFactory.deploy() @@ -482,6 +503,19 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = const FacadeTestFactory: ContractFactory = await ethers.getContractFactory('FacadeTest') const facadeTest = await FacadeTestFactory.deploy() + // Deploy FacadeMonitor + const FacadeMonitorFactory: ContractFactory = await ethers.getContractFactory('FacadeMonitor') + + const facadeMonitor = await upgrades.deployProxy( + FacadeMonitorFactory, + [owner.address], + { + kind: 'uups', + initializer: 'init', + constructorArgs: [monitorParams], + } + ) + // Deploy RSR chainlink feed const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( 'MockV3Aggregator' @@ -499,7 +533,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = ORACLE_ERROR, rsr.address, config.rTokenMaxTradeVolume, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) await rsrAsset.refresh() @@ -631,7 +665,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = ORACLE_ERROR, aaveToken.address, config.rTokenMaxTradeVolume, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) await aaveAsset.refresh() @@ -646,7 +680,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = ORACLE_ERROR, compToken.address, config.rTokenMaxTradeVolume, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) await compAsset.refresh() @@ -749,6 +783,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = facade, facadeAct, facadeTest, + facadeMonitor, rsrTrader, rTokenTrader, bySymbol, diff --git a/test/integration/AssetPlugins.test.ts b/test/integration/AssetPlugins.test.ts index 7e067fa875..71ae4bf11d 100644 --- a/test/integration/AssetPlugins.test.ts +++ b/test/integration/AssetPlugins.test.ts @@ -8,6 +8,7 @@ import { IMPLEMENTATION, ORACLE_ERROR, ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, REVENUE_HIDING, } from '../fixtures' @@ -19,7 +20,14 @@ import { expectEvents } from '../../common/events' import { bn, fp, toBNDecimals } from '../../common/numbers' import { advanceBlocks, advanceTime } from '../utils/time' import { whileImpersonating } from '../utils/impersonation' -import { expectPrice, expectRTokenPrice, expectUnpriced, setOraclePrice } from '../utils/oracles' +import { + expectDecayedPrice, + expectExactPrice, + expectPrice, + expectRTokenPrice, + expectUnpriced, + setOraclePrice, +} from '../utils/oracles' import forkBlockNumber from './fork-block-numbers' import { Asset, @@ -39,6 +47,7 @@ import { MockV3Aggregator, NonFiatCollateral, RTokenAsset, + SelfReferentialCollateral, StaticATokenLM, TestIBackingManager, TestIBasketHandler, @@ -1082,7 +1091,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, }) it('Should handle invalid/stale Price - Assets', async () => { - await advanceTime(ORACLE_TIMEOUT.toString()) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) // Stale Oracle await expectUnpriced(compAsset.address) @@ -1114,19 +1123,30 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, ORACLE_ERROR, networkConfig[chainId].tokens.stkAAVE || '', config.rTokenMaxTradeVolume, - MAX_ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) + await setOraclePrice(zeroPriceAsset.address, bn('1e10')) + await zeroPriceAsset.refresh() + + const initialPrice = await zeroPriceAsset.price() + await setOraclePrice(zeroPriceAsset.address, bn(0)) + await expectExactPrice(zeroPriceAsset.address, initialPrice) + // After oracle timeout, begins decay + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) await setOraclePrice(zeroPriceAsset.address, bn(0)) + await expectDecayedPrice(zeroPriceAsset.address) - // Unpriced + // After price timeout, unpriced + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(zeroPriceAsset.address, bn(0)) await expectUnpriced(zeroPriceAsset.address) }) it('Should handle invalid/stale Price - Collateral - Fiat', async () => { // Does not revert with stale price - await advanceTime(ORACLE_TIMEOUT.toString()) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) await expectUnpriced(daiCollateral.address) await expectUnpriced(usdcCollateral.address) @@ -1183,19 +1203,30 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, oracleError: ORACLE_ERROR, erc20: dai.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: MAX_ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, }) + await setOraclePrice(zeroFiatCollateral.address, bn('1e8')) await zeroFiatCollateral.refresh() + expect(await zeroFiatCollateral.status()).to.equal(CollateralStatus.SOUND) + const initialPrice = await zeroFiatCollateral.price() await setOraclePrice(zeroFiatCollateral.address, bn(0)) + await expectExactPrice(zeroFiatCollateral.address, initialPrice) - // Unpriced + // After oracle timeout, begins decay + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await setOraclePrice(zeroFiatCollateral.address, bn(0)) + await expectDecayedPrice(zeroFiatCollateral.address) + + // After price timeout, unpriced + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(zeroFiatCollateral.address, bn(0)) await expectUnpriced(zeroFiatCollateral.address) - // Refresh should mark status IFFY + // Marked IFFY after refresh await zeroFiatCollateral.refresh() expect(await zeroFiatCollateral.status()).to.equal(CollateralStatus.IFFY) }) @@ -1206,7 +1237,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, expect(await cUsdtCollateral.status()).to.equal(CollateralStatus.SOUND) // Does not revert with stale price - await advanceTime(ORACLE_TIMEOUT.toString()) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) // Compound await expectUnpriced(cDaiCollateral.address) @@ -1258,18 +1289,29 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, oracleError: ORACLE_ERROR, erc20: cDaiVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: MAX_ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, }, REVENUE_HIDING ) + await setOraclePrice(zeropriceCtokenCollateral.address, bn('1e8')) await zeropriceCtokenCollateral.refresh() + expect(await zeropriceCtokenCollateral.status()).to.equal(CollateralStatus.SOUND) + + const initialPrice = await zeropriceCtokenCollateral.price() + await setOraclePrice(zeropriceCtokenCollateral.address, bn(0)) + await expectExactPrice(zeropriceCtokenCollateral.address, initialPrice) + // After oracle timeout, begins decay + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) await setOraclePrice(zeropriceCtokenCollateral.address, bn(0)) + await expectDecayedPrice(zeropriceCtokenCollateral.address) - // Unpriced + // After price timeout, unpriced + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(zeropriceCtokenCollateral.address, bn(0)) await expectUnpriced(zeropriceCtokenCollateral.address) // Refresh should mark status IFFY @@ -1279,7 +1321,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, it('Should handle invalid/stale Price - Collateral - ATokens Fiat', async () => { // Does not revert with stale price - await advanceTime(ORACLE_TIMEOUT.toString()) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) // Aave await expectUnpriced(aDaiCollateral.address) @@ -1335,18 +1377,29 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, oracleError: ORACLE_ERROR, erc20: stataDai.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: MAX_ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, }, REVENUE_HIDING ) + await setOraclePrice(zeroPriceAtokenCollateral.address, bn('1e8')) await zeroPriceAtokenCollateral.refresh() + expect(await zeroPriceAtokenCollateral.status()).to.equal(CollateralStatus.SOUND) + + const initialPrice = await zeroPriceAtokenCollateral.price() + await setOraclePrice(zeroPriceAtokenCollateral.address, bn(0)) + await expectExactPrice(zeroPriceAtokenCollateral.address, initialPrice) + // After oracle timeout, begins decay + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) await setOraclePrice(zeroPriceAtokenCollateral.address, bn(0)) + await expectDecayedPrice(zeroPriceAtokenCollateral.address) - // Unpriced + // After price timeout, unpriced + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(zeroPriceAtokenCollateral.address, bn(0)) await expectUnpriced(zeroPriceAtokenCollateral.address) // Refresh should mark status IFFY @@ -1356,7 +1409,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, it('Should handle invalid/stale Price - Collateral - Non-Fiatcoins', async () => { // Does not revert with stale price - await advanceTime(ORACLE_TIMEOUT.toString()) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) // Aave await expectUnpriced(wbtcCollateral.address) @@ -1403,32 +1456,35 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, oracleError: ORACLE_ERROR, erc20: wbtc.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: MAX_ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold, delayUntilDefault, }, mockChainlinkFeed.address, - MAX_ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) + await setOraclePrice(zeroPriceNonFiatCollateral.address, bn('1e10')) await zeroPriceNonFiatCollateral.refresh() - // Set price = 0 - const chainlinkFeedAddr = await zeroPriceNonFiatCollateral.chainlinkFeed() - const v3Aggregator = await ethers.getContractAt('MockV3Aggregator', chainlinkFeedAddr) - await v3Aggregator.updateAnswer(bn(0)) + const initialPrice = await zeroPriceNonFiatCollateral.price() + await setOraclePrice(zeroPriceNonFiatCollateral.address, bn(0)) + await expectExactPrice(zeroPriceNonFiatCollateral.address, initialPrice) - // Unpriced - await expectUnpriced(zeroPriceNonFiatCollateral.address) + // After oracle timeout, begins decay + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await setOraclePrice(zeroPriceNonFiatCollateral.address, bn(0)) + await expectDecayedPrice(zeroPriceNonFiatCollateral.address) - // Refresh should mark status IFFY - await zeroPriceNonFiatCollateral.refresh() - expect(await zeroPriceNonFiatCollateral.status()).to.equal(CollateralStatus.IFFY) + // After price timeout, unpriced + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(zeroPriceNonFiatCollateral.address, bn(0)) + await expectUnpriced(zeroPriceNonFiatCollateral.address) }) it('Should handle invalid/stale Price - Collateral - CTokens Non-Fiat', async () => { // Does not revert with stale price - await advanceTime(ORACLE_TIMEOUT.toString()) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) // Compound await expectUnpriced(cWBTCCollateral.address) @@ -1480,36 +1536,39 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, oracleError: ORACLE_ERROR, erc20: cWBTCVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: MAX_ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold, delayUntilDefault, }, mockChainlinkFeed.address, - MAX_ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, REVENUE_HIDING ) ) + await setOraclePrice(zeropriceCtokenNonFiatCollateral.address, bn('1e10')) await zeropriceCtokenNonFiatCollateral.refresh() - // Set price = 0 - const chainlinkFeedAddr = await zeropriceCtokenNonFiatCollateral.targetUnitChainlinkFeed() - const v3Aggregator = await ethers.getContractAt('MockV3Aggregator', chainlinkFeedAddr) - await v3Aggregator.updateAnswer(bn(0)) + const initialPrice = await zeropriceCtokenNonFiatCollateral.price() + await setOraclePrice(zeropriceCtokenNonFiatCollateral.address, bn(0)) + await expectExactPrice(zeropriceCtokenNonFiatCollateral.address, initialPrice) - // Unpriced - await expectUnpriced(zeropriceCtokenNonFiatCollateral.address) + // After oracle timeout, begins decay + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await setOraclePrice(zeropriceCtokenNonFiatCollateral.address, bn(0)) + await expectDecayedPrice(zeropriceCtokenNonFiatCollateral.address) - // Refresh should mark status IFFY - await zeropriceCtokenNonFiatCollateral.refresh() - expect(await zeropriceCtokenNonFiatCollateral.status()).to.equal(CollateralStatus.IFFY) + // After price timeout, unpriced + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(zeropriceCtokenNonFiatCollateral.address, bn(0)) + await expectUnpriced(zeropriceCtokenNonFiatCollateral.address) }) it('Should handle invalid/stale Price - Collateral - Self-Referential', async () => { const delayUntilDefault = bn('86400') // 24h // Dows not revert with stale price - await advanceTime(ORACLE_TIMEOUT.toString()) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) // Aave await expectUnpriced(wethCollateral.address) @@ -1518,8 +1577,10 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, expect(await wethCollateral.status()).to.equal(CollateralStatus.IFFY) // Self referential collateral with no price - const nonpriceSelfReferentialCollateral: FiatCollateral = await ( - await ethers.getContractFactory('FiatCollateral') + const nonpriceSelfReferentialCollateral: SelfReferentialCollateral = < + SelfReferentialCollateral + >await ( + await ethers.getContractFactory('SelfReferentialCollateral') ).deploy({ priceTimeout: PRICE_TIMEOUT, chainlinkFeed: NO_PRICE_DATA_FEED, @@ -1540,28 +1601,40 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, expect(await nonpriceSelfReferentialCollateral.status()).to.equal(CollateralStatus.SOUND) // Self referential collateral with zero price - const zeroPriceSelfReferentialCollateral: FiatCollateral = await ( - await ethers.getContractFactory('FiatCollateral') + const zeroPriceSelfReferentialCollateral: SelfReferentialCollateral = < + SelfReferentialCollateral + >await ( + await ethers.getContractFactory('SelfReferentialCollateral') ).deploy({ priceTimeout: PRICE_TIMEOUT, chainlinkFeed: mockChainlinkFeed.address, oracleError: ORACLE_ERROR, erc20: weth.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: MAX_ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: bn('0'), delayUntilDefault, }) + await setOraclePrice(zeroPriceSelfReferentialCollateral.address, bn('1e10')) await zeroPriceSelfReferentialCollateral.refresh() + expect(await zeroPriceSelfReferentialCollateral.status()).to.equal(CollateralStatus.SOUND) + + const initialPrice = await zeroPriceSelfReferentialCollateral.price() + await setOraclePrice(zeroPriceSelfReferentialCollateral.address, bn(0)) + await expectExactPrice(zeroPriceSelfReferentialCollateral.address, initialPrice) - // Set price = 0 + // After oracle timeout, begins decay + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) await setOraclePrice(zeroPriceSelfReferentialCollateral.address, bn(0)) + await expectDecayedPrice(zeroPriceSelfReferentialCollateral.address) - // Unpriced + // After price timeout, unpriced + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(zeroPriceSelfReferentialCollateral.address, bn(0)) await expectUnpriced(zeroPriceSelfReferentialCollateral.address) - // Refresh should mark status IFFY + // Refresh should mark status DISABLED await zeroPriceSelfReferentialCollateral.refresh() expect(await zeroPriceSelfReferentialCollateral.status()).to.equal(CollateralStatus.IFFY) }) @@ -1570,7 +1643,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, const delayUntilDefault = bn('86400') // 24h // Does not revert with stale price - await advanceTime(ORACLE_TIMEOUT.toString()) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) // Compound await expectUnpriced(cETHCollateral.address) @@ -1621,7 +1694,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, oracleError: ORACLE_ERROR, erc20: cETHVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: MAX_ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: bn('0'), delayUntilDefault, @@ -1629,12 +1702,24 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, REVENUE_HIDING, await weth.decimals() ) + await setOraclePrice(zeroPriceCtokenSelfReferentialCollateral.address, bn('1e10')) await zeroPriceCtokenSelfReferentialCollateral.refresh() + expect(await zeroPriceCtokenSelfReferentialCollateral.status()).to.equal( + CollateralStatus.SOUND + ) - // Set price = 0 + const initialPrice = await zeroPriceCtokenSelfReferentialCollateral.price() await setOraclePrice(zeroPriceCtokenSelfReferentialCollateral.address, bn(0)) + await expectExactPrice(zeroPriceCtokenSelfReferentialCollateral.address, initialPrice) - // Unpriced + // After oracle timeout, begins decay + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await setOraclePrice(zeroPriceCtokenSelfReferentialCollateral.address, bn(0)) + await expectDecayedPrice(zeroPriceCtokenSelfReferentialCollateral.address) + + // After price timeout, unpriced + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(zeroPriceCtokenSelfReferentialCollateral.address, bn(0)) await expectUnpriced(zeroPriceCtokenSelfReferentialCollateral.address) // Refresh should mark status IFFY @@ -1646,7 +1731,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, it('Should handle invalid/stale Price - Collateral - EUR Fiat', async () => { // Does not revert with stale price - await advanceTime(ORACLE_TIMEOUT.toString()) + await advanceTime(ORACLE_TIMEOUT.add(PRICE_TIMEOUT).toString()) await expectUnpriced(eurtCollateral.address) @@ -1692,22 +1777,30 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, oracleError: ORACLE_ERROR, erc20: eurt.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: MAX_ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold, delayUntilDefault, }, mockChainlinkFeed.address, - MAX_ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) + await setOraclePrice(invalidPriceEURCollateral.address, bn('1e10')) await invalidPriceEURCollateral.refresh() + expect(await invalidPriceEURCollateral.status()).to.equal(CollateralStatus.SOUND) + + const initialPrice = await invalidPriceEURCollateral.price() + await setOraclePrice(invalidPriceEURCollateral.address, bn(0)) + await expectExactPrice(invalidPriceEURCollateral.address, initialPrice) - // Set price = 0 - const chainlinkFeedAddr = await invalidPriceEURCollateral.targetUnitChainlinkFeed() - const v3Aggregator = await ethers.getContractAt('MockV3Aggregator', chainlinkFeedAddr) - await v3Aggregator.updateAnswer(bn(0)) + // After oracle timeout, begins decay + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await setOraclePrice(invalidPriceEURCollateral.address, bn(0)) + await expectDecayedPrice(invalidPriceEURCollateral.address) - // With zero price + // After price timeout, unpriced + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(invalidPriceEURCollateral.address, bn(0)) await expectUnpriced(invalidPriceEURCollateral.address) // Refresh should mark status IFFY diff --git a/test/integration/EasyAuction.test.ts b/test/integration/EasyAuction.test.ts index 3d63f9e5b8..355d92f00c 100644 --- a/test/integration/EasyAuction.test.ts +++ b/test/integration/EasyAuction.test.ts @@ -551,10 +551,11 @@ describeFork(`Gnosis EasyAuction Mainnet Forking - P${IMPLEMENTATION}`, function }) it('should be able to scoop entire auction cheaply when minBuyAmount = 0', async () => { - // Make collateral0 lotPrice (0, 0) + // Make collateral0 price (0, FIX_MAX) await setOraclePrice(collateral0.address, bn('0')) await collateral0.refresh() await advanceTime(PRICE_TIMEOUT.add(ORACLE_TIMEOUT).toString()) + await setOraclePrice(collateral0.address, bn('0')) await setOraclePrice(await assetRegistry.toAsset(rsr.address), bn('1e8')) // force a revenue dust auction @@ -752,7 +753,7 @@ describeFork(`Gnosis EasyAuction Mainnet Forking - P${IMPLEMENTATION}`, function oracleError: ORACLE_ERROR, // shouldn't matter erc20: sellTok.address, maxTradeVolume: MAX_UINT192, - oracleTimeout: MAX_UINT48, + oracleTimeout: MAX_UINT48.sub(300), targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01'), // shouldn't matter delayUntilDefault: bn('604800'), // shouldn't matter @@ -764,7 +765,7 @@ describeFork(`Gnosis EasyAuction Mainnet Forking - P${IMPLEMENTATION}`, function oracleError: ORACLE_ERROR, // shouldn't matter erc20: buyTok.address, maxTradeVolume: MAX_UINT192, - oracleTimeout: MAX_UINT48, + oracleTimeout: MAX_UINT48.sub(300), targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.01'), // shouldn't matter delayUntilDefault: bn('604800'), // shouldn't matter diff --git a/test/integration/__snapshots__/CTokenVaultGas.test.ts.snap b/test/integration/__snapshots__/CTokenVaultGas.test.ts.snap deleted file mode 100644 index a9ec5a85ce..0000000000 --- a/test/integration/__snapshots__/CTokenVaultGas.test.ts.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CTokenVault contract Gas Reporting, cTokens Unwrapped Issue RToken 1`] = `816857`; - -exports[`CTokenVault contract Gas Reporting, cTokens Unwrapped Issue RToken 2`] = `677455`; - -exports[`CTokenVault contract Gas Reporting, cTokens Unwrapped Redeem RToken 1`] = `679421`; - -exports[`CTokenVault contract Gas Reporting, cTokens Unwrapped Transfer 1`] = `159307`; - -exports[`CTokenVault contract Gas Reporting, cTokens Unwrapped Transfer 2`] = `127937`; - -exports[`CTokenVault contract Gas Reporting, cTokens Unwrapped Transfer 3`] = `110849`; - -exports[`CTokenVault contract Gas Reporting, cTokens Wrapped Issue RToken 1`] = `965241`; - -exports[`CTokenVault contract Gas Reporting, cTokens Wrapped Issue RToken 2`] = `753143`; - -exports[`CTokenVault contract Gas Reporting, cTokens Wrapped Redeem RToken 1`] = `748958`; - -exports[`CTokenVault contract Gas Reporting, cTokens Wrapped Transfer 1`] = `310005`; - -exports[`CTokenVault contract Gas Reporting, cTokens Wrapped Transfer 2`] = `193085`; - -exports[`CTokenVault contract Gas Reporting, cTokens Wrapped Transfer 3`] = `175997`; diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index f22f9a1f1e..c9778bb7df 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -1,8 +1,14 @@ import { BigNumber, ContractFactory } from 'ethers' import hre, { ethers } from 'hardhat' import { getChainId } from '../../common/blockchain-utils' -import { IConfig, IImplementations, IRevenueShare, networkConfig } from '../../common/configuration' -import { PAUSER, SHORT_FREEZER, LONG_FREEZER } from '../../common/constants' +import { + IConfig, + IImplementations, + IMonitorParams, + IRevenueShare, + networkConfig, +} from '../../common/configuration' +import { PAUSER, SHORT_FREEZER, LONG_FREEZER, ZERO_ADDRESS } from '../../common/constants' import { expectInReceipt } from '../../common/events' import { advanceTime } from '../utils/time' import { bn, fp } from '../../common/numbers' @@ -54,13 +60,14 @@ import { TestIRToken, TestIStRSR, RecollateralizationLibP1, + FacadeMonitor, } from '../../typechain' import { Collateral, Implementation, IMPLEMENTATION, ORACLE_ERROR, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, REVENUE_HIDING, } from '../fixtures' @@ -190,7 +197,7 @@ export async function collateralFixture( oracleError: ORACLE_ERROR, erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -219,7 +226,7 @@ export async function collateralFixture( oracleError: ORACLE_ERROR, erc20: vault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -247,6 +254,7 @@ export async function collateralFixture( 'stat' + symbol ) ) + const coll = await ATokenCollateralFactory.deploy( { priceTimeout: PRICE_TIMEOUT, @@ -254,7 +262,7 @@ export async function collateralFixture( oracleError: ORACLE_ERROR, erc20: staticErc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -280,13 +288,13 @@ export async function collateralFixture( oracleError: ORACLE_ERROR, erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String(targetName), defaultThreshold, delayUntilDefault, }, targetUnitOracleAddr, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) await coll.refresh() return [erc20, coll] @@ -314,13 +322,13 @@ export async function collateralFixture( oracleError: ORACLE_ERROR, erc20: vault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String(targetName), defaultThreshold, delayUntilDefault, }, targetUnitOracleAddr, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, REVENUE_HIDING ) await coll.refresh() @@ -339,7 +347,7 @@ export async function collateralFixture( oracleError: ORACLE_ERROR, erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String(targetName), defaultThreshold: bn(0), delayUntilDefault, @@ -371,7 +379,7 @@ export async function collateralFixture( oracleError: ORACLE_ERROR, erc20: vault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String(targetName), defaultThreshold: bn(0), delayUntilDefault, @@ -399,13 +407,13 @@ export async function collateralFixture( oracleError: ORACLE_ERROR, erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String(targetName), defaultThreshold, delayUntilDefault, }, targetUnitOracleAddr, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) await coll.refresh() return [erc20, coll] @@ -584,7 +592,7 @@ type RSRAndCompAaveAndCollateralAndModuleFixture = RSRFixture & CollateralFixture & ModuleFixture -interface DefaultFixture extends RSRAndCompAaveAndCollateralAndModuleFixture { +export interface DefaultFixture extends RSRAndCompAaveAndCollateralAndModuleFixture { config: IConfig dist: IRevenueShare deployer: TestIDeployer @@ -603,6 +611,7 @@ interface DefaultFixture extends RSRAndCompAaveAndCollateralAndModuleFixture { facade: FacadeRead facadeAct: FacadeAct facadeTest: FacadeTest + facadeMonitor: FacadeMonitor broker: TestIBroker rsrTrader: TestIRevenueTrader rTokenTrader: TestIRevenueTrader @@ -663,6 +672,11 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = }, } + // Setup Monitor Params based on network + const monitorParams: IMonitorParams = { + AAVE_V2_DATA_PROVIDER_ADDR: networkConfig[chainId].AAVE_DATA_PROVIDER ?? ZERO_ADDRESS, + } + // Deploy FacadeRead const FacadeReadFactory: ContractFactory = await ethers.getContractFactory('FacadeRead') const facade = await FacadeReadFactory.deploy() @@ -675,6 +689,10 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = const FacadeTestFactory: ContractFactory = await ethers.getContractFactory('FacadeTest') const facadeTest = await FacadeTestFactory.deploy() + // Deploy FacadeMonitor - Use implementation to simplify deployments + const FacadeMonitorFactory: ContractFactory = await ethers.getContractFactory('FacadeMonitor') + const facadeMonitor = await FacadeMonitorFactory.deploy(monitorParams) + // Deploy TradingLib external library const TradingLibFactory: ContractFactory = await ethers.getContractFactory( 'RecollateralizationLibP1' @@ -696,7 +714,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = ORACLE_ERROR, rsr.address, config.rTokenMaxTradeVolume, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) await rsrAsset.refresh() @@ -820,7 +838,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = ORACLE_ERROR, aaveToken.address, config.rTokenMaxTradeVolume, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) await aaveAsset.refresh() @@ -834,7 +852,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = ORACLE_ERROR, compToken.address, config.rTokenMaxTradeVolume, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) await compAsset.refresh() @@ -930,6 +948,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = facade, facadeAct, facadeTest, + facadeMonitor, rsrTrader, rTokenTrader, } diff --git a/test/integration/fork-block-numbers.ts b/test/integration/fork-block-numbers.ts index c575f48e3d..f5b5dff068 100644 --- a/test/integration/fork-block-numbers.ts +++ b/test/integration/fork-block-numbers.ts @@ -5,6 +5,7 @@ const forkBlockNumber = { 'mainnet-deployment': 15690042, // Ethereum 'flux-finance': 16836855, // Ethereum 'mainnet-2.0': 17522362, // Ethereum + 'facade-monitor': 18742016, // Ethereum default: 18522901, // Ethereum } diff --git a/test/monitor/FacadeMonitor.test.ts b/test/monitor/FacadeMonitor.test.ts new file mode 100644 index 0000000000..45b7bf1d22 --- /dev/null +++ b/test/monitor/FacadeMonitor.test.ts @@ -0,0 +1,1417 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { BigNumber, Contract } from 'ethers' +import hre, { ethers } from 'hardhat' +import { Collateral, IMPLEMENTATION } from '../fixtures' +import { defaultFixtureNoBasket, DefaultFixture } from '../integration/fixtures' +import { getChainId } from '../../common/blockchain-utils' +import { IConfig, baseL2Chains, networkConfig } from '../../common/configuration' +import { bn, fp, toBNDecimals } from '../../common/numbers' +import { advanceTime } from '../utils/time' +import { whileImpersonating } from '../utils/impersonation' +import { pushOracleForward } from '../utils/oracles' + +import forkBlockNumber from '../integration/fork-block-numbers' +import { + ATokenFiatCollateral, + AaveV3FiatCollateral, + CTokenV3Collateral, + CTokenFiatCollateral, + ERC20Mock, + FacadeTest, + FacadeMonitor, + FiatCollateral, + IAToken, + IComptroller, + IERC20, + ILendingPool, + IPool, + IWETH, + StaticATokenLM, + IAssetRegistry, + TestIBackingManager, + TestIBasketHandler, + TestICToken, + TestIRToken, + USDCMock, + CTokenWrapper, + StaticATokenV3LM, + CusdcV3Wrapper, + CometInterface, + StargateRewardableWrapper, + StargatePoolFiatCollateral, + IStargatePool, + MorphoAaveV2TokenisedDeposit, +} from '../../typechain' +import { useEnv } from '#/utils/env' +import { MAX_UINT256 } from '#/common/constants' + +enum CollPluginType { + AAVE_V2, + AAVE_V3, + COMPOUND_V2, + COMPOUND_V3, + STARGATE, + FLUX, + MORPHO_AAVE_V2, +} + +// Relevant addresses (Mainnet) +const holderDAI = '0x075e72a5eDf65F0A5f44699c7654C1a76941Ddc8' +const holderCDAI = '0x01d127D90513CCB6071F83eFE15611C4d9890668' +const holderADAI = '0x07edE94cF6316F4809f2B725f5d79AD303fB4Dc8' +const holderaUSDCV3 = '0x1eAb3B222A5B57474E0c237E7E1C4312C1066855' +const holderWETH = '0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E' +const holdercUSDCV3 = '0x7f714b13249BeD8fdE2ef3FBDfB18Ed525544B03' +const holdersUSDC = '0xB0D502E938ed5f4df2E681fE6E419ff29631d62b' +const holderfUSDC = '0x86A07dDED024121b282362f4e7A249b00F5dAB37' +const holderUSDC = '0x28C6c06298d514Db089934071355E5743bf21d60' + +let owner: SignerWithAddress + +const describeFork = useEnv('FORK') ? describe : describe.skip + +describeFork(`FacadeMonitor - Integration - Mainnet Forking P${IMPLEMENTATION}`, function () { + let addr1: SignerWithAddress + let addr2: SignerWithAddress + + // Assets + let collateral: Collateral[] + + // Tokens and Assets + let dai: ERC20Mock + let aDai: IAToken + let stataDai: StaticATokenLM + let usdc: USDCMock + let aUsdcV3: IAToken + let sUsdc: IStargatePool + let fUsdc: TestICToken + let weth: IWETH + let cDai: TestICToken + let cDaiVault: CTokenWrapper + let cusdcV3: CometInterface + let daiCollateral: FiatCollateral + let aDaiCollateral: ATokenFiatCollateral + + // Contracts to retrieve after deploy + let rToken: TestIRToken + let facadeTest: FacadeTest + let facadeMonitor: FacadeMonitor + let assetRegistry: IAssetRegistry + let basketHandler: TestIBasketHandler + let backingManager: TestIBackingManager + let config: IConfig + + let initialBal: BigNumber + let basket: Collateral[] + let erc20s: IERC20[] + + let fullLiquidityAmt: BigNumber + let chainId: number + + // Setup test environment + const setup = async (blockNumber: number) => { + // Use Mainnet fork + await hre.network.provider.request({ + method: 'hardhat_reset', + params: [ + { + forking: { + jsonRpcUrl: useEnv('MAINNET_RPC_URL'), + blockNumber: blockNumber, + }, + }, + ], + }) + } + + describe('FacadeMonitor', () => { + before(async () => { + await setup(forkBlockNumber['facade-monitor']) + + chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + }) + + beforeEach(async () => { + ;[owner, addr1, addr2] = await ethers.getSigners() + ;({ + erc20s, + collateral, + basket, + assetRegistry, + basketHandler, + backingManager, + rToken, + facadeTest, + facadeMonitor, + config, + } = await loadFixture(defaultFixtureNoBasket)) + + // Get tokens + dai = erc20s[0] // DAI + cDaiVault = erc20s[6] // cDAI + cDai = await ethers.getContractAt('TestICToken', await cDaiVault.underlying()) // cDAI + stataDai = erc20s[10] // static aDAI + + // Get plain aTokens + aDai = ( + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aDAI || '' + ) + ) + + // Get collaterals + daiCollateral = collateral[0] // DAI + aDaiCollateral = collateral[10] // aDAI + + // Get assets and tokens for default basket + daiCollateral = basket[0] + aDaiCollateral = basket[1] + + dai = await ethers.getContractAt('ERC20Mock', await daiCollateral.erc20()) + stataDai = ( + await ethers.getContractAt('StaticATokenLM', await aDaiCollateral.erc20()) + ) + + // Get plain aToken + aDai = ( + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aDAI || '' + ) + ) + + usdc = ( + await ethers.getContractAt('USDCMock', networkConfig[chainId].tokens.USDC || '') + ) + aUsdcV3 = await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', // use V2 interface, it includes ERC20 + networkConfig[chainId].tokens.aEthUSDC || '' + ) + + cusdcV3 = ( + await ethers.getContractAt('CometInterface', networkConfig[chainId].tokens.cUSDCv3 || '') + ) + + sUsdc = ( + await ethers.getContractAt('IStargatePool', networkConfig[chainId].tokens.sUSDC || '') + ) + + fUsdc = ( + await ethers.getContractAt('TestICToken', networkConfig[chainId].tokens.fUSDC || '') + ) + + initialBal = bn('2500000e18') + + // Fund user with static aDAI + await whileImpersonating(holderADAI, async (adaiSigner) => { + // Wrap ADAI into static ADAI + await aDai.connect(adaiSigner).transfer(addr1.address, initialBal) + await aDai.connect(addr1).approve(stataDai.address, initialBal) + await stataDai.connect(addr1).deposit(addr1.address, initialBal, 0, false) + }) + + // Fund user with aUSDCV3 + await whileImpersonating(holderaUSDCV3, async (ausdcV3Signer) => { + await aUsdcV3.connect(ausdcV3Signer).transfer(addr1.address, toBNDecimals(initialBal, 6)) + }) + + // Fund user with DAI + await whileImpersonating(holderDAI, async (daiSigner) => { + await dai.connect(daiSigner).transfer(addr1.address, initialBal.mul(8)) + }) + + // Fund user with cDAI + await whileImpersonating(holderCDAI, async (cdaiSigner) => { + await cDai.connect(cdaiSigner).transfer(addr1.address, toBNDecimals(initialBal, 8).mul(100)) + await cDai.connect(addr1).approve(cDaiVault.address, toBNDecimals(initialBal, 8).mul(100)) + await cDaiVault.connect(addr1).deposit(toBNDecimals(initialBal, 8).mul(100), addr1.address) + }) + + // Fund user with cUSDCV3 + await whileImpersonating(holdercUSDCV3, async (cusdcV3Signer) => { + await cusdcV3.connect(cusdcV3Signer).transfer(addr1.address, toBNDecimals(initialBal, 6)) + }) + + // Fund user with sUSDC + await whileImpersonating(holdersUSDC, async (susdcSigner) => { + await sUsdc.connect(susdcSigner).transfer(addr1.address, toBNDecimals(initialBal, 6)) + }) + + // Fund user with fUSDC + await whileImpersonating(holderfUSDC, async (fusdcSigner) => { + await fUsdc + .connect(fusdcSigner) + .transfer(addr1.address, toBNDecimals(initialBal, 8).mul(100)) + }) + + // Fund user with USDC + await whileImpersonating(holderUSDC, async (usdcSigner) => { + await usdc.connect(usdcSigner).transfer(addr1.address, toBNDecimals(initialBal, 6)) + }) + + // Fund user with WETH + weth = await ethers.getContractAt('IWETH', networkConfig[chainId].tokens.WETH || '') + await whileImpersonating(holderWETH, async (signer) => { + await weth.connect(signer).transfer(addr1.address, fp('500000')) + }) + }) + + describe('AAVE V2', () => { + const issueAmount: BigNumber = bn('1000000e18') + let lendingPool: ILendingPool + let aaveV2DataProvider: Contract + + beforeEach(async () => { + // Setup basket + await basketHandler.connect(owner).setPrimeBasket([stataDai.address], [fp('1')]) + await basketHandler.connect(owner).refreshBasket() + await advanceTime(Number(config.warmupPeriod) + 1) + + // Provide approvals + await stataDai.connect(addr1).approve(rToken.address, issueAmount) + + // Advance time significantly - Recharge throttle + await advanceTime(100000) + + // Issue rTokens + await rToken.connect(addr1).issue(issueAmount) + + lendingPool = ( + await ethers.getContractAt('ILendingPool', networkConfig[chainId].AAVE_LENDING_POOL || '') + ) + + const aaveV2DataProviderAbi = [ + 'function getReserveData(address asset) external view returns (uint256 availableLiquidity,uint256 totalStableDebt,uint256 totalVariableDebt,uint256 liquidityRate,uint256 variableBorrowRate,uint256 stableBorrowRate,uint256 averageStableBorrowRate,uint256 liquidityIndex,uint256 variableBorrowIndex,uint40 lastUpdateTimestamp)', + ] + aaveV2DataProvider = await ethers.getContractAt( + aaveV2DataProviderAbi, + networkConfig[chainId].AAVE_DATA_PROVIDER || '' + ) + + // Get current liquidity + ;[fullLiquidityAmt, , , , , , , , ,] = await aaveV2DataProvider + .connect(addr1) + .getReserveData(dai.address) + + // Provide liquidity in AAVE V2 to be able to borrow + const amountToDeposit = fp('500000') + await weth.connect(addr1).approve(lendingPool.address, amountToDeposit) + await lendingPool.connect(addr1).deposit(weth.address, amountToDeposit, addr1.address, 0) + }) + + it('Should return 100% when full liquidity available', async function () { + // Check asset value + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( + issueAmount, + fp('150') + ) + + // AAVE V2 - All redeemable + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V2, + stataDai.address + ) + ).to.equal(fp('1')) + + // Confirm all can be redeemed + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await stataDai.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await stataDai.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await stataDai.connect(addr2).withdraw(addr2.address, bmBalanceAmt, false) + await expect(lendingPool.connect(addr2).withdraw(dai.address, MAX_UINT256, addr2.address)) + .to.not.be.reverted + expect(await dai.balanceOf(addr2.address)).to.be.gt(bn(0)) + expect(await aDai.balanceOf(addr2.address)).to.equal(bn(0)) + }) + + it('Should return backing redeemable percent correctly', async function () { + // AAVE V2 - All redeemable + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V2, + stataDai.address + ) + ).to.equal(fp('1')) + + // Leave only 80% of backing available to be redeemed + const borrowAmount = fullLiquidityAmt.sub(issueAmount.mul(80).div(100)) + await lendingPool.connect(addr1).borrow(dai.address, borrowAmount, 2, 0, addr1.address) + + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V2, + stataDai.address + ) + ).to.be.closeTo(fp('0.80'), fp('0.01')) + + // Borrow half of the remaining liquidity + const remainingLiquidity = fullLiquidityAmt.sub(borrowAmount) + await lendingPool + .connect(addr1) + .borrow(dai.address, remainingLiquidity.div(2), 2, 0, addr1.address) + + // Now only 40% is available to be redeemed + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V2, + stataDai.address + ) + ).to.be.closeTo(fp('0.40'), fp('0.01')) + + // Confirm we cannot redeem full balance + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await stataDai.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await stataDai.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await stataDai.connect(addr2).withdraw(addr2.address, bmBalanceAmt, false) + await expect(lendingPool.connect(addr2).withdraw(dai.address, MAX_UINT256, addr2.address)) + .to.be.reverted + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + + // But we can redeem if we reduce the amount to 30% + await expect( + lendingPool + .connect(addr2) + .withdraw( + dai.address, + (await aDai.balanceOf(addr2.address)).mul(30).div(100), + addr2.address + ) + ).to.not.be.reverted + expect(await dai.balanceOf(addr2.address)).to.be.gt(0) + }) + + it('Should handle no liquidity', async function () { + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V2, + stataDai.address + ) + ).to.equal(fp('1')) + + // Borrow full liquidity + await lendingPool.connect(addr1).borrow(dai.address, fullLiquidityAmt, 2, 0, addr1.address) + + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V2, + stataDai.address + ) + ).to.be.closeTo(fp('0'), fp('0.01')) + + // Confirm we cannot redeem anything, not even 1% + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await stataDai.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await stataDai.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await stataDai.connect(addr2).withdraw(addr2.address, bmBalanceAmt, false) + await expect( + lendingPool + .connect(addr2) + .withdraw(dai.address, (await aDai.balanceOf(addr2.address)).div(100), addr2.address) + ).to.be.reverted + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + }) + }) + + describe('AAVE V3', () => { + const issueAmount: BigNumber = bn('1000000e18') + let stataUsdcV3: StaticATokenV3LM + let pool: IPool + + beforeEach(async () => { + const StaticATokenFactory = await hre.ethers.getContractFactory('StaticATokenV3LM') + stataUsdcV3 = await StaticATokenFactory.deploy( + networkConfig[chainId].AAVE_V3_POOL!, + networkConfig[chainId].AAVE_V3_INCENTIVES_CONTROLLER! + ) + + await stataUsdcV3.deployed() + await ( + await stataUsdcV3.initialize( + networkConfig[chainId].tokens.aEthUSDC!, + 'Static Aave Ethereum USDC', + 'saEthUSDC' + ) + ).wait() + + /******** Deploy Aave V3 USDC collateral plugin **************************/ + const usdcOracleTimeout = '86400' // 24 hr + const usdcOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% + + const MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') + const chainlinkFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const CollateralFactory = await ethers.getContractFactory('AaveV3FiatCollateral') + const collateral = await CollateralFactory.connect(owner).deploy( + { + priceTimeout: bn('604800'), + chainlinkFeed: chainlinkFeed.address, + oracleError: usdcOracleError, + erc20: stataUsdcV3.address, + maxTradeVolume: fp('1e6'), + oracleTimeout: usdcOracleTimeout, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01').add(usdcOracleError), + delayUntilDefault: bn('86400'), + }, + fp('1e-6') + ) + + // Register and update collateral + await collateral.deployed() + await (await collateral.refresh()).wait() + await pushOracleForward(chainlinkFeed.address) + await assetRegistry.connect(owner).register(collateral.address) + + // Wrap aUsdcV3 + await aUsdcV3.connect(addr1).approve(stataUsdcV3.address, toBNDecimals(initialBal, 6)) + await stataUsdcV3 + .connect(addr1) + ['deposit(uint256,address,uint16,bool)']( + toBNDecimals(initialBal, 6), + addr1.address, + 0, + false + ) + + // Get current liquidity + fullLiquidityAmt = await usdc.balanceOf(aUsdcV3.address) + + // Setup basket + await pushOracleForward(chainlinkFeed.address) + await basketHandler.connect(owner).setPrimeBasket([stataUsdcV3.address], [fp('1')]) + await basketHandler.connect(owner).refreshBasket() + await advanceTime(Number(config.warmupPeriod) + 1) + + // Provide approvals + await stataUsdcV3.connect(addr1).approve(rToken.address, issueAmount) + + // Advance time significantly - Recharge throttle + await advanceTime(100000) + await pushOracleForward(chainlinkFeed.address) + + // Issue rTokens + await rToken.connect(addr1).issue(issueAmount) + + pool = await ethers.getContractAt('IPool', networkConfig[chainId].AAVE_V3_POOL || '') + + // Provide liquidity to be able to borrow + const amountToDeposit = fp('500000') + await weth.connect(addr1).approve(pool.address, amountToDeposit) + await pool.connect(addr1).supply(weth.address, amountToDeposit, addr1.address, 0) + }) + + it('Should return 100% when full liquidity available', async function () { + // Check asset value + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( + issueAmount, + fp('150') + ) + + // AAVE V3 - All redeemable + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V3, + stataUsdcV3.address + ) + ).to.equal(fp('1')) + + // Confirm all can be redeemed + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await stataUsdcV3.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await stataUsdcV3.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await stataUsdcV3 + .connect(addr2) + ['redeem(uint256,address,address,bool)']( + bmBalanceAmt, + addr2.address, + addr2.address, + false + ) + await expect(pool.connect(addr2).withdraw(usdc.address, MAX_UINT256, addr2.address)).to.not + .be.reverted + expect(await usdc.balanceOf(addr2.address)).to.be.gt(bn(0)) + expect(await aUsdcV3.balanceOf(addr2.address)).to.equal(bn(0)) + }) + + it('Should return backing redeemable percent correctly', async function () { + // AAVE V3 - All redeemable + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V3, + stataUsdcV3.address + ) + ).to.equal(fp('1')) + + // Leave only 80% of backing to be able to be redeemed + const borrowAmount = fullLiquidityAmt.sub(toBNDecimals(issueAmount, 6).mul(80).div(100)) + await pool.connect(addr1).borrow(usdc.address, borrowAmount, 2, 0, addr1.address) + + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V3, + stataUsdcV3.address + ) + ).to.be.closeTo(fp('0.80'), fp('0.01')) + + // Borrow half of the remaining liquidity + const remainingLiquidity = fullLiquidityAmt.sub(borrowAmount) + await pool + .connect(addr1) + .borrow(usdc.address, remainingLiquidity.div(2), 2, 0, addr1.address) + + // Only 40% available to be redeemed + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V3, + stataUsdcV3.address + ) + ).to.be.closeTo(fp('0.40'), fp('0.01')) + + // Confirm we cannot redeem full balance + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await stataUsdcV3.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await stataUsdcV3.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await stataUsdcV3 + .connect(addr2) + ['redeem(uint256,address,address,bool)']( + bmBalanceAmt, + addr2.address, + addr2.address, + false + ) + await expect(pool.connect(addr2).withdraw(usdc.address, MAX_UINT256, addr2.address)).to.be + .reverted + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + + // We can redeem if we reduce to 30% + await expect( + pool + .connect(addr2) + .withdraw( + usdc.address, + (await aUsdcV3.balanceOf(addr2.address)).mul(30).div(100), + addr2.address + ) + ).to.not.be.reverted + expect(await usdc.balanceOf(addr2.address)).to.be.gt(0) + }) + + it('Should handle no liquidity', async function () { + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V3, + stataUsdcV3.address + ) + ).to.equal(fp('1')) + + // Borrow full liquidity + await pool.connect(addr1).borrow(usdc.address, fullLiquidityAmt, 2, 0, addr1.address) + + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.AAVE_V3, + stataUsdcV3.address + ) + ).to.be.closeTo(fp('0'), fp('0.01')) + + // Confirm we cannot redeem anything, not even 1% + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await stataUsdcV3.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await stataUsdcV3.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await stataUsdcV3 + .connect(addr2) + ['redeem(uint256,address,address,bool)']( + bmBalanceAmt, + addr2.address, + addr2.address, + false + ) + await expect( + pool + .connect(addr2) + .withdraw( + usdc.address, + (await aUsdcV3.balanceOf(addr2.address)).div(100), + addr2.address + ) + ).to.be.reverted + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + }) + }) + + describe('Compound V2', () => { + const issueAmount: BigNumber = bn('1000000e18') + let comptroller: IComptroller + + beforeEach(async () => { + // Setup basket + await basketHandler.connect(owner).setPrimeBasket([cDaiVault.address], [fp('1')]) + await basketHandler.connect(owner).refreshBasket() + await advanceTime(Number(config.warmupPeriod) + 1) + + // Provide approvals + await cDaiVault + .connect(addr1) + .approve(rToken.address, toBNDecimals(issueAmount, 8).mul(100)) + + // Advance time significantly - Recharge throttle + await advanceTime(100000) + + // Issue rTokens + await rToken.connect(addr1).issue(issueAmount) + + // Get current liquidity + fullLiquidityAmt = await dai.balanceOf(cDai.address) + + // Compound Comptroller + comptroller = await ethers.getContractAt( + 'ComptrollerMock', + networkConfig[chainId].COMPTROLLER || '' + ) + + // Deposit ETH to be able to borrow + const cEtherAbi = [ + 'function mint(uint256 mintAmount) external payable returns (uint256)', + 'function balanceOf(address owner) external view returns (uint256 balance)', + ] + const cEth = await ethers.getContractAt(cEtherAbi, networkConfig[chainId].tokens.cETH || '') + await comptroller.connect(addr1).enterMarkets([cEth.address]) + const amountToDeposit = fp('500000') + await weth.connect(addr1).withdraw(amountToDeposit) + await cEth.connect(addr1).mint(amountToDeposit, { value: amountToDeposit }) + }) + + it('Should return 100% when full liquidity available', async function () { + // Check asset value + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( + issueAmount, + fp('150') + ) + + // COMPOUND V2 - All redeemable + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V2, + cDaiVault.address + ) + ).to.equal(fp('1')) + + // Confirm all can be redeemed + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await cDaiVault.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await cDaiVault.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await cDaiVault.connect(addr2).withdraw(bmBalanceAmt, addr2.address) + expect(await cDai.balanceOf(addr2.address)).to.equal(bmBalanceAmt) + + await expect(cDai.connect(addr2).redeem(bmBalanceAmt)).to.not.be.reverted + expect(await dai.balanceOf(addr2.address)).to.be.gt(bn(0)) + expect(await cDai.balanceOf(addr2.address)).to.equal(bn(0)) + }) + + it('Should return backing redeemable percent correctly', async function () { + // COMPOUND V2 - All redeemable + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V2, + cDaiVault.address + ) + ).to.equal(fp('1')) + + // Leave only 80% of backing to be able to be redeemed + const borrowAmount = fullLiquidityAmt.sub(issueAmount.mul(80).div(100)) + await cDai.connect(addr1).borrow(borrowAmount) + + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V2, + cDaiVault.address + ) + ).to.be.closeTo(fp('0.80'), fp('0.01')) + + // Borrow half of the remaining liquidity + const remainingLiquidity = fullLiquidityAmt.sub(borrowAmount) + await cDai.connect(addr1).borrow(bn(remainingLiquidity.div(2))) + + // Now only 40% of backing can be redeemed + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V2, + cDaiVault.address + ) + ).to.be.closeTo(fp('0.40'), fp('0.01')) + + // Confirm we cannot redeem full balance + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await cDaiVault.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await cDaiVault.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await cDaiVault.connect(addr2).withdraw(bmBalanceAmt, addr2.address) + await expect(cDai.connect(addr2).redeem(bmBalanceAmt)).to.be.reverted + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + + // We can redeem iff we reduce to 30% + await expect(cDai.connect(addr2).redeem(bmBalanceAmt.mul(30).div(100))).to.not.be.reverted + expect(await dai.balanceOf(addr2.address)).to.be.gt(0) + }) + + it('Should handle no liquidity', async function () { + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V2, + cDaiVault.address + ) + ).to.equal(fp('1')) + + // Borrow full liquidity + await cDai.connect(addr1).borrow(fullLiquidityAmt) + + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V2, + cDaiVault.address + ) + ).to.be.closeTo(fp('0'), fp('0.01')) + + // Confirm we cannot redeem anything, not even 1% + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await cDaiVault.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await cDaiVault.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await cDaiVault.connect(addr2).withdraw(bmBalanceAmt, addr2.address) + expect(await cDai.balanceOf(addr2.address)).to.equal(bmBalanceAmt) + + await expect(cDai.connect(addr2).redeem((await cDai.balanceOf(addr2.address)).div(100))).to + .be.reverted + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + }) + }) + + describe('Compound V3', () => { + const issueAmount: BigNumber = bn('1000000e18') + let wcusdcV3: CusdcV3Wrapper + + beforeEach(async () => { + const CUsdcV3WrapperFactory = await hre.ethers.getContractFactory('CusdcV3Wrapper') + + wcusdcV3 = ( + await CUsdcV3WrapperFactory.deploy( + cusdcV3.address, + networkConfig[chainId].COMET_REWARDS || '', + networkConfig[chainId].tokens.COMP || '' + ) + ) + await wcusdcV3.deployed() + + /******** Deploy Compound V3 USDC collateral plugin **************************/ + const CollateralFactory = await ethers.getContractFactory('CTokenV3Collateral') + + const usdcOracleTimeout = '86400' // 24 hr + const usdcOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% + + const MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') + const chainlinkFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const collateral = await CollateralFactory.connect(owner).deploy( + { + priceTimeout: bn('604800'), + chainlinkFeed: chainlinkFeed.address, + oracleError: usdcOracleError.toString(), + erc20: wcusdcV3.address, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: usdcOracleTimeout, // 24h hr, + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01').add(usdcOracleError).toString(), + delayUntilDefault: bn('86400').toString(), // 24h + }, + fp('1e-6'), + bn('10000e6').toString() // $10k + ) + + // Register and update collateral + await collateral.deployed() + await (await collateral.refresh()).wait() + await pushOracleForward(chainlinkFeed.address) + await assetRegistry.connect(owner).register(collateral.address) + + // Wrap cUSDCV3 + await cusdcV3.connect(addr1).allow(wcusdcV3.address, true) + await wcusdcV3.connect(addr1).deposit(toBNDecimals(initialBal, 6)) + + // Get current liquidity + fullLiquidityAmt = await usdc.balanceOf(cusdcV3.address) + + // Setup basket + await pushOracleForward(chainlinkFeed.address) + await basketHandler.connect(owner).setPrimeBasket([wcusdcV3.address], [fp('1')]) + await basketHandler.connect(owner).refreshBasket() + await advanceTime(Number(config.warmupPeriod) + 1) + + // Provide approvals + await wcusdcV3.connect(addr1).approve(rToken.address, MAX_UINT256) + + // Advance time significantly - Recharge throttle + await advanceTime(100000) + await pushOracleForward(chainlinkFeed.address) + + // Issue rTokens + await rToken.connect(addr1).issue(issueAmount) + + // Provide liquidity to be able to borrow + const amountToDeposit = fp('500000') + await weth.connect(addr1).approve(cusdcV3.address, amountToDeposit) + await cusdcV3.connect(addr1).supply(weth.address, amountToDeposit.div(2)) + }) + + it('Should return 100% when full liquidity available', async function () { + // Check asset value + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( + issueAmount, + fp('150') + ) + + // Compound V3 - All redeemable + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V3, + wcusdcV3.address + ) + ).to.equal(fp('1')) + + // Confirm all can be redeemed + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await wcusdcV3.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await wcusdcV3.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await wcusdcV3.connect(addr2).withdraw(MAX_UINT256) + + await expect(cusdcV3.connect(addr2).withdraw(usdc.address, MAX_UINT256)).to.not.be.reverted + expect(await usdc.balanceOf(addr2.address)).to.be.gt(bn(0)) + expect(await cusdcV3.balanceOf(addr2.address)).to.equal(bn(0)) + }) + + it('Should return backing redeemable percent correctly', async function () { + // AAVE V3 - All redeemable + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V3, + wcusdcV3.address + ) + ).to.equal(fp('1')) + + // Leave only 80% of backing to be able to be redeemed + const borrowAmount = fullLiquidityAmt.sub(toBNDecimals(issueAmount, 6).mul(80).div(100)) + await cusdcV3.connect(addr1).withdraw(usdc.address, borrowAmount) + + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V3, + wcusdcV3.address + ) + ).to.be.closeTo(fp('0.80'), fp('0.01')) + + // Borrow half of the remaining liquidity + const remainingLiquidity = fullLiquidityAmt.sub(borrowAmount) + await cusdcV3.connect(addr1).withdraw(usdc.address, remainingLiquidity.div(2)) + + // Only 40% available to be redeemed + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V3, + wcusdcV3.address + ) + ).to.be.closeTo(fp('0.40'), fp('0.01')) + + // Confirm we cannot redeem full balance + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await wcusdcV3.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await wcusdcV3.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await wcusdcV3.connect(addr2).withdraw(MAX_UINT256) + + await expect(cusdcV3.connect(addr2).withdraw(usdc.address, MAX_UINT256)).to.be.reverted + expect(await dai.balanceOf(addr2.address)).to.equal(bn(0)) + + // We can redeem if we reduce to 30% + await expect( + cusdcV3 + .connect(addr2) + .withdraw(usdc.address, (await cusdcV3.balanceOf(addr2.address)).mul(30).div(100)) + ).to.not.be.reverted + expect(await usdc.balanceOf(addr2.address)).to.be.gt(0) + }) + + it('Should handle no liquidity', async function () { + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V3, + wcusdcV3.address + ) + ).to.equal(fp('1')) + + // Borrow full liquidity + await cusdcV3.connect(addr1).withdraw(usdc.address, fullLiquidityAmt) + + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.COMPOUND_V3, + wcusdcV3.address + ) + ).to.be.closeTo(fp('0'), fp('0.01')) + + // Confirm we cannot redeem anything, not even 1% + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await wcusdcV3.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await wcusdcV3.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await wcusdcV3.connect(addr2).withdraw(MAX_UINT256) + + await expect( + cusdcV3 + .connect(addr2) + .withdraw(usdc.address, (await cusdcV3.balanceOf(addr2.address)).div(100)) + ).to.be.reverted + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + }) + }) + + describe('Stargate', () => { + const issueAmount: BigNumber = bn('1000000e18') + let wstgUsdc: StargateRewardableWrapper + + beforeEach(async () => { + const SthWrapperFactory = await hre.ethers.getContractFactory('StargateRewardableWrapper') + + wstgUsdc = await SthWrapperFactory.deploy( + 'Wrapped Stargate USDC', + 'wsgUSDC', + networkConfig[chainId].tokens.STG!, + networkConfig[chainId].STARGATE_STAKING_CONTRACT!, + networkConfig[chainId].tokens.sUSDC! + ) + await wstgUsdc.deployed() + + /******** Deploy Stargate USDC collateral plugin **************************/ + const usdcOracleTimeout = '86400' // 24 hr + const usdcOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% + + const MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') + const chainlinkFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const CollateralFactory = await hre.ethers.getContractFactory('StargatePoolFiatCollateral') + const collateral = await CollateralFactory.connect( + owner + ).deploy( + { + priceTimeout: bn('604800'), + chainlinkFeed: chainlinkFeed.address, + oracleError: usdcOracleError, + erc20: wstgUsdc.address, + maxTradeVolume: fp('1e6'), + oracleTimeout: usdcOracleTimeout, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01').add(usdcOracleError), + delayUntilDefault: bn('86400'), + }, + fp('1e-6') + ) + + // Register and update collateral + await collateral.deployed() + await (await collateral.refresh()).wait() + await pushOracleForward(chainlinkFeed.address) + await assetRegistry.connect(owner).register(collateral.address) + + // Wrap sUsdc + await sUsdc.connect(addr1).approve(wstgUsdc.address, toBNDecimals(initialBal, 6)) + await wstgUsdc.connect(addr1).deposit(toBNDecimals(initialBal, 6), addr1.address) + + // Get current liquidity + fullLiquidityAmt = await sUsdc.totalLiquidity() + + // Setup basket + await pushOracleForward(chainlinkFeed.address) + await basketHandler.connect(owner).setPrimeBasket([wstgUsdc.address], [fp('1')]) + await basketHandler.connect(owner).refreshBasket() + await advanceTime(Number(config.warmupPeriod) + 1) + + // Provide approvals + await wstgUsdc.connect(addr1).approve(rToken.address, issueAmount) + + // Advance time significantly - Recharge throttle + await advanceTime(100000) + await pushOracleForward(chainlinkFeed.address) + + // Issue rTokens + await rToken.connect(addr1).issue(issueAmount) + }) + + it('Should return 100%, full liquidity available at all times', async function () { + // Check asset value + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( + issueAmount, + fp('150') + ) + + // AAVE V3 - All redeemable + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.STARGATE, + wstgUsdc.address + ) + ).to.equal(fp('1')) + }) + }) + + describe('Flux', () => { + const issueAmount: BigNumber = bn('1000000e18') + + beforeEach(async () => { + /******** Deploy Flux USDC collateral plugin **************************/ + const CollateralFactory = await ethers.getContractFactory('CTokenFiatCollateral') + + const usdcOracleTimeout = '86400' // 24 hr + const usdcOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% + + const MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') + const chainlinkFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const collateral = await CollateralFactory.connect(owner).deploy( + { + priceTimeout: bn('604800'), + chainlinkFeed: chainlinkFeed.address, + oracleError: usdcOracleError.toString(), + erc20: fUsdc.address, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: usdcOracleTimeout, // 24h hr, + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01').add(usdcOracleError).toString(), + delayUntilDefault: bn('86400').toString(), // 24h + }, + fp('1e-6') + ) + + // Register and update collateral + await collateral.deployed() + await (await collateral.refresh()).wait() + await pushOracleForward(chainlinkFeed.address) + await assetRegistry.connect(owner).register(collateral.address) + + // Get current liquidity + fullLiquidityAmt = await usdc.balanceOf(fUsdc.address) + + // Setup basket + await pushOracleForward(chainlinkFeed.address) + await basketHandler.connect(owner).setPrimeBasket([fUsdc.address], [fp('1')]) + await basketHandler.connect(owner).refreshBasket() + await advanceTime(Number(config.warmupPeriod) + 1) + + // Provide approvals + await fUsdc.connect(addr1).approve(rToken.address, toBNDecimals(issueAmount, 8).mul(100)) + + // Advance time significantly - Recharge throttle + await advanceTime(100000) + await pushOracleForward(chainlinkFeed.address) + + // Issue rTokens + await rToken.connect(addr1).issue(issueAmount) + }) + + it('Should return 100% when full liquidity available', async function () { + // Check asset value + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( + issueAmount, + fp('150') + ) + + // FLUX - All redeemable + expect( + await facadeMonitor.backingReedemable(rToken.address, CollPluginType.FLUX, fUsdc.address) + ).to.equal(fp('1')) + + // Confirm all can be redeemed + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await fUsdc.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await fUsdc.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + await expect(fUsdc.connect(addr2).redeem(bmBalanceAmt)).to.not.be.reverted + expect(await usdc.balanceOf(addr2.address)).to.be.gt(bn(0)) + expect(await fUsdc.balanceOf(addr2.address)).to.equal(bn(0)) + }) + }) + + describe('MORPHO - AAVE V2', () => { + const issueAmount: BigNumber = bn('1000000e18') + let lendingPool: ILendingPool + let maUSDC: MorphoAaveV2TokenisedDeposit + let aaveV2DataProvider: Contract + + beforeEach(async () => { + /******** Deploy Morpho AAVE V2 USDC collateral plugin **************************/ + const MorphoTokenisedDepositFactory = await ethers.getContractFactory( + 'MorphoAaveV2TokenisedDeposit' + ) + maUSDC = await MorphoTokenisedDepositFactory.deploy({ + morphoController: networkConfig[chainId].MORPHO_AAVE_CONTROLLER!, + morphoLens: networkConfig[chainId].MORPHO_AAVE_LENS!, + rewardsDistributor: networkConfig[chainId].MORPHO_REWARDS_DISTRIBUTOR!, + underlyingERC20: networkConfig[chainId].tokens.USDC!, + poolToken: networkConfig[chainId].tokens.aUSDC!, + rewardToken: networkConfig[chainId].tokens.MORPHO!, + }) + + const CollateralFactory = await hre.ethers.getContractFactory('MorphoFiatCollateral') + + const usdcOracleTimeout = '86400' // 24 hr + const usdcOracleError = baseL2Chains.includes(hre.network.name) ? fp('0.003') : fp('0.0025') // 0.3% (Base) or 0.25% + const baseStableConfig = { + priceTimeout: bn('604800').toString(), + oracleError: usdcOracleError.toString(), + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: usdcOracleTimeout, // 24h + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: usdcOracleError.add(fp('0.01')), // 1.25% + delayUntilDefault: bn('86400').toString(), // 24h + } + const MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') + const chainlinkFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const collateral = await CollateralFactory.connect(owner).deploy( + { + ...baseStableConfig, + chainlinkFeed: chainlinkFeed.address, + erc20: maUSDC.address, + }, + fp('1e-6') + ) + + // Register and update collateral + await collateral.deployed() + await (await collateral.refresh()).wait() + await pushOracleForward(chainlinkFeed.address) + await assetRegistry.connect(owner).register(collateral.address) + + const aaveV2DataProviderAbi = [ + 'function getReserveData(address asset) external view returns (uint256 availableLiquidity,uint256 totalStableDebt,uint256 totalVariableDebt,uint256 liquidityRate,uint256 variableBorrowRate,uint256 stableBorrowRate,uint256 averageStableBorrowRate,uint256 liquidityIndex,uint256 variableBorrowIndex,uint40 lastUpdateTimestamp)', + ] + aaveV2DataProvider = await ethers.getContractAt( + aaveV2DataProviderAbi, + networkConfig[chainId].AAVE_DATA_PROVIDER || '' + ) + + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.MORPHO_AAVE_V2, + maUSDC.address + ) + + // Wrap maUSDC + await usdc.connect(addr1).approve(maUSDC.address, 0) + await usdc.connect(addr1).approve(maUSDC.address, MAX_UINT256) + await maUSDC.connect(addr1).mint(toBNDecimals(initialBal, 15), addr1.address) + + // Setup basket + await pushOracleForward(chainlinkFeed.address) + await basketHandler.connect(owner).setPrimeBasket([maUSDC.address], [fp('1')]) + await basketHandler.connect(owner).refreshBasket() + await advanceTime(Number(config.warmupPeriod) + 1) + + // Provide approvals + await maUSDC.connect(addr1).approve(rToken.address, toBNDecimals(issueAmount, 15)) + + // Advance time significantly - Recharge throttle + await advanceTime(100000) + await pushOracleForward(chainlinkFeed.address) + + // Issue rTokens + await rToken.connect(addr1).issue(issueAmount) + + lendingPool = ( + await ethers.getContractAt('ILendingPool', networkConfig[chainId].AAVE_LENDING_POOL || '') + ) + + // Provide liquidity in AAVE V2 to be able to borrow + const amountToDeposit = fp('500000') + await weth.connect(addr1).approve(lendingPool.address, amountToDeposit) + await lendingPool.connect(addr1).deposit(weth.address, amountToDeposit, addr1.address, 0) + }) + + it('Should return 100% when full liquidity available', async function () { + // Check asset value + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( + issueAmount, + fp('150') + ) + + // MORPHO AAVE V2 - All redeemable + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.MORPHO_AAVE_V2, + maUSDC.address + ) + ).to.equal(fp('1')) + + // Confirm all can be redeemed + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await maUSDC.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await maUSDC.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + const maxWithdraw = await maUSDC.maxWithdraw(addr2.address) + await expect(maUSDC.connect(addr2).withdraw(maxWithdraw, addr2.address, addr2.address)).to + .not.be.reverted + expect(await usdc.balanceOf(addr2.address)).to.be.gt(bn(0)) + }) + + it('Should return backing redeemable percent correctly', async function () { + // MORPHO AAVE V2 - All redeemable + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.MORPHO_AAVE_V2, + maUSDC.address + ) + ).to.equal(fp('1')) + + // Get current liquidity from Aave V2 (Morpho relies on this) + ;[fullLiquidityAmt, , , , , , , , ,] = await aaveV2DataProvider + .connect(addr1) + .getReserveData(usdc.address) + + // Leave only 80% of backing available to be redeemed + const borrowAmount = fullLiquidityAmt.sub(toBNDecimals(issueAmount, 6).mul(80).div(100)) + await lendingPool.connect(addr1).borrow(usdc.address, borrowAmount, 2, 0, addr1.address) + + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.MORPHO_AAVE_V2, + maUSDC.address + ) + ).to.be.closeTo(fp('0.80'), fp('0.01')) + + // Borrow half of the remaining liquidity + const remainingLiquidity = fullLiquidityAmt.sub(borrowAmount) + await lendingPool + .connect(addr1) + .borrow(usdc.address, remainingLiquidity.div(2), 2, 0, addr1.address) + + // Now only 40% is available to be redeemed + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.MORPHO_AAVE_V2, + maUSDC.address + ) + ).to.be.closeTo(fp('0.40'), fp('0.01')) + + // Confirm we cannot redeem full balance + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await maUSDC.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await maUSDC.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + const maxWithdraw = await maUSDC.maxWithdraw(addr2.address) + await expect(maUSDC.connect(addr2).withdraw(maxWithdraw, addr2.address, addr2.address)).to + .be.reverted + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + + // But we can redeem if we reduce the amount to 30% + await expect( + maUSDC.connect(addr2).withdraw(maxWithdraw.mul(30).div(100), addr2.address, addr2.address) + ).to.not.be.reverted + expect(await usdc.balanceOf(addr2.address)).to.be.gt(0) + }) + + it('Should handle no liquidity', async function () { + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.MORPHO_AAVE_V2, + maUSDC.address + ) + ).to.equal(fp('1')) + + // Get current liquidity from Aave V2 (Morpho relies on this) + ;[fullLiquidityAmt, , , , , , , , ,] = await aaveV2DataProvider + .connect(addr1) + .getReserveData(usdc.address) + + // Borrow full liquidity + await lendingPool.connect(addr1).borrow(usdc.address, fullLiquidityAmt, 2, 0, addr1.address) + + expect( + await facadeMonitor.backingReedemable( + rToken.address, + CollPluginType.MORPHO_AAVE_V2, + maUSDC.address + ) + ).to.be.closeTo(fp('0'), fp('0.01')) + + // Confirm we cannot redeem anything, not even 1% + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + const bmBalanceAmt = await maUSDC.balanceOf(backingManager.address) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await maUSDC.connect(bmSigner).transfer(addr2.address, bmBalanceAmt) + }) + const maxWithdraw = await maUSDC.maxWithdraw(addr2.address) + await expect( + maUSDC.connect(addr2).withdraw(maxWithdraw.div(100), addr2.address, addr2.address) + ).to.be.reverted + expect(await usdc.balanceOf(addr2.address)).to.equal(bn(0)) + }) + }) + }) +}) diff --git a/test/plugins/Asset.test.ts b/test/plugins/Asset.test.ts index 0a534ae9cc..5d801071a7 100644 --- a/test/plugins/Asset.test.ts +++ b/test/plugins/Asset.test.ts @@ -7,11 +7,14 @@ import { advanceBlocks, advanceTime, getLatestBlockTimestamp, + getLatestBlockNumber, setNextBlockTimestamp, } from '../utils/time' -import { ZERO_ADDRESS, ONE_ADDRESS, MAX_UINT192 } from '../../common/constants' +import { ZERO_ADDRESS, ONE_ADDRESS, MAX_UINT192, TradeKind } from '../../common/constants' import { bn, fp } from '../../common/numbers' import { + expectDecayedPrice, + expectExactPrice, expectPrice, expectRTokenPrice, expectUnpriced, @@ -26,12 +29,15 @@ import { CTokenWrapperMock, ERC20Mock, FiatCollateral, + GnosisMock, IAssetRegistry, InvalidFiatCollateral, InvalidMockV3Aggregator, RTokenAsset, StaticATokenMock, TestIBackingManager, + TestIBasketHandler, + TestIFurnace, TestIRToken, USDCMock, UnpricedAssetMock, @@ -42,10 +48,12 @@ import { IMPLEMENTATION, Implementation, ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, ORACLE_ERROR, PRICE_TIMEOUT, VERSION, } from '../fixtures' +import { getTrade } from '../utils/trades' import { useEnv } from '#/utils/env' import snapshotGasCost from '../utils/snapshotGasCost' @@ -86,11 +94,16 @@ describe('Assets contracts #fast', () => { let wallet: Wallet let assetRegistry: IAssetRegistry let backingManager: TestIBackingManager + let basketHandler: TestIBasketHandler + let furnace: TestIFurnace // Factory let AssetFactory: ContractFactory let RTokenAssetFactory: ContractFactory + // Gnosis + let gnosis: GnosisMock + const amt = fp('1e4') before('create fixture loader', async () => { @@ -109,7 +122,10 @@ describe('Assets contracts #fast', () => { basket, assetRegistry, backingManager, + basketHandler, config, + gnosis, + furnace, rToken, rTokenAsset, } = await loadFixture(defaultFixture)) @@ -260,34 +276,51 @@ describe('Assets contracts #fast', () => { await setOraclePrice(compAsset.address, bn('0')) await setOraclePrice(aaveAsset.address, bn('0')) await setOraclePrice(rsrAsset.address, bn('0')) + await setOraclePrice(collateral0.address, bn('0')) + await setOraclePrice(collateral1.address, bn('0')) + + // Fallback prices should be initial prices + await expectExactPrice(compAsset.address, compInitPrice) + await expectExactPrice(rsrAsset.address, rsrInitPrice) + await expectExactPrice(aaveAsset.address, aaveInitPrice) + await expectExactPrice(rTokenAsset.address, rTokenInitPrice) + + // Advance past oracle timeout + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await setOraclePrice(compAsset.address, bn('0')) + await setOraclePrice(aaveAsset.address, bn('0')) + await setOraclePrice(rsrAsset.address, bn('0')) + await setOraclePrice(collateral0.address, bn('0')) + await setOraclePrice(collateral1.address, bn('0')) + await compAsset.refresh() + await rsrAsset.refresh() + await aaveAsset.refresh() + await collateral0.refresh() + await collateral1.refresh() + + // Prices should be decaying + await expectDecayedPrice(compAsset.address) + await expectDecayedPrice(rsrAsset.address) + await expectDecayedPrice(aaveAsset.address) + const p = await rTokenAsset.price() + expect(p[0]).to.be.gt(0) + expect(p[0]).to.be.lt(rTokenInitPrice[0]) + expect(p[1]).to.be.gt(rTokenInitPrice[1]) + expect(p[1]).to.be.lt(MAX_UINT192) + + // After price timeout, should be unpriced + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(compAsset.address, bn('0')) + await setOraclePrice(aaveAsset.address, bn('0')) + await setOraclePrice(rsrAsset.address, bn('0')) + await setOraclePrice(collateral0.address, bn('0')) + await setOraclePrice(collateral1.address, bn('0')) - // Should be unpriced + // Should be unpriced now await expectUnpriced(rsrAsset.address) await expectUnpriced(compAsset.address) await expectUnpriced(aaveAsset.address) - - // Fallback prices should be initial prices - let [lotLow, lotHigh] = await compAsset.lotPrice() - expect(lotLow).to.eq(compInitPrice[0]) - expect(lotHigh).to.eq(compInitPrice[1]) - ;[lotLow, lotHigh] = await rsrAsset.lotPrice() - expect(lotLow).to.eq(rsrInitPrice[0]) - expect(lotHigh).to.eq(rsrInitPrice[1]) - ;[lotLow, lotHigh] = await aaveAsset.lotPrice() - expect(lotLow).to.eq(aaveInitPrice[0]) - expect(lotHigh).to.eq(aaveInitPrice[1]) - - // Update values of underlying tokens of RToken to 0 - await setOraclePrice(collateral0.address, bn(0)) - await setOraclePrice(collateral1.address, bn(0)) - - // RTokenAsset should be unpriced now await expectUnpriced(rTokenAsset.address) - - // Should have initial lot price - ;[lotLow, lotHigh] = await rTokenAsset.lotPrice() - expect(lotLow).to.eq(rTokenInitPrice[0]) - expect(lotHigh).to.eq(rTokenInitPrice[1]) }) it('Should return 0 price for RTokenAsset in full haircut scenario', async () => { @@ -317,12 +350,6 @@ describe('Assets contracts #fast', () => { config.minTradeVolume.mul((await assetRegistry.erc20s()).length) ) expect(await rTokenAsset.maxTradeVolume()).to.equal(config.rTokenMaxTradeVolume) - - // Should have lot price, equal to price when feed works OK - const [lowPrice, highPrice] = await rTokenAsset.price() - const [lotLow, lotHigh] = await rTokenAsset.lotPrice() - expect(lotLow).to.equal(lowPrice) - expect(lotHigh).to.equal(highPrice) }) it('Should calculate trade min correctly', async () => { @@ -349,38 +376,62 @@ describe('Assets contracts #fast', () => { expect(await rTokenAsset.maxTradeVolume()).to.equal(config.rTokenMaxTradeVolume) }) - it('Should be unpriced if price is stale', async () => { - await advanceTime(ORACLE_TIMEOUT.toString()) + it('Should remain at saved price if oracle is stale', async () => { + await advanceTime(ORACLE_TIMEOUT.sub(12).toString()) - // Check unpriced - await expectUnpriced(rsrAsset.address) - await expectUnpriced(compAsset.address) - await expectUnpriced(aaveAsset.address) + // lastSave should not be block timestamp after refresh + await rsrAsset.refresh() + await compAsset.refresh() + await aaveAsset.refresh() + expect(await rsrAsset.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await compAsset.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await aaveAsset.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + + // Check price + await expectPrice(rsrAsset.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(compAsset.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(aaveAsset.address, fp('1'), ORACLE_ERROR, false) }) - it('Should be unpriced in case of invalid timestamp', async () => { + it('Should remain at saved price in case of invalid timestamp', async () => { await setInvalidOracleTimestamp(rsrAsset.address) await setInvalidOracleTimestamp(compAsset.address) await setInvalidOracleTimestamp(aaveAsset.address) - // Check unpriced - await expectUnpriced(rsrAsset.address) - await expectUnpriced(compAsset.address) - await expectUnpriced(aaveAsset.address) + // lastSave should not be block timestamp after refresh + await rsrAsset.refresh() + await compAsset.refresh() + await aaveAsset.refresh() + expect(await rsrAsset.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await compAsset.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await aaveAsset.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + + // Check price is still at saved price + await expectPrice(rsrAsset.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(compAsset.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(aaveAsset.address, fp('1'), ORACLE_ERROR, false) }) - it('Should be unpriced in case of invalid answered round', async () => { + it('Should remain at saved price in case of invalid answered round', async () => { await setInvalidOracleAnsweredRound(rsrAsset.address) await setInvalidOracleAnsweredRound(compAsset.address) await setInvalidOracleAnsweredRound(aaveAsset.address) - // Check unpriced - await expectUnpriced(rsrAsset.address) - await expectUnpriced(compAsset.address) - await expectUnpriced(aaveAsset.address) + // lastSave should not be block timestamp after refresh + await rsrAsset.refresh() + await compAsset.refresh() + await aaveAsset.refresh() + expect(await rsrAsset.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await compAsset.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await aaveAsset.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + + // Check price is still at saved price + await expectPrice(rsrAsset.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(compAsset.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(aaveAsset.address, fp('1'), ORACLE_ERROR, false) }) - it('Should handle reverting edge cases for RToken', async () => { + it('Should handle reverting edge cases for RTokenAsset', async () => { // Swap one of the collaterals for an invalid one const InvalidFiatCollateralFactory = await ethers.getContractFactory('InvalidFiatCollateral') const invalidFiatCollateral: InvalidFiatCollateral = ( @@ -390,7 +441,7 @@ describe('Assets contracts #fast', () => { oracleError: ORACLE_ERROR, erc20: await collateral0.erc20(), maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -415,7 +466,7 @@ describe('Assets contracts #fast', () => { await expect(rTokenAsset.price()).to.be.reverted }) - it('Regression test -- Should handle unpriced collateral for RToken', async () => { + it('Regression test -- Should handle unpriced collateral for RTokenAsset', async () => { // https://github.com/code-423n4/2023-07-reserve-findings/issues/20 // Swap one of the collaterals for an invalid one @@ -427,7 +478,7 @@ describe('Assets contracts #fast', () => { oracleError: ORACLE_ERROR, erc20: await collateral0.erc20(), maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -444,6 +495,115 @@ describe('Assets contracts #fast', () => { await expectUnpriced(rTokenAsset.address) }) + it('Regression test -- RTokenAsset.refresh() should refresh everything', async () => { + // AssetRegistry should refresh + const lastRefreshed = await assetRegistry.lastRefresh() + await rTokenAsset.refresh() + expect(await assetRegistry.lastRefresh()).to.be.gt(lastRefreshed) + + // Furnace should melt + const lastPayout = await furnace.lastPayout() + await advanceTime(12) + await rTokenAsset.refresh() + expect(await furnace.lastPayout()).to.be.gt(lastPayout) + + // Should clear oracle cache + await rTokenAsset.forceUpdatePrice() + let [, cachedAtTime] = await rTokenAsset.cachedOracleData() + expect(cachedAtTime).to.be.gt(0) + await rTokenAsset.refresh() + ;[, cachedAtTime] = await rTokenAsset.cachedOracleData() + expect(cachedAtTime).to.eq(0) + }) + + it('Should handle tokens being out on trade for RTokenAsset', async () => { + // Summary: + // - Run a dutch auction that does not fill + // - Run a batch auction that fills for partial volume + // - Run a dutch auction that fills for full volume + + const low0 = fp('0.99') + const low1 = bn('975344098811881188') // after a 50% basket change + const low2 = bn('975343128415841584') // after batch auction at half volume + const low3 = bn('975560049627103964') // after dutch auction at full volume + + // Price should be [$0.99, $1.01] to start + await expectExactPrice(rTokenAsset.address, [low0, fp('1.01')]) + + // After 50% basket change, expected trading should decrease the lower price to ~$0.9753 + // Upper price remains $1.01 because of uncertainty around how trading will go + await basketHandler + .connect(wallet) + .setPrimeBasket([token.address, usdc.address], [fp('0.5'), fp('0.5')]) + await basketHandler.connect(wallet).refreshBasket() + await expectExactPrice(rTokenAsset.address, [low1, fp('1.01')]) + + // After launching a trade token price should not change + // Regression -- I've confirmed the lower price drops to ~$0.7352 when not tracking balances out on trade + await expect(backingManager.rebalance(TradeKind.DUTCH_AUCTION)).to.emit( + backingManager, + 'TradeStarted' + ) + expect(await backingManager.tradesOpen()).to.equal(1) + await expectExactPrice(rTokenAsset.address, [low1, fp('1.01')]) + + // Settling trade without bidding should not change price + let trade = await ethers.getContractAt( + 'DutchTrade', + await backingManager.trades(aToken.address) + ) + await advanceBlocks((await trade.endBlock()).sub(await getLatestBlockNumber())) + await expect(backingManager.settleTrade(aToken.address)).to.emit( + backingManager, + 'TradeSettled' + ) + expect(await backingManager.tradesOpen()).to.equal(0) + await expectExactPrice(rTokenAsset.address, [low1, fp('1.01')]) + + // Launching the trade a second time, this time Batch Auction, should not change price + await setNextBlockTimestamp((await trade.endTime()) + 13) + await expect(backingManager.rebalance(TradeKind.BATCH_AUCTION)).to.emit( + backingManager, + 'TradeStarted' + ) + expect(await backingManager.tradesOpen()).to.equal(1) + await expectExactPrice(rTokenAsset.address, [low1, fp('1.01')]) + + // Bid in Gnosis for half volume at even prices + const t = await getTrade(backingManager, aToken.address) + const sellAmt = (await t.initBal()).div(2) // half volume + await token.connect(wallet).approve(gnosis.address, sellAmt) + await gnosis.placeBid(0, { + bidder: wallet.address, + sellAmount: sellAmt, + buyAmount: sellAmt, + }) + await advanceTime(config.batchAuctionLength.toNumber()) + await expect(backingManager.settleTrade(aToken.address)).not.to.emit( + backingManager, + 'TradeStarted' + ) + expect(await backingManager.tradesOpen()).to.equal(0) + await expectExactPrice(rTokenAsset.address, [low2, fp('1.01')]) + + // Starting a 3rd auction should not change balances + await expect(backingManager.rebalance(TradeKind.DUTCH_AUCTION)).to.emit( + backingManager, + 'TradeStarted' + ) + expect(await backingManager.tradesOpen()).to.equal(1) + await expectExactPrice(rTokenAsset.address, [low2, fp('1.01')]) + + // Settle 3rd auction for full volume + trade = await ethers.getContractAt('DutchTrade', await backingManager.trades(cToken.address)) + const buyAmt = await trade.bidAmount(await trade.endBlock()) + await usdc.approve(trade.address, buyAmt) + await advanceBlocks((await trade.endBlock()).sub(await getLatestBlockNumber()).sub(1)) + await expect(trade.bid()).to.emit(backingManager, 'TradeSettled') + expect(await backingManager.tradesOpen()).to.equal(1) // launches another trade! + await expectExactPrice(rTokenAsset.address, [low3, bn('1007427552565834095')]) // high end starts to fall + }) + it('Should be able to refresh saved prices', async () => { // Check initial prices - use RSR as example let currBlockTimestamp: number = await getLatestBlockTimestamp() @@ -494,7 +654,7 @@ describe('Assets contracts #fast', () => { ORACLE_ERROR, rsr.address, config.rTokenMaxTradeVolume, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) @@ -533,37 +693,35 @@ describe('Assets contracts #fast', () => { expect(await unpricedRSRAsset.lastSave()).to.equal(currBlockTimestamp) }) - it('Should not revert on refresh if unpriced', async () => { + it('Should not revert on refresh if stale', async () => { // Check initial prices - use RSR as example - const currBlockTimestamp: number = await getLatestBlockTimestamp() - await expectPrice(rsrAsset.address, fp('1'), ORACLE_ERROR, true) + const startBlockTimestamp: number = await getLatestBlockTimestamp() + await expectPrice(rsrAsset.address, fp('1'), ORACLE_ERROR, false) const [prevLowPrice, prevHighPrice] = await rsrAsset.price() expect(await rsrAsset.savedLowPrice()).to.equal(prevLowPrice) expect(await rsrAsset.savedHighPrice()).to.equal(prevHighPrice) - expect(await rsrAsset.lastSave()).to.equal(currBlockTimestamp) + expect(await rsrAsset.lastSave()).to.equal(startBlockTimestamp) // Set invalid oracle await setInvalidOracleTimestamp(rsrAsset.address) - // Check unpriced - uses still previous prices - await expectUnpriced(rsrAsset.address) + // Check price - uses still previous prices + await rsrAsset.refresh() let [lowPrice, highPrice] = await rsrAsset.price() - expect(lowPrice).to.equal(bn(0)) - expect(highPrice).to.equal(MAX_UINT192) + expect(lowPrice).to.equal(prevLowPrice) + expect(highPrice).to.equal(prevHighPrice) expect(await rsrAsset.savedLowPrice()).to.equal(prevLowPrice) expect(await rsrAsset.savedHighPrice()).to.equal(prevHighPrice) - expect(await rsrAsset.lastSave()).to.equal(currBlockTimestamp) + expect(await rsrAsset.lastSave()).to.equal(startBlockTimestamp) - // Perform refresh + // Check price - no update on prices/timestamp await rsrAsset.refresh() - - // Check still unpriced - no update on prices/timestamp - await expectUnpriced(rsrAsset.address) ;[lowPrice, highPrice] = await rsrAsset.price() - expect(lowPrice).to.equal(bn(0)) - expect(highPrice).to.equal(MAX_UINT192) + expect(lowPrice).to.equal(prevLowPrice) + expect(highPrice).to.equal(prevHighPrice) expect(await rsrAsset.savedLowPrice()).to.equal(prevLowPrice) expect(await rsrAsset.savedHighPrice()).to.equal(prevHighPrice) + expect(await rsrAsset.lastSave()).to.equal(startBlockTimestamp) }) it('Reverts if Chainlink feed reverts or runs out of gas', async () => { @@ -581,79 +739,82 @@ describe('Assets contracts #fast', () => { ORACLE_ERROR, rsr.address, config.rTokenMaxTradeVolume, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) // Reverting with no reason await invalidChainlinkFeed.setSimplyRevert(true) await expect(invalidRSRAsset.price()).to.be.reverted - await expect(invalidRSRAsset.lotPrice()).to.be.reverted await expect(invalidRSRAsset.refresh()).to.be.reverted // Runnning out of gas (same error) await invalidChainlinkFeed.setSimplyRevert(false) await expect(invalidRSRAsset.price()).to.be.reverted - await expect(invalidRSRAsset.lotPrice()).to.be.reverted await expect(invalidRSRAsset.refresh()).to.be.reverted }) - it('Should handle lot price correctly', async () => { + it('Should handle price decay correctly', async () => { await rsrAsset.refresh() - // Check lot prices - use RSR as example - const currBlockTimestamp: number = await getLatestBlockTimestamp() - await expectPrice(rsrAsset.address, fp('1'), ORACLE_ERROR, true) + // Check prices - use RSR as example + const startBlockTimestamp: number = await getLatestBlockTimestamp() + await expectPrice(rsrAsset.address, fp('1'), ORACLE_ERROR, false) const [prevLowPrice, prevHighPrice] = await rsrAsset.price() expect(await rsrAsset.savedLowPrice()).to.equal(prevLowPrice) expect(await rsrAsset.savedHighPrice()).to.equal(prevHighPrice) - expect(await rsrAsset.lastSave()).to.equal(currBlockTimestamp) - - // Lot price equals price when feed works OK - const [lotLowPrice1, lotHighPrice1] = await rsrAsset.lotPrice() - expect(lotLowPrice1).to.equal(prevLowPrice) - expect(lotHighPrice1).to.equal(prevHighPrice) + expect(await rsrAsset.lastSave()).to.equal(startBlockTimestamp) // Set invalid oracle await setInvalidOracleTimestamp(rsrAsset.address) // Check unpriced - uses still previous prices - await expectUnpriced(rsrAsset.address) const [lowPrice, highPrice] = await rsrAsset.price() - expect(lowPrice).to.equal(bn(0)) - expect(highPrice).to.equal(MAX_UINT192) + expect(lowPrice).to.equal(prevLowPrice) + expect(highPrice).to.equal(prevHighPrice) expect(await rsrAsset.savedLowPrice()).to.equal(prevLowPrice) expect(await rsrAsset.savedHighPrice()).to.equal(prevHighPrice) - expect(await rsrAsset.lastSave()).to.equal(currBlockTimestamp) + expect(await rsrAsset.lastSave()).to.equal(startBlockTimestamp) - // At first lot price doesn't decrease - const [lotLowPrice2, lotHighPrice2] = await rsrAsset.lotPrice() - expect(lotLowPrice2).to.eq(lotLowPrice1) - expect(lotHighPrice2).to.eq(lotHighPrice1) + // At first price doesn't decrease + const [lowPrice2, highPrice2] = await rsrAsset.price() + expect(lowPrice2).to.eq(lowPrice) + expect(highPrice2).to.eq(highPrice) // Advance past oracleTimeout await advanceTime(await rsrAsset.oracleTimeout()) - // Now lot price decreases - const [lotLowPrice3, lotHighPrice3] = await rsrAsset.lotPrice() - expect(lotLowPrice3).to.be.lt(lotLowPrice2) - expect(lotHighPrice3).to.be.lt(lotHighPrice2) + // Now price widens + const [lowPrice3, highPrice3] = await rsrAsset.price() + expect(lowPrice3).to.be.lt(lowPrice2) + expect(highPrice3).to.be.gt(highPrice2) - // Advance block, lot price keeps decreasing + // Advance block, price keeps widening await advanceBlocks(1) - const [lotLowPrice4, lotHighPrice4] = await rsrAsset.lotPrice() - expect(lotLowPrice4).to.be.lt(lotLowPrice3) - expect(lotHighPrice4).to.be.lt(lotHighPrice3) + const [lowPrice4, highPrice4] = await rsrAsset.price() + expect(lowPrice4).to.be.lt(lowPrice3) + expect(highPrice4).to.be.gt(highPrice3) - // Advance blocks beyond PRICE_TIMEOUT + // Advance blocks beyond PRICE_TIMEOUT; price should be [O, FIX_MAX] await advanceTime(PRICE_TIMEOUT.toNumber()) // Lot price returns 0 once time elapses - const [lotLowPrice5, lotHighPrice5] = await rsrAsset.lotPrice() - expect(lotLowPrice5).to.be.lt(lotLowPrice4) - expect(lotHighPrice5).to.be.lt(lotHighPrice4) - expect(lotLowPrice5).to.be.equal(bn(0)) - expect(lotHighPrice5).to.be.equal(bn(0)) + const [lowPrice5, highPrice5] = await rsrAsset.price() + expect(lowPrice5).to.be.lt(lowPrice4) + expect(highPrice5).to.be.gt(highPrice4) + expect(lowPrice5).to.be.equal(bn(0)) + expect(highPrice5).to.be.equal(MAX_UINT192) + }) + + it('lotPrice (deprecated) is equal to price()', async () => { + for (const asset of [rsrAsset, compAsset, aaveAsset, rTokenAsset]) { + const lotPrice = await asset.lotPrice() + const price = await asset.price() + expect(price.length).to.equal(2) + expect(lotPrice.length).to.equal(price.length) + expect(lotPrice[0]).to.equal(price[0]) + expect(lotPrice[1]).to.equal(price[1]) + } }) }) @@ -724,9 +885,9 @@ describe('Assets contracts #fast', () => { it('refresh() after full price timeout', async () => { await advanceTime((await rsrAsset.priceTimeout()) + (await rsrAsset.oracleTimeout())) - const lotP = await rsrAsset.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) + const p = await rsrAsset.price() + expect(p[0]).to.equal(0) + expect(p[1]).to.equal(MAX_UINT192) }) }) }) diff --git a/test/plugins/Collateral.test.ts b/test/plugins/Collateral.test.ts index ff0441b43c..21ca93859c 100644 --- a/test/plugins/Collateral.test.ts +++ b/test/plugins/Collateral.test.ts @@ -39,6 +39,8 @@ import { } from '../utils/time' import snapshotGasCost from '../utils/snapshotGasCost' import { + expectDecayedPrice, + expectExactPrice, expectPrice, expectRTokenPrice, expectUnpriced, @@ -50,6 +52,7 @@ import { Collateral, defaultFixture, ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, ORACLE_ERROR, PRICE_TIMEOUT, REVENUE_HIDING, @@ -252,7 +255,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: token.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.constants.HashZero, defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -260,6 +263,44 @@ describe('Collateral contracts', () => { ).to.be.revertedWith('targetName missing') }) + it('Should not allow 0 defaultThreshold', async () => { + // ATokenFiatCollateral + await expect( + ATokenFiatCollateralFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: await tokenCollateral.chainlinkFeed(), + oracleError: ORACLE_ERROR, + erc20: aToken.address, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: bn(0), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + REVENUE_HIDING + ) + ).to.be.revertedWith('defaultThreshold zero') + + // CTokenFiatCollateral + await expect( + CTokenFiatCollateralFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: await tokenCollateral.chainlinkFeed(), + oracleError: ORACLE_ERROR, + erc20: cToken.address, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: bn(0), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + REVENUE_HIDING + ) + ).to.be.revertedWith('defaultThreshold zero') + }) + it('Should not allow missing delayUntilDefault', async () => { await expect( FiatCollateralFactory.deploy({ @@ -268,7 +309,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: token.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: bn(0), @@ -284,7 +325,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: aToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: bn(0), @@ -302,7 +343,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: bn(0), @@ -320,7 +361,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: token.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: MAX_DELAY_UNTIL_DEFAULT + 1, @@ -336,7 +377,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: aToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: MAX_DELAY_UNTIL_DEFAULT + 1, @@ -354,7 +395,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: MAX_DELAY_UNTIL_DEFAULT + 1, @@ -373,7 +414,7 @@ describe('Collateral contracts', () => { oracleError: bn('0'), erc20: token.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -389,7 +430,7 @@ describe('Collateral contracts', () => { oracleError: bn('0'), erc20: aToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -407,7 +448,7 @@ describe('Collateral contracts', () => { oracleError: bn('0'), erc20: cToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: bn(0), @@ -426,7 +467,7 @@ describe('Collateral contracts', () => { oracleError: fp('1'), erc20: token.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -442,7 +483,7 @@ describe('Collateral contracts', () => { oracleError: fp('1'), erc20: aToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -460,7 +501,7 @@ describe('Collateral contracts', () => { oracleError: fp('1'), erc20: cToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -481,7 +522,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: aToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -499,7 +540,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -582,7 +623,7 @@ describe('Collateral contracts', () => { ) }) - it('Should become unpriced if price is zero', async () => { + it('Should handle prices correctly when price is zero', async () => { const compInitPrice = await tokenCollateral.price() const aaveInitPrice = await aTokenCollateral.price() const rsrInitPrice = await cTokenCollateral.price() @@ -590,22 +631,25 @@ describe('Collateral contracts', () => { // Update values in Oracles to 0 await setOraclePrice(tokenCollateral.address, bn('0')) - // Should be unpriced - await expectUnpriced(cTokenCollateral.address) - await expectUnpriced(tokenCollateral.address) - await expectUnpriced(aTokenCollateral.address) - // Fallback prices should be initial prices - let [lotLow, lotHigh] = await tokenCollateral.lotPrice() + let [lotLow, lotHigh] = await tokenCollateral.price() expect(lotLow).to.eq(compInitPrice[0]) expect(lotHigh).to.eq(compInitPrice[1]) - ;[lotLow, lotHigh] = await cTokenCollateral.lotPrice() + ;[lotLow, lotHigh] = await cTokenCollateral.price() expect(lotLow).to.eq(rsrInitPrice[0]) expect(lotHigh).to.eq(rsrInitPrice[1]) - ;[lotLow, lotHigh] = await aTokenCollateral.lotPrice() + ;[lotLow, lotHigh] = await aTokenCollateral.price() expect(lotLow).to.eq(aaveInitPrice[0]) expect(lotHigh).to.eq(aaveInitPrice[1]) + // Advance past timeouts + await advanceTime(PRICE_TIMEOUT.add(ORACLE_TIMEOUT).toString()) + + // Should be unpriced + await expectUnpriced(cTokenCollateral.address) + await expectUnpriced(tokenCollateral.address) + await expectUnpriced(aTokenCollateral.address) + // When refreshed, sets status to Unpriced await tokenCollateral.refresh() await aTokenCollateral.refresh() @@ -616,38 +660,56 @@ describe('Collateral contracts', () => { expect(await cTokenCollateral.status()).to.equal(CollateralStatus.IFFY) }) - it('Should be unpriced in case of invalid timestamp', async () => { + it('Should remain at saved price in case of invalid timestamp', async () => { await setInvalidOracleTimestamp(tokenCollateral.address) + await setInvalidOracleTimestamp(usdcCollateral.address) - // Check price of token - await expectUnpriced(tokenCollateral.address) - await expectUnpriced(aTokenCollateral.address) - await expectUnpriced(cTokenCollateral.address) - - // When refreshed, sets status to Unpriced + // lastSave should not be block timestamp after refresh await tokenCollateral.refresh() + await usdcCollateral.refresh() await aTokenCollateral.refresh() await cTokenCollateral.refresh() - + expect(await tokenCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await usdcCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await aTokenCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await cTokenCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + + // Check price is still at saved price + await expectPrice(tokenCollateral.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(usdcCollateral.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(aTokenCollateral.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(cTokenCollateral.address, fp('1').div(50), ORACLE_ERROR, false) + + // Sets status to IFFY expect(await tokenCollateral.status()).to.equal(CollateralStatus.IFFY) + expect(await usdcCollateral.status()).to.equal(CollateralStatus.IFFY) expect(await aTokenCollateral.status()).to.equal(CollateralStatus.IFFY) expect(await cTokenCollateral.status()).to.equal(CollateralStatus.IFFY) }) - it('Should be unpriced in case of invalid answered round', async () => { + it('Should remain at saved price in case of invalid answered round', async () => { await setInvalidOracleAnsweredRound(tokenCollateral.address) + await setInvalidOracleAnsweredRound(usdcCollateral.address) - // Check price of token - await expectUnpriced(tokenCollateral.address) - await expectUnpriced(aTokenCollateral.address) - await expectUnpriced(cTokenCollateral.address) - - // When refreshed, sets status to Unpriced + // lastSave should not be block timestamp after refresh await tokenCollateral.refresh() + await usdcCollateral.refresh() await aTokenCollateral.refresh() await cTokenCollateral.refresh() - + expect(await tokenCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await usdcCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await aTokenCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await cTokenCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + + // Check price is still at saved price + await expectPrice(tokenCollateral.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(usdcCollateral.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(aTokenCollateral.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(cTokenCollateral.address, fp('1').div(50), ORACLE_ERROR, false) + + // Sets status to IFFY expect(await tokenCollateral.status()).to.equal(CollateralStatus.IFFY) + expect(await usdcCollateral.status()).to.equal(CollateralStatus.IFFY) expect(await aTokenCollateral.status()).to.equal(CollateralStatus.IFFY) expect(await cTokenCollateral.status()).to.equal(CollateralStatus.IFFY) }) @@ -714,7 +776,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: aToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -756,6 +818,17 @@ describe('Collateral contracts', () => { expect(await unpricedAppFiatCollateral.savedHighPrice()).to.equal(highPrice) expect(await unpricedAppFiatCollateral.lastSave()).to.equal(currBlockTimestamp) }) + + it('lotPrice (deprecated) is equal to price()', async () => { + for (const coll of [tokenCollateral, usdcCollateral, aTokenCollateral, cTokenCollateral]) { + const lotPrice = await coll.lotPrice() + const price = await coll.price() + expect(price.length).to.equal(2) + expect(lotPrice.length).to.equal(price.length) + expect(lotPrice[0]).to.equal(price[0]) + expect(lotPrice[1]).to.equal(price[1]) + } + }) }) describe('Status', () => { @@ -908,14 +981,24 @@ describe('Collateral contracts', () => { } }) - it('Unpriced if price is stale', async () => { - await advanceTime(ORACLE_TIMEOUT.toString()) + it('Should remain at saved price if oracle is stale', async () => { + await advanceTime(ORACLE_TIMEOUT.sub(12).toString()) - // Check unpriced - await expectUnpriced(tokenCollateral.address) - await expectUnpriced(usdcCollateral.address) - await expectUnpriced(cTokenCollateral.address) - await expectUnpriced(aTokenCollateral.address) + // lastSave should not be block timestamp after refresh + await tokenCollateral.refresh() + await usdcCollateral.refresh() + await cTokenCollateral.refresh() + await aTokenCollateral.refresh() + expect(await tokenCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await usdcCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await cTokenCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + expect(await aTokenCollateral.lastSave()).to.not.equal(await getLatestBlockTimestamp()) + + // Check price + await expectPrice(tokenCollateral.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(usdcCollateral.address, fp('1'), ORACLE_ERROR, false) + await expectPrice(cTokenCollateral.address, fp('1').div(50), ORACLE_ERROR, false) + await expectPrice(aTokenCollateral.address, fp('1'), ORACLE_ERROR, false) }) it('Enters IFFY state when price becomes stale', async () => { @@ -1109,13 +1192,13 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: nonFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, }, targetUnitOracle.address, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) await nonFiatCollateral.refresh() @@ -1132,13 +1215,13 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: nonFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: bn(0), }, targetUnitOracle.address, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ).to.be.revertedWith('delayUntilDefault zero') }) @@ -1152,13 +1235,13 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: nonFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, }, ZERO_ADDRESS, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ).to.be.revertedWith('missing targetUnit feed') }) @@ -1172,13 +1255,13 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: nonFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, }, targetUnitOracle.address, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ).to.be.revertedWith('missing chainlink feed') }) @@ -1192,7 +1275,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: nonFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1203,6 +1286,26 @@ describe('Collateral contracts', () => { ).to.be.revertedWith('targetUnitOracleTimeout zero') }) + it('Should not allow 0 defaultThreshold', async () => { + await expect( + NonFiatCollFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: referenceUnitOracle.address, + oracleError: ORACLE_ERROR, + erc20: nonFiatToken.address, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, + targetName: ethers.utils.formatBytes32String('BTC'), + defaultThreshold: bn(0), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + targetUnitOracle.address, + ORACLE_TIMEOUT_PRE_BUFFER + ) + ).to.be.revertedWith('defaultThreshold zero') + }) + it('Should setup collateral correctly', async function () { // Non-Fiat Token expect(await nonFiatCollateral.isCollateral()).to.equal(true) @@ -1231,6 +1334,8 @@ describe('Collateral contracts', () => { }) it('Should calculate prices correctly', async function () { + const initialPrice = await nonFiatCollateral.price() + // Check initial prices await expectPrice(nonFiatCollateral.address, fp('20000'), ORACLE_ERROR, true) @@ -1240,26 +1345,37 @@ describe('Collateral contracts', () => { // Check new prices await expectPrice(nonFiatCollateral.address, fp('22000'), ORACLE_ERROR, true) - // Unpriced if price is zero - Update Oracles and check prices + // Cached but IFFY if price is zero await targetUnitOracle.updateAnswer(bn('0')) - await expectUnpriced(nonFiatCollateral.address) - - // When refreshed, sets status to IFFY await nonFiatCollateral.refresh() expect(await nonFiatCollateral.status()).to.equal(CollateralStatus.IFFY) + await expectExactPrice(nonFiatCollateral.address, initialPrice) + + // Should become disabled after just ORACLE_TIMEOUT + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await targetUnitOracle.updateAnswer(bn('0')) + expect(await nonFiatCollateral.status()).to.equal(CollateralStatus.DISABLED) + await expectDecayedPrice(nonFiatCollateral.address) // Restore price await targetUnitOracle.updateAnswer(bn('20000e8')) + await referenceUnitOracle.updateAnswer(bn('1e8')) await nonFiatCollateral.refresh() - expect(await nonFiatCollateral.status()).to.equal(CollateralStatus.SOUND) + await expectExactPrice(nonFiatCollateral.address, initialPrice) - // Check the other oracle + // Check the other oracle's impact await referenceUnitOracle.updateAnswer(bn('0')) - await expectUnpriced(nonFiatCollateral.address) + await expectExactPrice(nonFiatCollateral.address, initialPrice) - // When refreshed, sets status to IFFY - await nonFiatCollateral.refresh() - expect(await nonFiatCollateral.status()).to.equal(CollateralStatus.IFFY) + // Advance past oracle timeout + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await referenceUnitOracle.updateAnswer(bn('0')) + await expectDecayedPrice(nonFiatCollateral.address) + + // Advance past price timeout + await advanceTime(PRICE_TIMEOUT.toString()) + await referenceUnitOracle.updateAnswer(bn('0')) + await expectUnpriced(nonFiatCollateral.address) }) it('Reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => { @@ -1275,13 +1391,13 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: nonFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, }, targetUnitOracle.address, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) @@ -1303,13 +1419,13 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: nonFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, }, invalidChainlinkFeed.address, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) // Reverting with no reason @@ -1364,13 +1480,13 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cNonFiatTokenVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, }, targetUnitOracle.address, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, REVENUE_HIDING ) await cTokenNonFiatCollateral.refresh() @@ -1388,13 +1504,13 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cNonFiatTokenVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: bn(0), }, targetUnitOracle.address, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, REVENUE_HIDING ) ).to.be.revertedWith('delayUntilDefault zero') @@ -1409,13 +1525,13 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cNonFiatTokenVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, }, targetUnitOracle.address, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, fp('1') ) ).to.be.revertedWith('revenueHiding out of range') @@ -1430,13 +1546,13 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cNonFiatTokenVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, }, targetUnitOracle.address, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, REVENUE_HIDING ) ).to.be.revertedWith('missing chainlink feed') @@ -1451,13 +1567,13 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cNonFiatTokenVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, }, ZERO_ADDRESS, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, REVENUE_HIDING ) ).to.be.revertedWith('missing targetUnit feed') @@ -1472,7 +1588,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cNonFiatTokenVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1484,6 +1600,27 @@ describe('Collateral contracts', () => { ).to.be.revertedWith('targetUnitOracleTimeout zero') }) + it('Should not allow 0 defaultThreshold', async () => { + await expect( + CTokenNonFiatFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: referenceUnitOracle.address, + oracleError: ORACLE_ERROR, + erc20: cNonFiatTokenVault.address, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, + targetName: ethers.utils.formatBytes32String('BTC'), + defaultThreshold: bn(0), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + targetUnitOracle.address, + ORACLE_TIMEOUT_PRE_BUFFER, + REVENUE_HIDING + ) + ).to.be.revertedWith('defaultThreshold zero') + }) + it('Should setup collateral correctly', async function () { // Non-Fiat Token expect(await cTokenNonFiatCollateral.isCollateral()).to.equal(true) @@ -1522,47 +1659,128 @@ describe('Collateral contracts', () => { }) it('Should calculate prices correctly', async function () { + // Check initial prices await expectPrice(cTokenNonFiatCollateral.address, fp('400'), ORACLE_ERROR, true) - - // Check refPerTok initial values expect(await cTokenNonFiatCollateral.refPerTok()).to.equal(fp('0.02')) // Increase rate to double await cNonFiatTokenVault.setExchangeRate(fp(2)) await cTokenNonFiatCollateral.refresh() - // Check price doubled - await expectPrice(cTokenNonFiatCollateral.address, fp('800'), ORACLE_ERROR, true) - // RefPerTok also doubles in this case expect(await cTokenNonFiatCollateral.refPerTok()).to.equal(fp('0.04')) + // Check new prices + await expectPrice(cTokenNonFiatCollateral.address, fp('800'), ORACLE_ERROR, true) + // Update values in Oracle increase by 10% await targetUnitOracle.updateAnswer(bn('22000e8')) // $22k // Check new price await expectPrice(cTokenNonFiatCollateral.address, fp('880'), ORACLE_ERROR, true) - // Unpriced if price is zero - Update Oracles and check prices - await targetUnitOracle.updateAnswer(bn('0')) - await expectUnpriced(cTokenNonFiatCollateral.address) + // Should be SOUND + await cTokenNonFiatCollateral.refresh() + expect(await cTokenNonFiatCollateral.status()).to.equal(CollateralStatus.SOUND) - // When refreshed, sets status to IFFY + const initialPrice = await cTokenNonFiatCollateral.price() + + // Cached but IFFY when price becomes zero + await targetUnitOracle.updateAnswer(bn('0')) await cTokenNonFiatCollateral.refresh() expect(await cTokenNonFiatCollateral.status()).to.equal(CollateralStatus.IFFY) + await expectExactPrice(cTokenNonFiatCollateral.address, initialPrice) - // Restore + // Should become disabled after just ORACLE_TIMEOUT + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await targetUnitOracle.updateAnswer(bn('0')) + expect(await cTokenNonFiatCollateral.status()).to.equal(CollateralStatus.DISABLED) + await cTokenNonFiatCollateral.refresh() + await expectDecayedPrice(cTokenNonFiatCollateral.address) + + // Restore price await targetUnitOracle.updateAnswer(bn('22000e8')) + await referenceUnitOracle.updateAnswer(bn('1e8')) await cTokenNonFiatCollateral.refresh() - expect(await cTokenNonFiatCollateral.status()).to.equal(CollateralStatus.SOUND) + await expectExactPrice(cTokenNonFiatCollateral.address, initialPrice) + + // Check the other oracle's impact + await referenceUnitOracle.updateAnswer(bn('0')) + await expectExactPrice(cTokenNonFiatCollateral.address, initialPrice) + + // Advance past oracle timeout + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await referenceUnitOracle.updateAnswer(bn('0')) + await expectDecayedPrice(cTokenNonFiatCollateral.address) - // Revert if price is zero - Update the other Oracle + // Advance past price timeout + await advanceTime(PRICE_TIMEOUT.toString()) await referenceUnitOracle.updateAnswer(bn('0')) await expectUnpriced(cTokenNonFiatCollateral.address) + }) - // When refreshed, sets status to IFFY - await cTokenNonFiatCollateral.refresh() - expect(await cTokenNonFiatCollateral.status()).to.equal(CollateralStatus.IFFY) + it('Enters DISABLED state when exchangeRateCurrent() reverts', async () => { + const currRate = await cNonFiatTokenVault.exchangeRateStored() + const [currLow, currHigh] = await cTokenNonFiatCollateral.price() + + expect(await cTokenNonFiatCollateral.status()).to.equal(CollateralStatus.SOUND) + await expectPrice(cTokenNonFiatCollateral.address, fp('400'), ORACLE_ERROR, true) + + // Make cToken revert on exchangeRateCurrent() + const cTokenErc20Mock = ( + await ethers.getContractAt('CTokenMock', await cNonFiatTokenVault.underlying()) + ) + await cTokenErc20Mock.setRevertExchangeRate(true) + + // Refresh - should not revert - Sets DISABLED + await expect(cTokenNonFiatCollateral.refresh()) + .to.emit(cTokenNonFiatCollateral, 'CollateralStatusChanged') + .withArgs(CollateralStatus.SOUND, CollateralStatus.DISABLED) + + expect(await cTokenNonFiatCollateral.status()).to.equal(CollateralStatus.DISABLED) + const expectedDefaultTimestamp: BigNumber = bn(await getLatestBlockTimestamp()) + expect(await cTokenNonFiatCollateral.whenDefault()).to.equal(expectedDefaultTimestamp) + + // Exchange rate stored is still accessible + expect(await cNonFiatTokenVault.exchangeRateStored()).to.equal(currRate) + + // Price remains the same + await expectPrice(cTokenNonFiatCollateral.address, fp('400'), ORACLE_ERROR, true) + const [newLow, newHigh] = await cTokenNonFiatCollateral.price() + expect(newLow).to.equal(currLow) + expect(newHigh).to.equal(currHigh) + }) + + it('Enters DISABLED state when exchangeRateCurrent() reverts', async () => { + const currRate = await cNonFiatTokenVault.exchangeRateStored() + const [currLow, currHigh] = await cTokenNonFiatCollateral.price() + + expect(await cTokenNonFiatCollateral.status()).to.equal(CollateralStatus.SOUND) + await expectPrice(cTokenNonFiatCollateral.address, fp('400'), ORACLE_ERROR, true) + + // Make cToken revert on exchangeRateCurrent() + const cTokenErc20Mock = ( + await ethers.getContractAt('CTokenMock', await cNonFiatTokenVault.underlying()) + ) + await cTokenErc20Mock.setRevertExchangeRate(true) + + // Refresh - should not revert - Sets DISABLED + await expect(cTokenNonFiatCollateral.refresh()) + .to.emit(cTokenNonFiatCollateral, 'CollateralStatusChanged') + .withArgs(CollateralStatus.SOUND, CollateralStatus.DISABLED) + + expect(await cTokenNonFiatCollateral.status()).to.equal(CollateralStatus.DISABLED) + const expectedDefaultTimestamp: BigNumber = bn(await getLatestBlockTimestamp()) + expect(await cTokenNonFiatCollateral.whenDefault()).to.equal(expectedDefaultTimestamp) + + // Exchange rate stored is still accessible + expect(await cNonFiatTokenVault.exchangeRateStored()).to.equal(currRate) + + // Price remains the same + await expectPrice(cTokenNonFiatCollateral.address, fp('400'), ORACLE_ERROR, true) + const [newLow, newHigh] = await cTokenNonFiatCollateral.price() + expect(newLow).to.equal(currLow) + expect(newHigh).to.equal(currHigh) }) it('Enters DISABLED state when exchangeRateCurrent() reverts', async () => { @@ -1610,7 +1828,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cNonFiatTokenVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1639,7 +1857,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cNonFiatTokenVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1681,7 +1899,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: selfRefToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: 0, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1721,7 +1939,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: selfRefToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: bn(100), delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1739,9 +1957,17 @@ describe('Collateral contracts', () => { // Check new prices await expectPrice(selfReferentialCollateral.address, fp('1.1'), ORACLE_ERROR, true) - // Unpriced if price is zero - Update Oracles and check prices + await selfReferentialCollateral.refresh() + const initialPrice = await selfReferentialCollateral.price() + + // Cached price if oracle price is zero await setOraclePrice(selfReferentialCollateral.address, bn(0)) - await expectUnpriced(selfReferentialCollateral.address) + await expectExactPrice(selfReferentialCollateral.address, initialPrice) + + // Decay starts after oracle timeout + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await setOraclePrice(selfReferentialCollateral.address, bn(0)) + await expectDecayedPrice(selfReferentialCollateral.address) // When refreshed, sets status to IFFY await selfReferentialCollateral.refresh() @@ -1758,6 +1984,12 @@ describe('Collateral contracts', () => { // Another call would not change the state await selfReferentialCollateral.refresh() expect(await selfReferentialCollateral.status()).to.equal(CollateralStatus.DISABLED) + + // Final price checks + await expectDecayedPrice(selfReferentialCollateral.address) + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(selfReferentialCollateral.address, bn(0)) + await expectUnpriced(selfReferentialCollateral.address) }) it('Reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => { @@ -1772,7 +2004,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: selfRefToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: 0, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1822,7 +2054,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cSelfRefToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: 0, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1846,7 +2078,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cSelfRefToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1866,7 +2098,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cSelfRefToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: bn(0), delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1886,7 +2118,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cSelfRefToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: bn(200), delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -1950,13 +2182,90 @@ describe('Collateral contracts', () => { // Check new prices await expectPrice(cTokenSelfReferentialCollateral.address, fp('0.044'), ORACLE_ERROR, true) - // Unpriced if price is zero - Update Oracles and check prices + await cTokenSelfReferentialCollateral.refresh() + const initialPrice = await cTokenSelfReferentialCollateral.price() await setOraclePrice(cTokenSelfReferentialCollateral.address, bn(0)) - await expectUnpriced(cTokenSelfReferentialCollateral.address) + await expectExactPrice(cTokenSelfReferentialCollateral.address, initialPrice) - // When refreshed, sets status to IFFY + // Decays if price is zero await cTokenSelfReferentialCollateral.refresh() expect(await cTokenSelfReferentialCollateral.status()).to.equal(CollateralStatus.IFFY) + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await setOraclePrice(cTokenSelfReferentialCollateral.address, bn(0)) + await expectDecayedPrice(cTokenSelfReferentialCollateral.address) + + // Unpriced after price timeout + await advanceTime(PRICE_TIMEOUT.toString()) + await setOraclePrice(cTokenSelfReferentialCollateral.address, bn(0)) + await expectUnpriced(cTokenSelfReferentialCollateral.address) + + // When refreshed, sets status to DISABLED + await cTokenSelfReferentialCollateral.refresh() + expect(await cTokenSelfReferentialCollateral.status()).to.equal(CollateralStatus.DISABLED) + }) + + it('Enters DISABLED state when exchangeRateCurrent() reverts', async () => { + const currRate = await cSelfRefToken.exchangeRateStored() + const [currLow, currHigh] = await cTokenSelfReferentialCollateral.price() + + expect(await cTokenSelfReferentialCollateral.status()).to.equal(CollateralStatus.SOUND) + await expectPrice(cTokenSelfReferentialCollateral.address, fp('0.02'), ORACLE_ERROR, true) + + // Make cToken revert on exchangeRateCurrent() + const cTokenErc20Mock = ( + await ethers.getContractAt('CTokenMock', await cSelfRefToken.underlying()) + ) + await cTokenErc20Mock.setRevertExchangeRate(true) + + // Refresh - should not revert - Sets DISABLED + await expect(cTokenSelfReferentialCollateral.refresh()) + .to.emit(cTokenSelfReferentialCollateral, 'CollateralStatusChanged') + .withArgs(CollateralStatus.SOUND, CollateralStatus.DISABLED) + + expect(await cTokenSelfReferentialCollateral.status()).to.equal(CollateralStatus.DISABLED) + const expectedDefaultTimestamp: BigNumber = bn(await getLatestBlockTimestamp()) + expect(await cTokenSelfReferentialCollateral.whenDefault()).to.equal(expectedDefaultTimestamp) + + // Exchange rate stored is still accessible + expect(await cSelfRefToken.exchangeRateStored()).to.equal(currRate) + + // Price remains the same + await expectPrice(cTokenSelfReferentialCollateral.address, fp('0.02'), ORACLE_ERROR, true) + const [newLow, newHigh] = await cTokenSelfReferentialCollateral.price() + expect(newLow).to.equal(currLow) + expect(newHigh).to.equal(currHigh) + }) + + it('Enters DISABLED state when exchangeRateCurrent() reverts', async () => { + const currRate = await cSelfRefToken.exchangeRateStored() + const [currLow, currHigh] = await cTokenSelfReferentialCollateral.price() + + expect(await cTokenSelfReferentialCollateral.status()).to.equal(CollateralStatus.SOUND) + await expectPrice(cTokenSelfReferentialCollateral.address, fp('0.02'), ORACLE_ERROR, true) + + // Make cToken revert on exchangeRateCurrent() + const cTokenErc20Mock = ( + await ethers.getContractAt('CTokenMock', await cSelfRefToken.underlying()) + ) + await cTokenErc20Mock.setRevertExchangeRate(true) + + // Refresh - should not revert - Sets DISABLED + await expect(cTokenSelfReferentialCollateral.refresh()) + .to.emit(cTokenSelfReferentialCollateral, 'CollateralStatusChanged') + .withArgs(CollateralStatus.SOUND, CollateralStatus.DISABLED) + + expect(await cTokenSelfReferentialCollateral.status()).to.equal(CollateralStatus.DISABLED) + const expectedDefaultTimestamp: BigNumber = bn(await getLatestBlockTimestamp()) + expect(await cTokenSelfReferentialCollateral.whenDefault()).to.equal(expectedDefaultTimestamp) + + // Exchange rate stored is still accessible + expect(await cSelfRefToken.exchangeRateStored()).to.equal(currRate) + + // Price remains the same + await expectPrice(cTokenSelfReferentialCollateral.address, fp('0.02'), ORACLE_ERROR, true) + const [newLow, newHigh] = await cTokenSelfReferentialCollateral.price() + expect(newLow).to.equal(currLow) + expect(newHigh).to.equal(currHigh) }) it('Enters DISABLED state when exchangeRateCurrent() reverts', async () => { @@ -2004,7 +2313,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: cSelfRefToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: 0, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -2054,7 +2363,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: eurFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -2077,7 +2386,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: eurFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: bn(0), @@ -2097,7 +2406,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: eurFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -2117,7 +2426,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: eurFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -2137,7 +2446,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: eurFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -2148,6 +2457,26 @@ describe('Collateral contracts', () => { ).to.be.revertedWith('targetUnitOracleTimeout zero') }) + it('Should not allow 0 defaultThreshold', async () => { + await expect( + EURFiatCollateralFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: referenceUnitOracle.address, + oracleError: ORACLE_ERROR, + erc20: eurFiatToken.address, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, + targetName: ethers.utils.formatBytes32String('BTC'), + defaultThreshold: bn(0), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + targetUnitOracle.address, + ORACLE_TIMEOUT + ) + ).to.be.revertedWith('defaultThreshold zero') + }) + it('Should not revert during refresh when price2 is 0', async () => { const targetFeedAddr = await eurFiatCollateral.targetUnitChainlinkFeed() const targetFeed = await ethers.getContractAt('MockV3Aggregator', targetFeedAddr) @@ -2193,10 +2522,12 @@ describe('Collateral contracts', () => { // Check new prices await expectPrice(eurFiatCollateral.address, fp('2'), ORACLE_ERROR, true) + await eurFiatCollateral.refresh() + const initialPrice = await eurFiatCollateral.price() - // Unpriced if price is zero - Update Oracles and check prices + // Decays if price is zero await referenceUnitOracle.updateAnswer(bn('0')) - await expectUnpriced(eurFiatCollateral.address) + await expectExactPrice(eurFiatCollateral.address, initialPrice) // When refreshed, sets status to IFFY await eurFiatCollateral.refresh() @@ -2211,6 +2542,18 @@ describe('Collateral contracts', () => { await targetUnitOracle.updateAnswer(bn('0')) await eurFiatCollateral.refresh() expect(await eurFiatCollateral.status()).to.equal(CollateralStatus.IFFY) + + // Decays if price is zero + await referenceUnitOracle.updateAnswer(bn('0')) + await expectExactPrice(eurFiatCollateral.address, initialPrice) + await advanceTime(ORACLE_TIMEOUT.add(1).toString()) + await referenceUnitOracle.updateAnswer(bn('0')) + await expectDecayedPrice(eurFiatCollateral.address) + + // After timeout, unpriced + await advanceTime(PRICE_TIMEOUT.toString()) + await referenceUnitOracle.updateAnswer(bn('0')) + await expectUnpriced(eurFiatCollateral.address) }) it('Reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => { @@ -2226,7 +2569,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: eurFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -2254,7 +2597,7 @@ describe('Collateral contracts', () => { oracleError: ORACLE_ERROR, erc20: eurFiatToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -2329,15 +2672,15 @@ describe('Collateral contracts', () => { const oracleTimeout = await tokenCollateral.oracleTimeout() await setNextBlockTimestamp((await getLatestBlockTimestamp()) + oracleTimeout) await advanceBlocks(bn(oracleTimeout).div(12)) + await snapshotGasCost(tokenCollateral.refresh()) }) it('after full price timeout', async () => { await advanceTime( (await tokenCollateral.priceTimeout()) + (await tokenCollateral.oracleTimeout()) ) - const lotP = await tokenCollateral.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) + await expectUnpriced(tokenCollateral.address) + await snapshotGasCost(tokenCollateral.refresh()) }) }) }) diff --git a/test/plugins/RewardableERC20.test.ts b/test/plugins/RewardableERC20.test.ts index abc94deb66..2b10d1847b 100644 --- a/test/plugins/RewardableERC20.test.ts +++ b/test/plugins/RewardableERC20.test.ts @@ -18,12 +18,16 @@ import snapshotGasCost from '../utils/snapshotGasCost' import { formatUnits, parseUnits } from 'ethers/lib/utils' import { MAX_UINT256 } from '#/common/constants' +const SHARE_DECIMAL_OFFSET = 9 // decimals buffer for shares and rewards per share +const BN_SHARE_FACTOR = bn(10).pow(SHARE_DECIMAL_OFFSET) + type Fixture = () => Promise interface RewardableERC20Fixture { rewardableVault: RewardableERC4626VaultTest | RewardableERC20WrapperTest rewardableAsset: ERC20MockRewarding rewardToken: ERC20MockDecimals + rewardableVaultFactory: ContractFactory } // 18 cases: test two wrappers with 2 combinations of decimals [6, 8, 18] @@ -76,6 +80,7 @@ for (const wrapperName of wrapperNames) { rewardableVault, rewardableAsset, rewardToken, + rewardableVaultFactory, } } return fixture @@ -118,18 +123,19 @@ for (const wrapperName of wrapperNames) { describe(wrapperName, () => { // Decimals let shareDecimals: number - + let rewardShareDecimals: number // Assets let rewardableVault: RewardableERC20WrapperTest | RewardableERC4626VaultTest let rewardableAsset: ERC20MockRewarding let rewardToken: ERC20MockDecimals + let rewardableVaultFactory: ContractFactory // Main let alice: Wallet let bob: Wallet const initBalance = parseUnits('10000', assetDecimals) - const rewardAmount = parseUnits('200', rewardDecimals) + let rewardAmount = parseUnits('200', rewardDecimals) let oneShare: BigNumber let initShares: BigNumber @@ -141,14 +147,16 @@ for (const wrapperName of wrapperNames) { beforeEach(async () => { // Deploy fixture - ;({ rewardableVault, rewardableAsset, rewardToken } = await loadFixture(fixture)) + ;({ rewardableVault, rewardableAsset, rewardToken, rewardableVaultFactory } = + await loadFixture(fixture)) await rewardableAsset.mint(alice.address, initBalance) await rewardableAsset.connect(alice).approve(rewardableVault.address, initBalance) await rewardableAsset.mint(bob.address, initBalance) await rewardableAsset.connect(bob).approve(rewardableVault.address, initBalance) - shareDecimals = await rewardableVault.decimals() + shareDecimals = (await rewardableVault.decimals()) + SHARE_DECIMAL_OFFSET + rewardShareDecimals = rewardDecimals + SHARE_DECIMAL_OFFSET initShares = toShares(initBalance, assetDecimals, shareDecimals) oneShare = bn('1').mul(bn(10).pow(shareDecimals)) }) @@ -181,7 +189,9 @@ for (const wrapperName of wrapperNames) { expect(await rewardableVault.lastRewardsPerShare(alice.address)).to.equal(bn(0)) await rewardToken.mint(rewardableVault.address, parseUnits('10', rewardDecimals)) await rewardableVault.sync() - expect(await rewardableVault.rewardsPerShare()).to.equal(parseUnits('1', rewardDecimals)) + expect(await rewardableVault.rewardsPerShare()).to.equal( + parseUnits('1', rewardShareDecimals) + ) }) it('correctly handles reward tracking if supply is burned', async () => { @@ -192,7 +202,9 @@ for (const wrapperName of wrapperNames) { expect(await rewardableVault.lastRewardsPerShare(alice.address)).to.equal(bn(0)) await rewardToken.mint(rewardableVault.address, parseUnits('10', rewardDecimals)) await rewardableVault.sync() - expect(await rewardableVault.rewardsPerShare()).to.equal(parseUnits('1', rewardDecimals)) + expect(await rewardableVault.rewardsPerShare()).to.equal( + parseUnits('1', rewardShareDecimals) + ) // Setting supply to 0 await withdrawAll(rewardableVault.connect(alice)) @@ -211,7 +223,9 @@ for (const wrapperName of wrapperNames) { // Nothing updates.. as totalSupply as totalSupply is 0 await rewardableVault.sync() - expect(await rewardableVault.rewardsPerShare()).to.equal(parseUnits('1', rewardDecimals)) + expect(await rewardableVault.rewardsPerShare()).to.equal( + parseUnits('1', rewardShareDecimals) + ) await rewardableVault .connect(alice) .deposit(parseUnits('10', assetDecimals), alice.address) @@ -223,6 +237,23 @@ for (const wrapperName of wrapperNames) { ) }) + it('checks reward and underlying token are not the same', async () => { + const errorMsg = + wrapperName == Wrapper.ERC4626 + ? 'reward and asset cannot match' + : 'reward and underlying cannot match' + + // Attempt to deploy with same reward and underlying + await expect( + rewardableVaultFactory.deploy( + rewardableAsset.address, + 'Rewarding Test Asset Vault', + 'vrewardTEST', + rewardableAsset.address + ) + ).to.be.revertedWith(errorMsg) + }) + it('1 wei supply', async () => { await rewardableVault.connect(alice).deposit('1', alice.address) expect(await rewardableVault.rewardsPerShare()).to.equal(bn(0)) @@ -259,7 +290,9 @@ for (const wrapperName of wrapperNames) { }) it('alice shows correct balance', async () => { - expect(initShares.mul(3).div(8)).equal(await rewardableVault.balanceOf(alice.address)) + expect(initShares.mul(3).div(8).div(BN_SHARE_FACTOR)).equal( + await rewardableVault.balanceOf(alice.address) + ) }) it('alice shows correct lastRewardsPerShare', async () => { @@ -267,7 +300,9 @@ for (const wrapperName of wrapperNames) { }) it('bob shows correct balance', async () => { - expect(initShares.div(8)).equal(await rewardableVault.balanceOf(bob.address)) + expect(initShares.div(8).div(BN_SHARE_FACTOR)).equal( + await rewardableVault.balanceOf(bob.address) + ) }) it('bob shows correct lastRewardsPerShare', async () => { @@ -276,7 +311,9 @@ for (const wrapperName of wrapperNames) { it('rewardsPerShare is correct', async () => { // rewards / alice's deposit - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + expect(rewardsPerShare).equal( + rewardAmount.mul(oneShare).div(initShares.div(4)).mul(BN_SHARE_FACTOR) + ) }) }) @@ -303,7 +340,9 @@ for (const wrapperName of wrapperNames) { it('alice shows correct lastRewardsPerShare', async () => { // rewards / alice's deposit - expect(initRewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + expect(initRewardsPerShare).equal( + rewardAmount.mul(oneShare).div(initShares.div(4)).mul(BN_SHARE_FACTOR) + ) expect(initRewardsPerShare).equal( await rewardableVault.lastRewardsPerShare(alice.address) ) @@ -314,6 +353,7 @@ for (const wrapperName of wrapperNames) { .mul(oneShare) .div(initShares.div(4)) .add(rewardAmount.mul(oneShare).div(initShares.div(2))) + .mul(BN_SHARE_FACTOR) expect(rewardsPerShare).equal(expectedRewardsPerShare) expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) }) @@ -337,7 +377,9 @@ for (const wrapperName of wrapperNames) { it('rewardsPerShare is correct', async () => { // rewards / alice's deposit - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + expect(rewardsPerShare).equal( + rewardAmount.mul(oneShare).div(initShares.div(4)).mul(BN_SHARE_FACTOR) + ) }) }) @@ -378,7 +420,9 @@ for (const wrapperName of wrapperNames) { it('rewardsPerShare is correct', async () => { // rewards / alice's deposit - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + expect(rewardsPerShare).equal( + rewardAmount.mul(oneShare).div(initShares.div(4)).mul(BN_SHARE_FACTOR) + ) }) }) @@ -404,7 +448,9 @@ for (const wrapperName of wrapperNames) { }) it('bob shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(bob.address)) + expect(initShares.div(4).div(BN_SHARE_FACTOR)).equal( + await rewardableVault.balanceOf(bob.address) + ) }) it('bob shows correct lastRewardsPerShare', async () => { @@ -413,7 +459,9 @@ for (const wrapperName of wrapperNames) { it('rewardsPerShare is correct', async () => { // rewards / alice's deposit - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + expect(rewardsPerShare).equal( + rewardAmount.mul(oneShare).div(initShares.div(4)).mul(BN_SHARE_FACTOR) + ) }) }) @@ -433,7 +481,9 @@ for (const wrapperName of wrapperNames) { }) it('alice shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) + expect(initShares.div(4).div(BN_SHARE_FACTOR)).equal( + await rewardableVault.balanceOf(alice.address) + ) }) it('alice has claimed rewards', async () => { @@ -445,7 +495,9 @@ for (const wrapperName of wrapperNames) { }) it('bob shows correct balance', async () => { - expect(initShares.div(8)).equal(await rewardableVault.balanceOf(bob.address)) + expect(initShares.div(8).div(BN_SHARE_FACTOR)).equal( + await rewardableVault.balanceOf(bob.address) + ) }) it('bob shows correct lastRewardsPerShare', async () => { @@ -454,10 +506,29 @@ for (const wrapperName of wrapperNames) { it('rewardsPerShare is correct', async () => { // rewards / alice's deposit - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + expect(rewardsPerShare).equal( + rewardAmount.mul(oneShare).div(initShares.div(4)).mul(BN_SHARE_FACTOR) + ) }) }) + it('Cannot frontrun claimRewards by inflating your shares', async () => { + await rewardableAsset.connect(bob).approve(rewardableVault.address, MAX_UINT256) + await rewardableAsset.mint(bob.address, initBalance.mul(100)) + await rewardableVault.connect(alice).deposit(initBalance, alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + + // Bob 'flashloans' 100x the current balance of the vault and claims rewards + await rewardableVault.connect(bob).deposit(initBalance.mul(100), bob.address) + await rewardableVault.connect(bob).claimRewards() + + // Alice claimsRewards a bit later + await rewardableVault.connect(alice).claimRewards() + expect(await rewardToken.balanceOf(alice.address)).to.be.gt( + await rewardToken.balanceOf(bob.address) + ) + }) + describe('alice deposit, accrue, bob deposit, accrue, bob claim, alice claim', () => { let rewardsPerShare: BigNumber @@ -480,7 +551,9 @@ for (const wrapperName of wrapperNames) { }) it('alice shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) + expect(initShares.div(4).div(BN_SHARE_FACTOR)).equal( + await rewardableVault.balanceOf(alice.address) + ) }) it('alice has claimed rewards', async () => { @@ -494,7 +567,9 @@ for (const wrapperName of wrapperNames) { }) it('bob shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(bob.address)) + expect(initShares.div(4).div(BN_SHARE_FACTOR)).equal( + await rewardableVault.balanceOf(bob.address) + ) }) it('bob shows correct lastRewardsPerShare', async () => { @@ -511,6 +586,7 @@ for (const wrapperName of wrapperNames) { .mul(oneShare) .div(initShares.div(4)) .add(rewardAmount.mul(oneShare).div(initShares.div(2))) + .mul(BN_SHARE_FACTOR) expect(rewardsPerShare).equal(expectedRewardsPerShare) }) }) @@ -540,7 +616,9 @@ for (const wrapperName of wrapperNames) { }) it('alice shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) + expect(initShares.div(4).div(BN_SHARE_FACTOR)).equal( + await rewardableVault.balanceOf(alice.address) + ) }) it('alice shows correct lastRewardsPerShare', async () => { @@ -552,7 +630,9 @@ for (const wrapperName of wrapperNames) { }) it('bob shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(bob.address)) + expect(initShares.div(4).div(BN_SHARE_FACTOR)).equal( + await rewardableVault.balanceOf(bob.address) + ) }) it('bob shows correct lastRewardsPerShare', async () => { @@ -565,7 +645,9 @@ for (const wrapperName of wrapperNames) { it('rewardsPerShare is correct', async () => { // (rewards / alice's deposit) + (rewards / bob's deposit) - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4)).mul(2)) + expect(rewardsPerShare).equal( + rewardAmount.mul(oneShare).div(initShares.div(4)).mul(2).mul(BN_SHARE_FACTOR) + ) }) }) @@ -576,7 +658,9 @@ for (const wrapperName of wrapperNames) { await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) await rewardableVault.connect(bob).deposit(initBalance.div(4), bob.address) - await rewardableVault.connect(alice).transfer(bob.address, initShares.div(4)) + await rewardableVault + .connect(alice) + .transfer(bob.address, initShares.div(4).div(BN_SHARE_FACTOR)) await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) await rewardableVault.connect(bob).claimRewards() @@ -586,7 +670,9 @@ for (const wrapperName of wrapperNames) { }) it('alice shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) + expect(initShares.div(4).div(BN_SHARE_FACTOR)).equal( + await rewardableVault.balanceOf(alice.address) + ) }) it('alice shows correct lastRewardsPerShare', async () => { @@ -598,7 +684,9 @@ for (const wrapperName of wrapperNames) { }) it('bob shows correct balance', async () => { - expect(initShares.div(2)).equal(await rewardableVault.balanceOf(bob.address)) + expect(initShares.div(2).div(BN_SHARE_FACTOR)).equal( + await rewardableVault.balanceOf(bob.address) + ) }) it('bob shows correct lastRewardsPerShare', async () => { @@ -616,6 +704,84 @@ for (const wrapperName of wrapperNames) { .mul(oneShare) .div(initShares.div(4)) .add(rewardAmount.mul(oneShare).div(initShares.div(2))) + .mul(BN_SHARE_FACTOR) + ) + }) + }) + + describe('correctly applies fractional reward tracking', () => { + rewardAmount = parseUnits('1.9', rewardDecimals) + + beforeEach(async () => { + // Deploy fixture + ;({ rewardableVault, rewardableAsset } = await loadFixture(fixture)) + + await rewardableAsset.mint(alice.address, initBalance) + await rewardableAsset.connect(alice).approve(rewardableVault.address, MAX_UINT256) + await rewardableAsset.mint(bob.address, initBalance) + await rewardableAsset.connect(bob).approve(rewardableVault.address, MAX_UINT256) + }) + + it('Correctly handles fractional rewards', async () => { + expect(await rewardableVault.rewardsPerShare()).to.equal(0) + + await rewardableVault.connect(alice).deposit(initBalance, alice.address) + + for (let i = 0; i < 10; i++) { + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + await rewardableVault.claimRewards() + expect(await rewardableVault.rewardsPerShare()).to.equal( + rewardAmount + .mul(i + 1) + .mul(oneShare) + .div(initShares) + .mul(BN_SHARE_FACTOR) + ) + } + }) + }) + + describe(`correctly rounds rewards`, () => { + // Assets + rewardAmount = parseUnits('1.7', rewardDecimals) + + beforeEach(async () => { + // Deploy fixture + ;({ rewardableVault, rewardableAsset, rewardToken } = await loadFixture(fixture)) + + await rewardableAsset.mint(alice.address, initBalance) + await rewardableAsset.connect(alice).approve(rewardableVault.address, MAX_UINT256) + await rewardableAsset.mint(bob.address, initBalance) + await rewardableAsset.connect(bob).approve(rewardableVault.address, MAX_UINT256) + }) + + it('Avoids wrong distribution of rewards when rounding', async () => { + expect(await rewardToken.balanceOf(alice.address)).to.equal(bn(0)) + expect(await rewardToken.balanceOf(bob.address)).to.equal(bn(0)) + expect(await rewardableVault.rewardsPerShare()).to.equal(0) + + // alice deposit and accrue rewards + await rewardableVault.connect(alice).deposit(initBalance, alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + + // bob deposit + await rewardableVault.connect(bob).deposit(initBalance, bob.address) + + // accrue additional rewards (twice the amount) + await rewardableAsset.accrueRewards(rewardAmount.mul(2), rewardableVault.address) + + // claim all rewards + await rewardableVault.connect(bob).claimRewards() + await rewardableVault.connect(alice).claimRewards() + + // Alice got all first rewards plus half of the second + expect(await rewardToken.balanceOf(alice.address)).to.equal(rewardAmount.mul(2)) + + // Bob only got half of the second rewards + expect(await rewardToken.balanceOf(bob.address)).to.equal(rewardAmount) + + expect(await rewardableVault.rewardsPerShare()).equal( + rewardAmount.mul(2).mul(oneShare).div(initShares).mul(BN_SHARE_FACTOR) ) }) }) @@ -667,12 +833,70 @@ for (const wrapperName of wrapperNames) { for (let i = 0; i < 10; i++) { await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) await rewardableVault.claimRewards() - - expect(await rewardableVault.rewardsPerShare()).to.equal(Math.floor(1.9 * (i + 1))) + expect(await rewardableVault.rewardsPerShare()).to.equal( + bn(`1.9e${SHARE_DECIMAL_OFFSET}`).mul(i + 1) + ) } }) }) + describe(`${wrapperName.replace('Test', '')} Special Case: Rounding - Regression test`, () => { + // Assets + let rewardableVault: RewardableERC20WrapperTest | RewardableERC4626VaultTest + let rewardableAsset: ERC20MockRewarding + let rewardToken: ERC20MockDecimals + // Main + let alice: Wallet + let bob: Wallet + + const initBalance = parseUnits('1000000', 18) + const rewardAmount = parseUnits('1.7', 6) + + const fixture = getFixture(18, 6) + + before('load wallets', async () => { + ;[alice, bob] = (await ethers.getSigners()) as unknown as Wallet[] + }) + + beforeEach(async () => { + // Deploy fixture + ;({ rewardableVault, rewardableAsset, rewardToken } = await loadFixture(fixture)) + + await rewardableAsset.mint(alice.address, initBalance) + await rewardableAsset.connect(alice).approve(rewardableVault.address, MAX_UINT256) + await rewardableAsset.mint(bob.address, initBalance) + await rewardableAsset.connect(bob).approve(rewardableVault.address, MAX_UINT256) + }) + + it('Avoids wrong distribution of rewards when rounding', async () => { + expect(await rewardToken.balanceOf(alice.address)).to.equal(bn(0)) + expect(await rewardToken.balanceOf(bob.address)).to.equal(bn(0)) + expect(await rewardableVault.rewardsPerShare()).to.equal(0) + + // alice deposit and accrue rewards + await rewardableVault.connect(alice).deposit(initBalance, alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + + // bob deposit + await rewardableVault.connect(bob).deposit(initBalance, bob.address) + + // accrue additional rewards (twice the amount) + await rewardableAsset.accrueRewards(rewardAmount.mul(2), rewardableVault.address) + + // claim all rewards + await rewardableVault.connect(bob).claimRewards() + await rewardableVault.connect(alice).claimRewards() + + // Alice got all first rewards plus half of the second + expect(await rewardToken.balanceOf(alice.address)).to.equal(bn(3.4e6)) + + // Bob only got half of the second rewards + expect(await rewardToken.balanceOf(bob.address)).to.equal(bn(1.7e6)) + + expect(await rewardableVault.rewardsPerShare()).to.equal(bn(`3.4e${SHARE_DECIMAL_OFFSET}`)) + }) + }) + const IMPLEMENTATION: Implementation = useEnv('PROTO_IMPL') == Implementation.P1.toString() ? Implementation.P1 : Implementation.P0 diff --git a/test/plugins/__snapshots__/Collateral.test.ts.snap b/test/plugins/__snapshots__/Collateral.test.ts.snap index 83c6bf2eb6..926d33902f 100644 --- a/test/plugins/__snapshots__/Collateral.test.ts.snap +++ b/test/plugins/__snapshots__/Collateral.test.ts.snap @@ -1,8 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Collateral contracts Gas Reporting refresh() after hard default 1`] = `71881`; +exports[`Collateral contracts Gas Reporting refresh() after full price timeout 1`] = `46228`; -exports[`Collateral contracts Gas Reporting refresh() after hard default 2`] = `75163`; +exports[`Collateral contracts Gas Reporting refresh() after hard default 1`] = `71859`; + +exports[`Collateral contracts Gas Reporting refresh() after hard default 2`] = `75141`; + +exports[`Collateral contracts Gas Reporting refresh() after oracle timeout 1`] = `46228`; exports[`Collateral contracts Gas Reporting refresh() during + after soft default 1`] = `61571`; diff --git a/test/plugins/individual-collateral/aave-v3/AaveV3FiatCollateral.test.ts b/test/plugins/individual-collateral/aave-v3/AaveV3FiatCollateral.test.ts index 89bc877c66..a15ac37a23 100644 --- a/test/plugins/individual-collateral/aave-v3/AaveV3FiatCollateral.test.ts +++ b/test/plugins/individual-collateral/aave-v3/AaveV3FiatCollateral.test.ts @@ -15,6 +15,7 @@ import { noop } from 'lodash' import { PRICE_TIMEOUT } from '#/test/fixtures' import { resetFork } from './helpers' import { whileImpersonating } from '#/test/utils/impersonation' +import { pushOracleForward } from '../../../utils/oracles' import { forkNetwork, AUSDC_V3, @@ -72,6 +73,9 @@ export const deployCollateral = async (opts: Partial = {}) => ) await collateral.deployed() + // Push forward chainlink feed + await pushOracleForward(combinedOpts.chainlinkFeed!) + // sometimes we are trying to test a negative test case and we want this to fail silently // fortunately this syntax fails silently because our tools are terrible await expect(collateral.refresh()) @@ -211,6 +215,7 @@ export const stableOpts = { itChecksTargetPerRefDefault: it, itChecksRefPerTokDefault: it, itHasRevenueHiding: it, + itChecksNonZeroDefaultThreshold: it, itIsPricedByPeg: true, chainlinkDefaultAnswer: 1e8, itChecksPriceChanges: it, diff --git a/test/plugins/individual-collateral/aave-v3/__snapshots__/AaveV3FiatCollateral.test.ts.snap b/test/plugins/individual-collateral/aave-v3/__snapshots__/AaveV3FiatCollateral.test.ts.snap index 62ee74c7f7..996921a268 100644 --- a/test/plugins/individual-collateral/aave-v3/__snapshots__/AaveV3FiatCollateral.test.ts.snap +++ b/test/plugins/individual-collateral/aave-v3/__snapshots__/AaveV3FiatCollateral.test.ts.snap @@ -4,26 +4,26 @@ exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting ERC20 transfer 2`] = `36409`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after full price timeout 1`] = `69299`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after full price timeout 1`] = `69288`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after full price timeout 2`] = `67631`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after full price timeout 2`] = `67620`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after hard default 1`] = `72125`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after hard default 1`] = `72103`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after hard default 2`] = `64443`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after hard default 2`] = `64421`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `69299`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `69288`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `67631`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `67620`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after soft default 1`] = `67290`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after soft default 1`] = `87699`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after soft default 2`] = `67290`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() after soft default 2`] = `87625`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() during SOUND 1`] = `87706`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() during SOUND 1`] = `87684`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() during SOUND 2`] = `87706`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() during SOUND 2`] = `87684`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() during soft default 1`] = `89656`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() during soft default 1`] = `89708`; -exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() during soft default 2`] = `87988`; +exports[`Collateral: Aave V3 Fiat Collateral (USDC) collateral functionality Gas Reporting refresh() during soft default 2`] = `87966`; diff --git a/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts b/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts index ed84d0b200..7a4a52862c 100644 --- a/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts @@ -10,7 +10,13 @@ import { PRICE_TIMEOUT, REVENUE_HIDING, } from '../../../fixtures' -import { DefaultFixture, Fixture, getDefaultFixture, ORACLE_TIMEOUT } from '../fixtures' +import { + DefaultFixture, + Fixture, + getDefaultFixture, + ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, +} from '../fixtures' import { getChainId } from '../../../../common/blockchain-utils' import forkBlockNumber from '../../../integration/fork-block-numbers' import { @@ -22,15 +28,20 @@ import { IRTokenSetup, networkConfig, } from '../../../../common/configuration' -import { CollateralStatus, MAX_UINT48, ZERO_ADDRESS } from '../../../../common/constants' +import { + CollateralStatus, + MAX_UINT48, + MAX_UINT192, + ZERO_ADDRESS, +} from '../../../../common/constants' import { expectEvents, expectInIndirectReceipt } from '../../../../common/events' import { bn, fp } from '../../../../common/numbers' import { whileImpersonating } from '../../../utils/impersonation' import { expectPrice, expectRTokenPrice, - expectUnpriced, setOraclePrice, + expectUnpriced, } from '../../../utils/oracles' import { advanceBlocks, @@ -204,7 +215,7 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi ORACLE_ERROR, stkAave.address, config.rTokenMaxTradeVolume, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) @@ -228,7 +239,7 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi oracleError: ORACLE_ERROR, erc20: staticAToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -423,7 +434,7 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi // Validate constructor arguments // Note: Adapt it to your plugin constructor validations it('Should validate constructor arguments correctly', async () => { - // stkAAVEtroller + // Missing erc20 await expect( ATokenFiatCollateralFactory.deploy( { @@ -432,7 +443,7 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi oracleError: ORACLE_ERROR, erc20: ZERO_ADDRESS, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -440,6 +451,24 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi REVENUE_HIDING ) ).to.be.revertedWith('missing erc20') + + // defaultThreshold = 0 + await expect( + ATokenFiatCollateralFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.DAI as string, + oracleError: ORACLE_ERROR, + erc20: staticAToken.address, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: bn(0), + delayUntilDefault, + }, + REVENUE_HIDING + ) + ).to.be.revertedWith('defaultThreshold zero') }) }) @@ -653,10 +682,14 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi describe('Price Handling', () => { it('Should handle invalid/stale Price', async () => { // Does not revert with stale price - await advanceTime(ORACLE_TIMEOUT.toString()) + await advanceTime(ORACLE_TIMEOUT.sub(12).toString()) - // stkAAVEound - await expectUnpriced(aDaiCollateral.address) + // Price is at saved prices + const savedLowPrice = await aDaiCollateral.savedLowPrice() + const savedHighPrice = await aDaiCollateral.savedHighPrice() + const p = await aDaiCollateral.price() + expect(p[0]).to.equal(savedLowPrice) + expect(p[1]).to.equal(savedHighPrice) // Refresh should mark status IFFY await aDaiCollateral.refresh() @@ -672,7 +705,7 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi oracleError: ORACLE_ERROR, erc20: staticAToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -697,7 +730,7 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi oracleError: ORACLE_ERROR, erc20: staticAToken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -714,6 +747,15 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi await zeropriceCtokenCollateral.refresh() expect(await zeropriceCtokenCollateral.status()).to.equal(CollateralStatus.IFFY) }) + + it('lotPrice (deprecated) is equal to price()', async () => { + const lotPrice = await aDaiCollateral.lotPrice() + const price = await aDaiCollateral.price() + expect(price.length).to.equal(2) + expect(lotPrice.length).to.equal(price.length) + expect(lotPrice[0]).to.equal(price[0]) + expect(lotPrice[1]).to.equal(price[1]) + }) }) // Note: Here the idea is to test all possible statuses and check all possible paths to default @@ -968,9 +1010,9 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi await advanceTime( (await aDaiCollateral.priceTimeout()) + (await aDaiCollateral.oracleTimeout()) ) - const lotP = await aDaiCollateral.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) + const p = await aDaiCollateral.price() + expect(p[0]).to.equal(0) + expect(p[1]).to.equal(MAX_UINT192) await snapshotGasCost(aDaiCollateral.refresh()) await snapshotGasCost(aDaiCollateral.refresh()) // 2nd refresh can be different than 1st }) diff --git a/test/plugins/individual-collateral/aave/__snapshots__/ATokenFiatCollateral.test.ts.snap b/test/plugins/individual-collateral/aave/__snapshots__/ATokenFiatCollateral.test.ts.snap index 6cc30614b8..13f8ae6777 100644 --- a/test/plugins/individual-collateral/aave/__snapshots__/ATokenFiatCollateral.test.ts.snap +++ b/test/plugins/individual-collateral/aave/__snapshots__/ATokenFiatCollateral.test.ts.snap @@ -4,26 +4,26 @@ exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting ERC20 Wrapper t exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting ERC20 Wrapper transfer 2`] = `53409`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after full price timeout 1`] = `74365`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after full price timeout 1`] = `74354`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after full price timeout 2`] = `72697`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after full price timeout 2`] = `72686`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after hard default 1`] = `72960`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after hard default 1`] = `72938`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after hard default 2`] = `65278`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after hard default 2`] = `65256`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after oracle timeout 1`] = `74365`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after oracle timeout 1`] = `74354`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after oracle timeout 2`] = `72697`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after oracle timeout 2`] = `72686`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after soft default 1`] = `91169`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after soft default 1`] = `91073`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after soft default 2`] = `91095`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after soft default 2`] = `91073`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during SOUND 1`] = `92233`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during SOUND 1`] = `92285`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during SOUND 2`] = `92307`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during SOUND 2`] = `92285`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during soft default 1`] = `127378`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during soft default 1`] = `127282`; -exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during soft default 2`] = `91436`; +exports[`ATokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during soft default 2`] = `91488`; diff --git a/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts b/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts index 79f063c586..e21a0f66a0 100644 --- a/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts @@ -10,6 +10,7 @@ import { TestICollateral, IAnkrETH, } from '../../../../typechain' +import { pushOracleForward } from '../../../utils/oracles' import { bn, fp } from '../../../../common/numbers' import { ZERO_ADDRESS } from '../../../../common/constants' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' @@ -100,6 +101,9 @@ export const deployCollateral = async ( ) await collateral.deployed() + // Push forward chainlink feed + await pushOracleForward(opts.chainlinkFeed!) + // sometimes we are trying to test a negative test case and we want this to fail silently // fortunately this syntax fails silently because our tools are terrible await expect(collateral.refresh()) @@ -285,6 +289,7 @@ const opts = { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itHasRevenueHiding: it, + itChecksNonZeroDefaultThreshold: it, resetFork, collateralName: 'AnkrStakedETH', chainlinkDefaultAnswer, diff --git a/test/plugins/individual-collateral/ankr/__snapshots__/AnkrEthCollateralTestSuite.test.ts.snap b/test/plugins/individual-collateral/ankr/__snapshots__/AnkrEthCollateralTestSuite.test.ts.snap index 513e5ca5b6..64458dcd1a 100644 --- a/test/plugins/individual-collateral/ankr/__snapshots__/AnkrEthCollateralTestSuite.test.ts.snap +++ b/test/plugins/individual-collateral/ankr/__snapshots__/AnkrEthCollateralTestSuite.test.ts.snap @@ -4,26 +4,26 @@ exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting ERC20 exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting ERC20 transfer 2`] = `43994`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after full price timeout 1`] = `60337`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after full price timeout 1`] = `60326`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after full price timeout 2`] = `55868`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after full price timeout 2`] = `55857`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after hard default 1`] = `99413`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after hard default 1`] = `99391`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after hard default 2`] = `91730`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after hard default 2`] = `91708`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `60337`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `60326`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `55868`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `55857`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after soft default 1`] = `55527`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after soft default 1`] = `55516`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after soft default 2`] = `55527`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() after soft default 2`] = `55516`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() during SOUND 1`] = `91657`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() during SOUND 1`] = `91635`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() during SOUND 2`] = `91657`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() during SOUND 2`] = `91635`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() during soft default 1`] = `99208`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() during soft default 1`] = `99186`; -exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() during soft default 2`] = `91939`; +exports[`Collateral: AnkrStakedETH collateral functionality Gas Reporting refresh() during soft default 2`] = `91917`; diff --git a/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts b/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts index 9f659f1b45..8f5bb5efe1 100644 --- a/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts +++ b/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts @@ -12,6 +12,7 @@ import { ORACLE_TIMEOUT, PRICE_TIMEOUT, } from './constants' +import { pushOracleForward } from '../../../utils/oracles' import { BigNumber, BigNumberish, ContractFactory } from 'ethers' import { bn, fp } from '#/common/numbers' import { TestICollateral } from '@typechain/TestICollateral' @@ -60,6 +61,10 @@ export const deployCollateral = async ( ) await collateral.deployed() + // Push forward chainlink feeds + await pushOracleForward(opts.chainlinkFeed!) + await pushOracleForward(opts.targetPerTokChainlinkFeed ?? CBETH_ETH_PRICE_FEED) + await expect(collateral.refresh()) return collateral @@ -241,6 +246,7 @@ const opts = { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itHasRevenueHiding: it, + itChecksNonZeroDefaultThreshold: it, resetFork, collateralName: 'CBEthCollateral', chainlinkDefaultAnswer, diff --git a/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts b/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts index 489f89d3df..fbc3f6874b 100644 --- a/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts +++ b/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts @@ -277,6 +277,7 @@ const opts = { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itHasRevenueHiding: it, + itChecksNonZeroDefaultThreshold: it, resetFork, collateralName: 'CBEthCollateralL2', chainlinkDefaultAnswer, diff --git a/test/plugins/individual-collateral/cbeth/__snapshots__/CBETHCollateral.test.ts.snap b/test/plugins/individual-collateral/cbeth/__snapshots__/CBETHCollateral.test.ts.snap index afb2111ab5..f7366a3fb9 100644 --- a/test/plugins/individual-collateral/cbeth/__snapshots__/CBETHCollateral.test.ts.snap +++ b/test/plugins/individual-collateral/cbeth/__snapshots__/CBETHCollateral.test.ts.snap @@ -4,26 +4,26 @@ exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting ERC2 exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting ERC20 transfer 2`] = `48379`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `59824`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `59813`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `55355`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `55344`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after hard default 1`] = `98317`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after hard default 1`] = `98295`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after hard default 2`] = `90634`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after hard default 2`] = `90612`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `59824`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `59813`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `55355`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `55344`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after soft default 1`] = `55014`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after soft default 1`] = `55003`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after soft default 2`] = `55014`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() after soft default 2`] = `55003`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `90631`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `90609`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `90631`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `90609`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() during soft default 1`] = `98182`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() during soft default 1`] = `98160`; -exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() during soft default 2`] = `90913`; +exports[`Collateral: CBEthCollateral collateral functionality Gas Reporting refresh() during soft default 2`] = `90891`; diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 40465f48dc..dcb238c332 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -1,35 +1,59 @@ import { expect } from 'chai' import hre, { ethers } from 'hardhat' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { BigNumber } from 'ethers' +import { BigNumber, ContractFactory } from 'ethers' import { useEnv } from '#/utils/env' import { getChainId } from '../../../common/blockchain-utils' -import { networkConfig } from '../../../common/configuration' -import { bn, fp } from '../../../common/numbers' -import { - IERC20Metadata, - InvalidMockV3Aggregator, - MockV3Aggregator, - TestICollateral, -} from '../../../typechain' +import { bn, fp, toBNDecimals } from '../../../common/numbers' +import { DefaultFixture, Fixture, getDefaultFixture, ORACLE_TIMEOUT } from './fixtures' +import { expectInIndirectReceipt } from '../../../common/events' +import { whileImpersonating } from '../../utils/impersonation' +import { IGovParams, IGovRoles, IRTokenSetup, networkConfig } from '../../../common/configuration' import { advanceTime, advanceBlocks, + getLatestBlockNumber, getLatestBlockTimestamp, setNextBlockTimestamp, } from '../../utils/time' -import { MAX_UINT48, MAX_UINT192 } from '../../../common/constants' +import { + MAX_UINT48, + MAX_UINT192, + MAX_UINT256, + TradeKind, + ZERO_ADDRESS, +} from '../../../common/constants' import { CollateralFixtureContext, CollateralTestSuiteFixtures, CollateralStatus, } from './pluginTestTypes' -import { expectPrice, expectUnpriced } from '../../utils/oracles' +import { + expectDecayedPrice, + expectExactPrice, + expectPrice, + expectUnpriced, +} from '../../utils/oracles' +import { + ERC20Mock, + FacadeWrite, + IAssetRegistry, + IERC20Metadata, + InvalidMockV3Aggregator, + MockV3Aggregator, + TestIBackingManager, + TestIBasketHandler, + TestICollateral, + TestIDeployer, + TestIMain, + TestIRevenueTrader, + TestIRToken, +} from '../../../typechain' import snapshotGasCost from '../../utils/snapshotGasCost' -import { IMPLEMENTATION, Implementation } from '../../fixtures' +import { IMPLEMENTATION, Implementation, ORACLE_ERROR, PRICE_TIMEOUT } from '../../fixtures' -// const describeFork = useEnv('FORK') ? describe : describe.skip const getDescribeFork = (targetNetwork = 'mainnet') => { return useEnv('FORK') && useEnv('FORK_NETWORK') === targetNetwork ? describe : describe.skip } @@ -56,6 +80,7 @@ export default function fn( itChecksTargetPerRefDefault, itChecksRefPerTokDefault, itChecksPriceChanges, + itChecksNonZeroDefaultThreshold, itHasRevenueHiding, itIsPricedByPeg, resetFork, @@ -105,6 +130,12 @@ export default function fn( ) }) + itChecksNonZeroDefaultThreshold('does not allow 0 defaultThreshold', async () => { + await expect(deployCollateral({ defaultThreshold: bn('0') })).to.be.revertedWith( + 'defaultThreshold zero' + ) + }) + describe('collateral-specific tests', collateralSpecificConstructorTests) }) @@ -286,28 +317,40 @@ export default function fn( expect(newHigh).to.be.gt(initHigh) }) - it('returns unpriced for 0-valued oracle', async () => { + it('decays for 0-valued oracle', async () => { + const initialPrice = await collateral.price() + // Set price of underlying to 0 const updateAnswerTx = await chainlinkFeed.updateAnswer(0) await updateAnswerTx.wait() - // (0, FIX_MAX) is returned + // Price remains same at first, though IFFY + await collateral.refresh() + await expectExactPrice(collateral.address, initialPrice) + expect(await collateral.status()).to.equal(CollateralStatus.IFFY) + + // After oracle timeout decay begins + const oracleTimeout = await collateral.oracleTimeout() + await setNextBlockTimestamp((await getLatestBlockTimestamp()) + oracleTimeout) + await advanceBlocks(1 + oracleTimeout / 12) + await collateral.refresh() + await expectDecayedPrice(collateral.address) + + // After price timeout it becomes unpriced + const priceTimeout = await collateral.priceTimeout() + await setNextBlockTimestamp((await getLatestBlockTimestamp()) + priceTimeout) + await advanceBlocks(1 + priceTimeout / 12) await expectUnpriced(collateral.address) - // When refreshed, sets status to Unpriced + // When refreshed, sets status to DISABLED await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) + expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) }) - it('reverts in case of invalid timestamp', async () => { + it('does not revert in case of invalid timestamp', async () => { await chainlinkFeed.setInvalidTimestamp() - // Check price of token - const [low, high] = await collateral.price() - expect(low).to.equal(0) - expect(high).to.equal(MAX_UINT192) - - // When refreshed, sets status to Unpriced + // When refreshed, sets status to IFFY await collateral.refresh() expect(await collateral.status()).to.equal(CollateralStatus.IFFY) }) @@ -319,14 +362,29 @@ export default function fn( }) // Should remain SOUND after a 1% decrease + let refPerTok = await ctx.collateral.refPerTok() await reduceRefPerTok(ctx, 1) // 1% decrease await ctx.collateral.refresh() expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) + // refPerTok should be unchanged + expect(await ctx.collateral.refPerTok()).to.be.closeTo( + refPerTok, + refPerTok.div(bn('1e3')) + ) // within 1-part-in-1-thousand + // Should become DISABLED if drops more than that + refPerTok = await ctx.collateral.refPerTok() await reduceRefPerTok(ctx, 1) // another 1% decrease await ctx.collateral.refresh() expect(await ctx.collateral.status()).to.equal(CollateralStatus.DISABLED) + + // refPerTok should have fallen 1% + refPerTok = refPerTok.sub(refPerTok.div(100)) + expect(await ctx.collateral.refPerTok()).to.be.closeTo( + refPerTok, + refPerTok.div(bn('1e3')) + ) // within 1-part-in-1-thousand }) it('reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => { @@ -361,29 +419,36 @@ export default function fn( expect(await collateral.status()).to.equal(CollateralStatus.IFFY) }) - it('decays lotPrice over priceTimeout period', async () => { - // Prices should start out equal + it('decays price over priceTimeout period', async () => { await collateral.refresh() - const p = await collateral.price() - let lotP = await collateral.lotPrice() - expect(p.length).to.equal(lotP.length) - expect(p[0]).to.equal(lotP[0]) - expect(p[1]).to.equal(lotP[1]) + const savedLow = await collateral.savedLowPrice() + const savedHigh = await collateral.savedHighPrice() + // Price should start out at saved prices + let p = await collateral.price() + expect(p[0]).to.equal(savedLow) + expect(p[1]).to.equal(savedHigh) await advanceTime(await collateral.oracleTimeout()) // Should be roughly half, after half of priceTimeout const priceTimeout = await collateral.priceTimeout() await advanceTime(priceTimeout / 2) - lotP = await collateral.lotPrice() - expect(lotP[0]).to.be.closeTo(p[0].div(2), p[0].div(2).div(10000)) // 1 part in 10 thousand - expect(lotP[1]).to.be.closeTo(p[1].div(2), p[1].div(2).div(10000)) // 1 part in 10 thousand + p = await collateral.price() + expect(p[0]).to.be.closeTo(savedLow.div(2), p[0].div(2).div(10000)) // 1 part in 10 thousand + expect(p[1]).to.be.closeTo(savedHigh.mul(2), p[1].mul(2).div(10000)) // 1 part in 10 thousand - // Should be 0 after full priceTimeout + // Should be unpriced after full priceTimeout await advanceTime(priceTimeout / 2) - lotP = await collateral.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) + await expectUnpriced(collateral.address) + }) + + it('lotPrice (deprecated) is equal to price()', async () => { + const lotPrice = await collateral.lotPrice() + const price = await collateral.price() + expect(price.length).to.equal(2) + expect(lotPrice.length).to.equal(price.length) + expect(lotPrice[0]).to.equal(price[0]) + expect(lotPrice[1]).to.equal(price[1]) }) }) @@ -547,9 +612,9 @@ export default function fn( await advanceTime( (await collateral.priceTimeout()) + (await collateral.oracleTimeout()) ) - const lotP = await collateral.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) + const p = await collateral.price() + expect(p[0]).to.equal(0) + expect(p[1]).to.equal(MAX_UINT192) }) itChecksRefPerTokDefault('after hard default', async () => { @@ -568,5 +633,368 @@ export default function fn( }) }) }) + + describe('integration tests', () => { + before(resetFork) + + let ctx: X + let owner: SignerWithAddress + let addr1: SignerWithAddress + + let chainId: number + + let defaultFixture: Fixture + + let supply: BigNumber + + // Tokens/Assets + let pairedColl: TestICollateral + let pairedERC20: ERC20Mock + let collateralERC20: IERC20Metadata + let collateral: TestICollateral + + // Core Contracts + let main: TestIMain + let rToken: TestIRToken + let assetRegistry: IAssetRegistry + let backingManager: TestIBackingManager + let basketHandler: TestIBasketHandler + let rTokenTrader: TestIRevenueTrader + + let deployer: TestIDeployer + let facadeWrite: FacadeWrite + let govParams: IGovParams + let govRoles: IGovRoles + + const config = { + dist: { + rTokenDist: bn(100), // 100% RToken + rsrDist: bn(0), // 0% RSR + }, + minTradeVolume: bn('0'), // $0 + rTokenMaxTradeVolume: MAX_UINT192, // +inf + shortFreeze: bn('259200'), // 3 days + longFreeze: bn('2592000'), // 30 days + rewardRatio: bn('1069671574938'), // approx. half life of 90 days + unstakingDelay: bn('1209600'), // 2 weeks + withdrawalLeak: fp('0'), // 0%; always refresh + warmupPeriod: bn('60'), // (the delay _after_ SOUND was regained) + tradingDelay: bn('0'), // (the delay _after_ default has been confirmed) + batchAuctionLength: bn('900'), // 15 minutes + dutchAuctionLength: bn('1800'), // 30 minutes + backingBuffer: fp('0'), // 0% + maxTradeSlippage: fp('0.01'), // 1% + issuanceThrottle: { + amtRate: fp('1e6'), // 1M RToken + pctRate: fp('0.05'), // 5% + }, + redemptionThrottle: { + amtRate: fp('1e6'), // 1M RToken + pctRate: fp('0.05'), // 5% + }, + } + + interface IntegrationFixture { + ctx: X + protocol: DefaultFixture + } + + const integrationFixture: Fixture = + async function (): Promise { + return { + ctx: await loadFixture( + makeCollateralFixtureContext(owner, { maxTradeVolume: MAX_UINT192 }) + ), + protocol: await loadFixture(defaultFixture), + } + } + + before(async () => { + defaultFixture = await getDefaultFixture(collateralName) + chainId = await getChainId(hre) + if (useEnv('FORK_NETWORK').toLowerCase() === 'base') chainId = 8453 + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + ;[, owner, addr1] = await ethers.getSigners() + }) + + beforeEach(async () => { + let protocol: DefaultFixture + ;({ ctx, protocol } = await loadFixture(integrationFixture)) + ;({ collateral } = ctx) + ;({ deployer, facadeWrite, govParams } = protocol) + + supply = fp('1') + + // Create a paired collateral of the same targetName + pairedColl = await makePairedCollateral(await collateral.targetName()) + await pairedColl.refresh() + expect(await pairedColl.status()).to.equal(CollateralStatus.SOUND) + pairedERC20 = await ethers.getContractAt('ERC20Mock', await pairedColl.erc20()) + + // Prep collateral + collateralERC20 = await ethers.getContractAt('IERC20Metadata', await collateral.erc20()) + await mintCollateralTo( + ctx, + toBNDecimals(fp('1'), await collateralERC20.decimals()), + addr1, + addr1.address + ) + + // Set primary basket + const rTokenSetup: IRTokenSetup = { + assets: [], + primaryBasket: [collateral.address, pairedColl.address], + weights: [fp('0.5e-4'), fp('0.5e-4')], + backups: [], + beneficiaries: [], + } + + // Deploy RToken via FacadeWrite + const receipt = await ( + await facadeWrite.connect(owner).deployRToken( + { + name: 'RTKN RToken', + symbol: 'RTKN', + mandate: 'mandate', + params: config, + }, + rTokenSetup + ) + ).wait() + + // Get Main + const mainAddr = expectInIndirectReceipt(receipt, deployer.interface, 'RTokenCreated').args + .main + main = await ethers.getContractAt('TestIMain', mainAddr) + + // Get core contracts + assetRegistry = ( + await ethers.getContractAt('IAssetRegistry', await main.assetRegistry()) + ) + backingManager = ( + await ethers.getContractAt('TestIBackingManager', await main.backingManager()) + ) + basketHandler = ( + await ethers.getContractAt('TestIBasketHandler', await main.basketHandler()) + ) + rToken = await ethers.getContractAt('TestIRToken', await main.rToken()) + rTokenTrader = ( + await ethers.getContractAt('TestIRevenueTrader', await main.rTokenTrader()) + ) + + // Set initial governance roles + govRoles = { + owner: owner.address, + guardian: ZERO_ADDRESS, + pausers: [], + shortFreezers: [], + longFreezers: [], + } + // Setup owner and unpause + await facadeWrite.connect(owner).setupGovernance( + rToken.address, + false, // do not deploy governance + true, // unpaused + govParams, // mock values, not relevant + govRoles + ) + + // Advance past warmup period + await setNextBlockTimestamp( + (await getLatestBlockTimestamp()) + (await basketHandler.warmupPeriod()) + ) + + // Should issue + await collateralERC20.connect(addr1).approve(rToken.address, MAX_UINT256) + await pairedERC20.connect(addr1).approve(rToken.address, MAX_UINT256) + await rToken.connect(addr1).issue(supply) + }) + + it('can be put into an RToken basket', async () => { + await assetRegistry.refresh() + expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) + }) + + it('issues', async () => { + // Issuance in beforeEach + expect(await rToken.totalSupply()).to.equal(supply) + }) + + it('redeems', async () => { + await rToken.connect(addr1).redeem(supply) + expect(await rToken.totalSupply()).to.equal(0) + const initialCollBal = toBNDecimals(fp('1'), await collateralERC20.decimals()) + expect(await collateralERC20.balanceOf(addr1.address)).to.be.closeTo( + initialCollBal, + initialCollBal.div(bn('1e5')) // 1-part-in-100k + ) + }) + + it('rebalances out of the collateral', async () => { + // Remove collateral from basket + await basketHandler.connect(owner).setPrimeBasket([pairedERC20.address], [fp('1e-4')]) + await expect(basketHandler.connect(owner).refreshBasket()) + .to.emit(basketHandler, 'BasketSet') + .withArgs(anyValue, [pairedERC20.address], [fp('1e-4')], false) + await setNextBlockTimestamp( + (await getLatestBlockTimestamp()) + config.warmupPeriod.toNumber() + ) + + // Run rebalancing auction + await expect(backingManager.rebalance(TradeKind.DUTCH_AUCTION)) + .to.emit(backingManager, 'TradeStarted') + .withArgs(anyValue, collateralERC20.address, pairedERC20.address, anyValue, anyValue) + const tradeAddr = await backingManager.trades(collateralERC20.address) + expect(tradeAddr).to.not.equal(ZERO_ADDRESS) + const trade = await ethers.getContractAt('DutchTrade', tradeAddr) + expect(await trade.sell()).to.equal(collateralERC20.address) + expect(await trade.buy()).to.equal(pairedERC20.address) + const buyAmt = await trade.bidAmount(await trade.endBlock()) + await pairedERC20.connect(addr1).approve(trade.address, buyAmt) + await advanceBlocks((await trade.endBlock()).sub(await getLatestBlockNumber()).sub(1)) + const pairedBal = await pairedERC20.balanceOf(backingManager.address) + await expect(trade.connect(addr1).bid()).to.emit(backingManager, 'TradeSettled') + expect(await pairedERC20.balanceOf(backingManager.address)).to.be.gt(pairedBal) + expect(await backingManager.tradesOpen()).to.equal(0) + }) + + it('forwards revenue and sells in a revenue auction', async () => { + // Send excess collateral to the RToken trader via forwardRevenue() + const mintAmt = toBNDecimals(fp('1e-6'), await collateralERC20.decimals()) + await mintCollateralTo( + ctx, + mintAmt.gt('150') ? mintAmt : bn('150'), + addr1, + backingManager.address + ) + await backingManager.forwardRevenue([collateralERC20.address]) + expect(await collateralERC20.balanceOf(rTokenTrader.address)).to.be.gt(0) + + // Run revenue auction + await expect( + rTokenTrader.manageTokens([collateralERC20.address], [TradeKind.DUTCH_AUCTION]) + ) + .to.emit(rTokenTrader, 'TradeStarted') + .withArgs(anyValue, collateralERC20.address, rToken.address, anyValue, anyValue) + const tradeAddr = await rTokenTrader.trades(collateralERC20.address) + expect(tradeAddr).to.not.equal(ZERO_ADDRESS) + const trade = await ethers.getContractAt('DutchTrade', tradeAddr) + expect(await trade.sell()).to.equal(collateralERC20.address) + expect(await trade.buy()).to.equal(rToken.address) + const buyAmt = await trade.bidAmount(await trade.endBlock()) + await rToken.connect(addr1).approve(trade.address, buyAmt) + await advanceBlocks((await trade.endBlock()).sub(await getLatestBlockNumber()).sub(1)) + await expect(trade.connect(addr1).bid()).to.emit(rTokenTrader, 'TradeSettled') + expect(await rTokenTrader.tradesOpen()).to.equal(0) + }) + + // === Integration Test Helpers === + + const makePairedCollateral = async (target: string): Promise => { + const onBase = useEnv('FORK_NETWORK').toLowerCase() == 'base' + const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( + 'MockV3Aggregator' + ) + const chainlinkFeed: MockV3Aggregator = ( + await MockV3AggregatorFactory.deploy(8, bn('1e8')) + ) + + if (target == ethers.utils.formatBytes32String('USD')) { + // USD + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + onBase ? networkConfig[chainId].tokens.USDbC! : networkConfig[chainId].tokens.USDC! + ) + const whale = onBase + ? '0xb4885bc63399bf5518b994c1d0c153334ee579d0' + : '0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf' + await whileImpersonating(whale, async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) + const FiatCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'FiatCollateral' + ) + return await FiatCollateralFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20.address, + maxTradeVolume: MAX_UINT192, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01'), // 1% + delayUntilDefault: bn('86400'), // 24h, + }) + } else if (target == ethers.utils.formatBytes32String('ETH')) { + // ETH + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + networkConfig[chainId].tokens.WETH! + ) + const whale = onBase + ? '0xb4885bc63399bf5518b994c1d0c153334ee579d0' + : '0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E' + await whileImpersonating(whale, async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) + const SelfReferentialFactory: ContractFactory = await ethers.getContractFactory( + 'SelfReferentialCollateral' + ) + return await SelfReferentialFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20.address, + maxTradeVolume: MAX_UINT192, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('ETH'), + defaultThreshold: fp('0'), // 0% + delayUntilDefault: bn('0'), // 0, + }) + } else if (target == ethers.utils.formatBytes32String('BTC')) { + // No official WBTC on base yet + if (onBase) throw new Error('no WBTC on base') + // BTC + const targetUnitOracle: MockV3Aggregator = ( + await MockV3AggregatorFactory.deploy(8, bn('1e8')) + ) + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + networkConfig[chainId].tokens.WBTC! + ) + await whileImpersonating('0xccf4429db6322d5c611ee964527d42e5d685dd6a', async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) + const NonFiatFactory: ContractFactory = await ethers.getContractFactory( + 'NonFiatCollateral' + ) + return await NonFiatFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20.address, + maxTradeVolume: MAX_UINT192, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('BTC'), + defaultThreshold: fp('0.01'), // 1% + delayUntilDefault: bn('86400'), // 24h, + }, + targetUnitOracle.address, + ORACLE_TIMEOUT + ) + } else { + throw new Error(`Unknown target: ${target}`) + } + } + }) }) } diff --git a/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts b/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts index 876b6e5e52..921b3f1bec 100644 --- a/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts @@ -10,7 +10,13 @@ import { PRICE_TIMEOUT, REVENUE_HIDING, } from '../../../fixtures' -import { DefaultFixture, Fixture, getDefaultFixture, ORACLE_TIMEOUT } from '../fixtures' +import { + DefaultFixture, + Fixture, + getDefaultFixture, + ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, +} from '../fixtures' import { getChainId } from '../../../../common/blockchain-utils' import forkBlockNumber from '../../../integration/fork-block-numbers' import { @@ -22,7 +28,12 @@ import { IRTokenSetup, networkConfig, } from '../../../../common/configuration' -import { CollateralStatus, MAX_UINT48, ZERO_ADDRESS } from '../../../../common/constants' +import { + CollateralStatus, + MAX_UINT48, + MAX_UINT192, + ZERO_ADDRESS, +} from '../../../../common/constants' import { expectEvents, expectInIndirectReceipt } from '../../../../common/events' import { bn, fp, toBNDecimals } from '../../../../common/numbers' import { whileImpersonating } from '../../../utils/impersonation' @@ -207,7 +218,7 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi ORACLE_ERROR, compToken.address, config.rTokenMaxTradeVolume, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) @@ -230,7 +241,7 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi oracleError: ORACLE_ERROR, erc20: cDaiVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -425,7 +436,7 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi oracleError: ORACLE_ERROR, erc20: ZERO_ADDRESS, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -461,7 +472,7 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi oracleError: ORACLE_ERROR, erc20: vault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -469,6 +480,24 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi REVENUE_HIDING ) ).to.be.revertedWith('referenceERC20Decimals missing') + + // defaultThreshold = 0 + await expect( + CTokenCollateralFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.DAI as string, + oracleError: ORACLE_ERROR, + erc20: cDaiVault.address, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: bn(0), + delayUntilDefault, + }, + REVENUE_HIDING + ) + ).to.be.revertedWith('defaultThreshold zero') }) }) @@ -666,10 +695,14 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi describe('Price Handling', () => { it('Should handle invalid/stale Price', async () => { // Does not revert with stale price - await advanceTime(ORACLE_TIMEOUT.toString()) + await advanceTime(ORACLE_TIMEOUT.sub(12).toString()) - // Compound - await expectUnpriced(cDaiCollateral.address) + // Price is at saved prices + const savedLowPrice = await cDaiCollateral.savedLowPrice() + const savedHighPrice = await cDaiCollateral.savedHighPrice() + const p = await cDaiCollateral.price() + expect(p[0]).to.equal(savedLowPrice) + expect(p[1]).to.equal(savedHighPrice) // Refresh should mark status IFFY await cDaiCollateral.refresh() @@ -685,7 +718,7 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi oracleError: ORACLE_ERROR, erc20: cDaiVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -710,7 +743,7 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi oracleError: ORACLE_ERROR, erc20: cDaiVault.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold, delayUntilDefault, @@ -727,6 +760,15 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi await zeropriceCtokenCollateral.refresh() expect(await zeropriceCtokenCollateral.status()).to.equal(CollateralStatus.IFFY) }) + + it('lotPrice (deprecated) is equal to price()', async () => { + const lotPrice = await cDaiCollateral.lotPrice() + const price = await cDaiCollateral.price() + expect(price.length).to.equal(2) + expect(lotPrice.length).to.equal(price.length) + expect(lotPrice[0]).to.equal(price[0]) + expect(lotPrice[1]).to.equal(price[1]) + }) }) // Note: Here the idea is to test all possible statuses and check all possible paths to default @@ -1054,9 +1096,9 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi await advanceTime( (await cDaiCollateral.priceTimeout()) + (await cDaiCollateral.oracleTimeout()) ) - const lotP = await cDaiCollateral.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) + const p = await cDaiCollateral.price() + expect(p[0]).to.equal(0) + expect(p[1]).to.equal(MAX_UINT192) await snapshotGasCost(cDaiCollateral.refresh()) await snapshotGasCost(cDaiCollateral.refresh()) // 2nd refresh can be different than 1st }) diff --git a/test/plugins/individual-collateral/compoundv2/__snapshots__/CTokenFiatCollateral.test.ts.snap b/test/plugins/individual-collateral/compoundv2/__snapshots__/CTokenFiatCollateral.test.ts.snap index b0874c79c7..23638304fb 100644 --- a/test/plugins/individual-collateral/compoundv2/__snapshots__/CTokenFiatCollateral.test.ts.snap +++ b/test/plugins/individual-collateral/compoundv2/__snapshots__/CTokenFiatCollateral.test.ts.snap @@ -4,26 +4,26 @@ exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting ERC20 transfer exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting ERC20 transfer 2`] = `173113`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after full price timeout 1`] = `119361`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after full price timeout 1`] = `119350`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after full price timeout 2`] = `117692`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after full price timeout 2`] = `117681`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after hard default 1`] = `76242`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after hard default 1`] = `76220`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after hard default 2`] = `68560`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after hard default 2`] = `68538`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after oracle timeout 1`] = `119361`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after oracle timeout 1`] = `119350`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after oracle timeout 2`] = `117692`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after oracle timeout 2`] = `117681`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after soft default 1`] = `138781`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after soft default 1`] = `138759`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after soft default 2`] = `138707`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() after soft default 2`] = `138685`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during SOUND 1`] = `139858`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during SOUND 1`] = `139836`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during SOUND 2`] = `139858`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during SOUND 2`] = `139836`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during soft default 1`] = `175004`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during soft default 1`] = `174982`; -exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during soft default 2`] = `139061`; +exports[`CTokenFiatCollateral - Mainnet Forking P1 Gas Reporting refresh() during soft default 2`] = `139039`; diff --git a/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts b/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts index 45f9e0cc8e..7c91bd2064 100644 --- a/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts +++ b/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts @@ -22,6 +22,7 @@ import { CometMock__factory, TestICollateral, } from '../../../../typechain' +import { pushOracleForward } from '../../../utils/oracles' import { bn, fp } from '../../../../common/numbers' import { MAX_UINT48 } from '../../../../common/constants' import { expect } from 'chai' @@ -119,6 +120,9 @@ export const deployCollateral = async ( ) await collateral.deployed() + // Push forward chainlink feed + await pushOracleForward(opts.chainlinkFeed!) + // sometimes we are trying to test a negative test case and we want this to fail silently // fortunately this syntax fails silently because our tools are terrible await expect(collateral.refresh()) @@ -353,6 +357,7 @@ const collateralSpecificStatusTests = () => { }) // Should remain SOUND after a 1% decrease + let refPerTok = await collateral.refPerTok() let currentExchangeRate = await wcusdcV3Mock.exchangeRate() await wcusdcV3Mock.setMockExchangeRate( true, @@ -361,7 +366,11 @@ const collateralSpecificStatusTests = () => { await collateral.refresh() expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + // refPerTok should be unchanged + expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand + // Should become DISABLED if drops more than that + refPerTok = await collateral.refPerTok() currentExchangeRate = await wcusdcV3Mock.exchangeRate() await wcusdcV3Mock.setMockExchangeRate( true, @@ -369,6 +378,10 @@ const collateralSpecificStatusTests = () => { ) await collateral.refresh() expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) + + // refPerTok should have fallen 1% + refPerTok = refPerTok.sub(refPerTok.div(100)) + expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand }) } @@ -396,6 +409,7 @@ const opts = { itChecksTargetPerRefDefault: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it.skip, // implemented in this file itIsPricedByPeg: true, resetFork, diff --git a/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts b/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts index cbbd48ed43..7e4d782570 100644 --- a/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts +++ b/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts @@ -103,12 +103,43 @@ describeFork('Wrapped CUSDCv3', () => { expect(await wcusdcV3.balanceOf(don.address)).to.eq(expectedAmount) }) + it('checks for correct approval on deposit - regression test', async () => { + await expect( + wcusdcV3.connect(don).depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + ).revertedWithCustomError(wcusdcV3, 'Unauthorized') + + // Provide approval on the wrapper + await wcusdcV3.connect(bob).allow(don.address, true) + + const expectedAmount = await wcusdcV3.convertDynamicToStatic( + await cusdcV3.balanceOf(bob.address) + ) + + // This should fail even when bob approved wcusdcv3 to spend his tokens, + // because there is no explicit approval of cUSDCv3 from bob to don, only + // approval on the wrapper + await expect( + wcusdcV3.connect(don).depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + ).to.be.revertedWithCustomError(cusdcV3, 'Unauthorized') + + // Add explicit approval of cUSDCv3 and retry + await cusdcV3.connect(bob).allow(don.address, true) + await wcusdcV3 + .connect(don) + .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + + expect(await wcusdcV3.balanceOf(bob.address)).to.eq(0) + expect(await wcusdcV3.balanceOf(charles.address)).to.eq(expectedAmount) + }) + it('deposits from a different account', async () => { expect(await wcusdcV3.balanceOf(charles.address)).to.eq(0) await expect( wcusdcV3.connect(don).depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) ).revertedWithCustomError(wcusdcV3, 'Unauthorized') - await wcusdcV3.connect(bob).connect(bob).allow(don.address, true) + + // Approval has to be on cUsdcV3, not the wrapper + await cusdcV3.connect(bob).allow(don.address, true) const expectedAmount = await wcusdcV3.convertDynamicToStatic( await cusdcV3.balanceOf(bob.address) ) @@ -623,6 +654,44 @@ describeFork('Wrapped CUSDCv3', () => { expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) }) + it('caps at balance to avoid reverts when claiming rewards (claimTo)', async () => { + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) + await advanceTime(1000) + await enableRewardsAccrual(cusdcV3) + + // Accrue multiple times + for (let i = 0; i < 10; i++) { + await advanceTime(1000) + await wcusdcV3.accrue() + } + + // Get rewards from Comet + const cometRewards = await ethers.getContractAt('ICometRewards', REWARDS) + await whileImpersonating(wcusdcV3.address, async (signer) => { + await cometRewards + .connect(signer) + .claimTo(cusdcV3.address, wcusdcV3.address, wcusdcV3.address, true) + }) + + // Accrue individual account + await wcusdcV3.accrueAccount(bob.address) + + // Due to rounding, balance is smaller that owed + const owed = await wcusdcV3.getRewardOwed(bob.address) + const bal = await compToken.balanceOf(wcusdcV3.address) + expect(owed).to.be.greaterThan(bal) + + // Should still be able to claimTo (caps at balance) + const balanceBobPrev = await compToken.balanceOf(bob.address) + await expect(wcusdcV3.connect(bob).claimTo(bob.address, bob.address)).to.emit( + wcusdcV3, + 'RewardsClaimed' + ) + + expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(balanceBobPrev) + }) + it('claims rewards and sends to claimer (claimRewards)', async () => { const compToken = await ethers.getContractAt('ERC20Mock', COMP) expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) diff --git a/test/plugins/individual-collateral/compoundv3/__snapshots__/CometTestSuite.test.ts.snap b/test/plugins/individual-collateral/compoundv3/__snapshots__/CometTestSuite.test.ts.snap index a899da3b86..d2dee358c6 100644 --- a/test/plugins/individual-collateral/compoundv3/__snapshots__/CometTestSuite.test.ts.snap +++ b/test/plugins/individual-collateral/compoundv3/__snapshots__/CometTestSuite.test.ts.snap @@ -4,26 +4,26 @@ exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting ERC20 exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting ERC20 transfer 2`] = `90521`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after full price timeout 1`] = `109063`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after full price timeout 1`] = `109052`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after full price timeout 2`] = `104326`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after full price timeout 2`] = `104315`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after hard default 1`] = `134471`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after hard default 1`] = `134449`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after hard default 2`] = `126788`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after hard default 2`] = `126766`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `109063`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `109052`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `104326`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `104315`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after soft default 1`] = `107053`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after soft default 1`] = `132572`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after soft default 2`] = `103985`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() after soft default 2`] = `126704`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() during SOUND 1`] = `126785`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() during SOUND 1`] = `126763`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() during SOUND 2`] = `126785`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() during SOUND 2`] = `126763`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() during soft default 1`] = `134336`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() during soft default 1`] = `134314`; -exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() during soft default 2`] = `127067`; +exports[`Collateral: CompoundV3USDC collateral functionality Gas Reporting refresh() during soft default 2`] = `127045`; diff --git a/test/plugins/individual-collateral/curve/collateralTests.ts b/test/plugins/individual-collateral/curve/collateralTests.ts index 2cd38cd51e..1e4fe95ab9 100644 --- a/test/plugins/individual-collateral/curve/collateralTests.ts +++ b/test/plugins/individual-collateral/curve/collateralTests.ts @@ -4,29 +4,67 @@ import { CurveCollateralTestSuiteFixtures, } from './pluginTestTypes' import { CollateralStatus } from '../pluginTestTypes' -import { ethers } from 'hardhat' -import { ERC20Mock, InvalidMockV3Aggregator } from '../../../../typechain' -import { BigNumber } from 'ethers' -import { bn, fp } from '../../../../common/numbers' -import { MAX_UINT48, ZERO_ADDRESS, ONE_ADDRESS } from '../../../../common/constants' +import hre, { ethers } from 'hardhat' +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumber, ContractFactory } from 'ethers' +import { getChainId } from '../../../../common/blockchain-utils' +import { bn, fp, toBNDecimals } from '../../../../common/numbers' +import { DefaultFixture, Fixture, getDefaultFixture, ORACLE_TIMEOUT } from '../fixtures' +import { expectInIndirectReceipt } from '../../../../common/events' +import { whileImpersonating } from '../../../utils/impersonation' +import { + MAX_UINT48, + MAX_UINT192, + MAX_UINT256, + TradeKind, + ZERO_ADDRESS, + ONE_ADDRESS, +} from '../../../../common/constants' import { expect } from 'chai' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { useEnv } from '#/utils/env' -import { expectUnpriced } from '../../../utils/oracles' +import { expectDecayedPrice, expectExactPrice, expectUnpriced } from '../../../utils/oracles' +import { + IGovParams, + IGovRoles, + IRTokenSetup, + networkConfig, +} from '../../../../common/configuration' import { advanceBlocks, advanceTime, + getLatestBlockNumber, getLatestBlockTimestamp, setNextBlockTimestamp, } from '#/test/utils/time' +import { + ERC20Mock, + FacadeWrite, + IAssetRegistry, + IERC20Metadata, + InvalidMockV3Aggregator, + MockV3Aggregator, + TestIBackingManager, + TestIBasketHandler, + TestICollateral, + TestIDeployer, + TestIMain, + TestIRevenueTrader, + TestIRToken, +} from '../../../../typechain' import snapshotGasCost from '../../../utils/snapshotGasCost' -import { IMPLEMENTATION, Implementation } from '../../../fixtures' +import { IMPLEMENTATION, Implementation, ORACLE_ERROR, PRICE_TIMEOUT } from '../../../fixtures' const describeGas = IMPLEMENTATION == Implementation.P1 && useEnv('REPORT_GAS') ? describe.only : describe.skip const describeFork = useEnv('FORK') ? describe : describe.skip +const getDescribeFork = (targetNetwork = 'mainnet') => { + return useEnv('FORK') && useEnv('FORK_NETWORK') === targetNetwork ? describe : describe.skip +} + export default function fn( fixtures: CurveCollateralTestSuiteFixtures ) { @@ -392,29 +430,49 @@ export default function fn( } }) - it('returns unpriced for 0-valued oracle', async () => { + it('decays for 0-valued oracle', async () => { + const initialPrice = await ctx.collateral.price() + + // Set price of underlyings to 0 for (const feed of ctx.feeds) { await feed.updateAnswer(0).then((e) => e.wait()) } - // (0, FIX_MAX) is returned + // Price remains same at first, though IFFY + await ctx.collateral.refresh() + await expectExactPrice(ctx.collateral.address, initialPrice) + expect(await ctx.collateral.status()).to.equal(CollateralStatus.IFFY) + + // After oracle timeout decay begins + const oracleTimeout = await ctx.collateral.oracleTimeout() + await setNextBlockTimestamp((await getLatestBlockTimestamp()) + oracleTimeout) + await advanceBlocks(1 + oracleTimeout / 12) + await ctx.collateral.refresh() + await expectDecayedPrice(ctx.collateral.address) + + // After price timeout it becomes unpriced + const priceTimeout = await ctx.collateral.priceTimeout() + await setNextBlockTimestamp((await getLatestBlockTimestamp()) + priceTimeout) + await advanceBlocks(1 + priceTimeout / 12) await expectUnpriced(ctx.collateral.address) - // When refreshed, sets status to Unpriced + // When refreshed, sets status to DISABLED await ctx.collateral.refresh() - expect(await ctx.collateral.status()).to.equal(CollateralStatus.IFFY) + expect(await ctx.collateral.status()).to.equal(CollateralStatus.DISABLED) }) it('does not revert in case of invalid timestamp', async () => { await ctx.feeds[0].setInvalidTimestamp() - // When refreshed, sets status to Unpriced + // When refreshed, sets status to IFFY await ctx.collateral.refresh() expect(await ctx.collateral.status()).to.equal(CollateralStatus.IFFY) }) - it('Handles stale price', async () => { - await advanceTime(await ctx.collateral.priceTimeout()) + it('handles stale price', async () => { + await advanceTime( + (await ctx.collateral.oracleTimeout()) + (await ctx.collateral.priceTimeout()) + ) // (0, FIX_MAX) is returned await expectUnpriced(ctx.collateral.address) @@ -424,28 +482,36 @@ export default function fn( expect(await ctx.collateral.status()).to.equal(CollateralStatus.IFFY) }) - it('decays lotPrice over priceTimeout period', async () => { - // Prices should start out equal - const p = await ctx.collateral.price() - let lotP = await ctx.collateral.lotPrice() - expect(p.length).to.equal(lotP.length) - expect(p[0]).to.equal(lotP[0]) - expect(p[1]).to.equal(lotP[1]) + it('decays price over priceTimeout period', async () => { + const savedLow = await ctx.collateral.savedLowPrice() + const savedHigh = await ctx.collateral.savedHighPrice() + // Price should start out at saved prices + await ctx.collateral.refresh() + let p = await ctx.collateral.price() + expect(p[0]).to.equal(savedLow) + expect(p[1]).to.equal(savedHigh) await advanceTime(await ctx.collateral.oracleTimeout()) // Should be roughly half, after half of priceTimeout const priceTimeout = await ctx.collateral.priceTimeout() await advanceTime(priceTimeout / 2) - lotP = await ctx.collateral.lotPrice() - expect(lotP[0]).to.be.closeTo(p[0].div(2), p[0].div(2).div(10000)) // 1 part in 10 thousand - expect(lotP[1]).to.be.closeTo(p[1].div(2), p[1].div(2).div(10000)) // 1 part in 10 thousand + p = await ctx.collateral.price() + expect(p[0]).to.be.closeTo(savedLow.div(2), p[0].div(2).div(10000)) // 1 part in 10 thousand + expect(p[1]).to.be.closeTo(savedHigh.mul(2), p[1].mul(2).div(10000)) // 1 part in 10 thousand // Should be 0 after full priceTimeout await advanceTime(priceTimeout / 2) - lotP = await ctx.collateral.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) + await expectUnpriced(ctx.collateral.address) + }) + + it('lotPrice (deprecated) is equal to price()', async () => { + const lotPrice = await ctx.collateral.lotPrice() + const price = await ctx.collateral.price() + expect(price.length).to.equal(2) + expect(lotPrice.length).to.equal(price.length) + expect(lotPrice[0]).to.equal(price[0]) + expect(lotPrice[1]).to.equal(price[1]) }) }) @@ -617,7 +683,8 @@ export default function fn( expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) expect(await ctx.collateral.whenDefault()).to.equal(MAX_UINT48) - // Decrease refPerTok by nearly 1 part in a million + // Decrease refPerTok by 1 part in a million + const refPerTok = await ctx.collateral.refPerTok() const currentExchangeRate = await ctx.curvePool.get_virtual_price() const newVirtualPrice = currentExchangeRate.sub(currentExchangeRate.div(bn('1e6'))).add(2) await ctx.curvePool.setVirtualPrice(newVirtualPrice) @@ -635,6 +702,9 @@ export default function fn( await expect(ctx.collateral.refresh()).to.emit(ctx.collateral, 'CollateralStatusChanged') expect(await ctx.collateral.status()).to.equal(CollateralStatus.DISABLED) expect(await ctx.collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) + + // refPerTok should have fallen exactly 2e-18 + expect(await ctx.collateral.refPerTok()).to.equal(refPerTok.sub(2)) }) describe('collateral-specific tests', collateralSpecificStatusTests) @@ -684,9 +754,9 @@ export default function fn( await advanceTime( (await ctx.collateral.priceTimeout()) + (await ctx.collateral.oracleTimeout()) ) - const lotP = await ctx.collateral.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) + const p = await ctx.collateral.price() + expect(p[0]).to.equal(0) + expect(p[1]).to.equal(MAX_UINT192) }) it('after hard default', async () => { @@ -708,5 +778,360 @@ export default function fn( }) }) }) + + // Only run full protocol integration tests on mainnet + // Protocol integration fixture not currently set up to deploy onto base + getDescribeFork('mainnet')('integration tests', () => { + before(resetFork) + + let ctx: X + let owner: SignerWithAddress + let addr1: SignerWithAddress + + let chainId: number + + let defaultFixture: Fixture + + let supply: BigNumber + + // Tokens/Assets + let pairedColl: TestICollateral + let pairedERC20: ERC20Mock + let collateralERC20: IERC20Metadata + let collateral: TestICollateral + + // Core Contracts + let main: TestIMain + let rToken: TestIRToken + let assetRegistry: IAssetRegistry + let backingManager: TestIBackingManager + let basketHandler: TestIBasketHandler + let rTokenTrader: TestIRevenueTrader + + let deployer: TestIDeployer + let facadeWrite: FacadeWrite + let govParams: IGovParams + let govRoles: IGovRoles + + const config = { + dist: { + rTokenDist: bn(100), // 100% RToken + rsrDist: bn(0), // 0% RSR + }, + minTradeVolume: bn('0'), // $0 + rTokenMaxTradeVolume: MAX_UINT192, // +inf + shortFreeze: bn('259200'), // 3 days + longFreeze: bn('2592000'), // 30 days + rewardRatio: bn('1069671574938'), // approx. half life of 90 days + unstakingDelay: bn('1209600'), // 2 weeks + withdrawalLeak: fp('0'), // 0%; always refresh + warmupPeriod: bn('60'), // (the delay _after_ SOUND was regained) + tradingDelay: bn('0'), // (the delay _after_ default has been confirmed) + batchAuctionLength: bn('900'), // 15 minutes + dutchAuctionLength: bn('1800'), // 30 minutes + backingBuffer: fp('0'), // 0% + maxTradeSlippage: fp('0.01'), // 1% + issuanceThrottle: { + amtRate: fp('1e6'), // 1M RToken + pctRate: fp('0.05'), // 5% + }, + redemptionThrottle: { + amtRate: fp('1e6'), // 1M RToken + pctRate: fp('0.05'), // 5% + }, + } + + interface IntegrationFixture { + ctx: X + protocol: DefaultFixture + } + + const integrationFixture: Fixture = + async function (): Promise { + return { + ctx: await loadFixture( + makeCollateralFixtureContext(owner, { maxTradeVolume: MAX_UINT192 }) + ), + protocol: await loadFixture(defaultFixture), + } + } + + before(async () => { + defaultFixture = await getDefaultFixture(collateralName) + chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + ;[, owner, addr1] = await ethers.getSigners() + }) + + beforeEach(async () => { + let protocol: DefaultFixture + ;({ ctx, protocol } = await loadFixture(integrationFixture)) + ;({ collateral } = ctx) + ;({ deployer, facadeWrite, govParams } = protocol) + + supply = fp('1') + + // Create a paired collateral of the same targetName + pairedColl = await makePairedCollateral(await collateral.targetName()) + await pairedColl.refresh() + expect(await pairedColl.status()).to.equal(CollateralStatus.SOUND) + pairedERC20 = await ethers.getContractAt('ERC20Mock', await pairedColl.erc20()) + + // Prep collateral + collateralERC20 = await ethers.getContractAt('IERC20Metadata', await collateral.erc20()) + await mintCollateralTo( + ctx, + toBNDecimals(fp('1'), await collateralERC20.decimals()), + addr1, + addr1.address + ) + + // Set primary basket + const rTokenSetup: IRTokenSetup = { + assets: [], + primaryBasket: [collateral.address, pairedColl.address], + weights: [fp('0.5e-4'), fp('0.5e-4')], + backups: [], + beneficiaries: [], + } + + // Deploy RToken via FacadeWrite + const receipt = await ( + await facadeWrite.connect(owner).deployRToken( + { + name: 'RTKN RToken', + symbol: 'RTKN', + mandate: 'mandate', + params: config, + }, + rTokenSetup + ) + ).wait() + + // Get Main + const mainAddr = expectInIndirectReceipt(receipt, deployer.interface, 'RTokenCreated').args + .main + main = await ethers.getContractAt('TestIMain', mainAddr) + + // Get core contracts + assetRegistry = ( + await ethers.getContractAt('IAssetRegistry', await main.assetRegistry()) + ) + backingManager = ( + await ethers.getContractAt('TestIBackingManager', await main.backingManager()) + ) + basketHandler = ( + await ethers.getContractAt('TestIBasketHandler', await main.basketHandler()) + ) + rToken = await ethers.getContractAt('TestIRToken', await main.rToken()) + rTokenTrader = ( + await ethers.getContractAt('TestIRevenueTrader', await main.rTokenTrader()) + ) + + // Set initial governance roles + govRoles = { + owner: owner.address, + guardian: ZERO_ADDRESS, + pausers: [], + shortFreezers: [], + longFreezers: [], + } + // Setup owner and unpause + await facadeWrite.connect(owner).setupGovernance( + rToken.address, + false, // do not deploy governance + true, // unpaused + govParams, // mock values, not relevant + govRoles + ) + + // Advance past warmup period + await setNextBlockTimestamp( + (await getLatestBlockTimestamp()) + (await basketHandler.warmupPeriod()) + ) + + // Should issue + await collateralERC20.connect(addr1).approve(rToken.address, MAX_UINT256) + await pairedERC20.connect(addr1).approve(rToken.address, MAX_UINT256) + await rToken.connect(addr1).issue(supply) + }) + + it('can be put into an RToken basket', async () => { + await assetRegistry.refresh() + expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) + }) + + it('issues', async () => { + // Issuance in beforeEach + expect(await rToken.totalSupply()).to.equal(supply) + }) + + it('redeems', async () => { + await rToken.connect(addr1).redeem(supply) + expect(await rToken.totalSupply()).to.equal(0) + const initialCollBal = toBNDecimals(fp('1'), await collateralERC20.decimals()) + expect(await collateralERC20.balanceOf(addr1.address)).to.be.closeTo( + initialCollBal, + initialCollBal.div(bn('1e5')) // 1-part-in-100k + ) + }) + + it('rebalances out of the collateral', async () => { + // Remove collateral from basket + await basketHandler.connect(owner).setPrimeBasket([pairedERC20.address], [fp('1e-4')]) + await expect(basketHandler.connect(owner).refreshBasket()) + .to.emit(basketHandler, 'BasketSet') + .withArgs(anyValue, [pairedERC20.address], [fp('1e-4')], false) + await setNextBlockTimestamp( + (await getLatestBlockTimestamp()) + config.warmupPeriod.toNumber() + ) + + // Run rebalancing auction + await expect(backingManager.rebalance(TradeKind.DUTCH_AUCTION)) + .to.emit(backingManager, 'TradeStarted') + .withArgs(anyValue, collateralERC20.address, pairedERC20.address, anyValue, anyValue) + const tradeAddr = await backingManager.trades(collateralERC20.address) + expect(tradeAddr).to.not.equal(ZERO_ADDRESS) + const trade = await ethers.getContractAt('DutchTrade', tradeAddr) + expect(await trade.sell()).to.equal(collateralERC20.address) + expect(await trade.buy()).to.equal(pairedERC20.address) + const buyAmt = await trade.bidAmount(await trade.endBlock()) + await pairedERC20.connect(addr1).approve(trade.address, buyAmt) + await advanceBlocks((await trade.endBlock()).sub(await getLatestBlockNumber()).sub(1)) + const pairedBal = await pairedERC20.balanceOf(backingManager.address) + await expect(trade.connect(addr1).bid()).to.emit(backingManager, 'TradeSettled') + expect(await pairedERC20.balanceOf(backingManager.address)).to.be.gt(pairedBal) + expect(await backingManager.tradesOpen()).to.equal(0) + }) + + it('forwards revenue and sells in a revenue auction', async () => { + // Send excess collateral to the RToken trader via forwardRevenue() + const mintAmt = toBNDecimals(fp('1e-6'), await collateralERC20.decimals()) + await mintCollateralTo( + ctx, + mintAmt.gt('150') ? mintAmt : bn('150'), + addr1, + backingManager.address + ) + await backingManager.forwardRevenue([collateralERC20.address]) + expect(await collateralERC20.balanceOf(rTokenTrader.address)).to.be.gt(0) + + // Run revenue auction + await expect( + rTokenTrader.manageTokens([collateralERC20.address], [TradeKind.DUTCH_AUCTION]) + ) + .to.emit(rTokenTrader, 'TradeStarted') + .withArgs(anyValue, collateralERC20.address, rToken.address, anyValue, anyValue) + const tradeAddr = await rTokenTrader.trades(collateralERC20.address) + expect(tradeAddr).to.not.equal(ZERO_ADDRESS) + const trade = await ethers.getContractAt('DutchTrade', tradeAddr) + expect(await trade.sell()).to.equal(collateralERC20.address) + expect(await trade.buy()).to.equal(rToken.address) + const buyAmt = await trade.bidAmount(await trade.endBlock()) + await rToken.connect(addr1).approve(trade.address, buyAmt) + await advanceBlocks((await trade.endBlock()).sub(await getLatestBlockNumber()).sub(1)) + await expect(trade.connect(addr1).bid()).to.emit(rTokenTrader, 'TradeSettled') + expect(await rTokenTrader.tradesOpen()).to.equal(0) + }) + + // === Integration Test Helpers === + + const makePairedCollateral = async (target: string): Promise => { + const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( + 'MockV3Aggregator' + ) + const chainlinkFeed: MockV3Aggregator = ( + await MockV3AggregatorFactory.deploy(8, bn('1e8')) + ) + + if (target == ethers.utils.formatBytes32String('USD')) { + // USD + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + networkConfig[chainId].tokens.USDC! + ) + await whileImpersonating('0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf', async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) + const FiatCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'FiatCollateral' + ) + return await FiatCollateralFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20.address, + maxTradeVolume: MAX_UINT192, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01'), // 1% + delayUntilDefault: bn('86400'), // 24h, + }) + } else if (target == ethers.utils.formatBytes32String('ETH')) { + // ETH + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + networkConfig[chainId].tokens.WETH! + ) + await whileImpersonating('0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E', async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) + const SelfReferentialFactory: ContractFactory = await ethers.getContractFactory( + 'SelfReferentialCollateral' + ) + return await SelfReferentialFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20.address, + maxTradeVolume: MAX_UINT192, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('ETH'), + defaultThreshold: fp('0'), // 0% + delayUntilDefault: bn('0'), // 0, + }) + } else if (target == ethers.utils.formatBytes32String('BTC')) { + // BTC + const targetUnitOracle: MockV3Aggregator = ( + await MockV3AggregatorFactory.deploy(8, bn('1e8')) + ) + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + networkConfig[chainId].tokens.WBTC! + ) + await whileImpersonating('0xccf4429db6322d5c611ee964527d42e5d685dd6a', async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) + const NonFiatFactory: ContractFactory = await ethers.getContractFactory( + 'NonFiatCollateral' + ) + return await NonFiatFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: erc20.address, + maxTradeVolume: MAX_UINT192, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('BTC'), + defaultThreshold: fp('0.01'), // 1% + delayUntilDefault: bn('86400'), // 24h, + }, + targetUnitOracle.address, + ORACLE_TIMEOUT + ) + } else { + throw new Error(`Unknown target: ${target}`) + } + } + }) }) } diff --git a/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts b/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts index 3ff406f171..bbabc4c8aa 100644 --- a/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts +++ b/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts @@ -7,13 +7,14 @@ import { import { makeWeUSDFraxBP, mintWeUSDFraxBP, resetFork } from './helpers' import { ethers } from 'hardhat' import { ContractFactory, BigNumberish } from 'ethers' -import { expectUnpriced } from '../../../../utils/oracles' +import { expectDecayedPrice, expectExactPrice, expectUnpriced } from '../../../../utils/oracles' import { ERC20Mock, MockV3Aggregator, MockV3Aggregator__factory, TestICollateral, } from '../../../../../typechain' +import { advanceTime } from '../../../../utils/time' import { bn } from '../../../../../common/numbers' import { ZERO_ADDRESS, ONE_ADDRESS, MAX_UINT192 } from '../../../../../common/constants' import { expect } from 'chai' @@ -227,17 +228,53 @@ const collateralSpecificStatusTests = () => { // Set RTokenAsset to unpriced // Would be the price under a stale oracle timeout for a poorly-coded RTokenAsset await mockRTokenAsset.setPrice(0, MAX_UINT192) + await expectExactPrice(collateral.address, initialPrice) + + // Should decay after oracle timeout + await advanceTime(await collateral.oracleTimeout()) + await expectDecayedPrice(collateral.address) + + // Should be unpriced after price timeout + await advanceTime(await collateral.priceTimeout()) + await expectUnpriced(collateral.address) // refresh() should not revert await collateral.refresh() + }) - // Should be unpriced - await expectUnpriced(collateral.address) + it('Regression test -- refreshes inner RTokenAsset on refresh()', async () => { + const [collateral] = await deployCollateral({}) + const initialPrice = await collateral.price() + expect(initialPrice[0]).to.be.gt(0) + expect(initialPrice[1]).to.be.lt(MAX_UINT192) + + // Swap out eUSD's RTokenAsset with a mock one + const AssetMockFactory = await ethers.getContractFactory('AssetMock') + const mockRTokenAsset = await AssetMockFactory.deploy( + bn('1'), // unused + ONE_ADDRESS, // unused + bn('1'), // unused + eUSD, + bn('1'), // unused + bn('1') // unused + ) + const eUSDAssetRegistry = await ethers.getContractAt( + 'IAssetRegistry', + '0x9B85aC04A09c8C813c37de9B3d563C2D3F936162' + ) + await whileImpersonating('0xc8Ee187A5e5c9dC9b42414Ddf861FFc615446a2c', async (signer) => { + await eUSDAssetRegistry.connect(signer).swapRegistered(mockRTokenAsset.address) + }) + + // Set RTokenAsset price to stale + await mockRTokenAsset.setStale(true) + expect(await mockRTokenAsset.stale()).to.be.true + + // Refresh CurveStableRTokenMetapoolCollateral + await collateral.refresh() - // Lot price should be initial price - const lotP = await collateral.lotPrice() - expect(lotP[0]).to.eq(initialPrice[0]) - expect(lotP[1]).to.eq(initialPrice[1]) + // Stale should be false again + expect(await mockRTokenAsset.stale()).to.be.false }) } diff --git a/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableMetapoolSuite.test.ts.snap b/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableMetapoolSuite.test.ts.snap index ea00035779..4e3d02729f 100644 --- a/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableMetapoolSuite.test.ts.snap +++ b/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableMetapoolSuite.test.ts.snap @@ -4,26 +4,26 @@ exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collatera exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting ERC20 Wrapper transfer 2`] = `360937`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `52705`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `52713`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `48237`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `48245`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `251539`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `251771`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `246657`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `246889`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `52705`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `52713`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `48237`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `48245`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `47896`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `79713`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `47896`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `79713`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `246654`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `246886`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `246654`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `246886`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `254250`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `254482`; -exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `246982`; +exports[`Collateral: CurveStableMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `247214`; diff --git a/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableRTokenMetapoolTestSuite.test.ts.snap b/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableRTokenMetapoolTestSuite.test.ts.snap index 7c535da1b7..b1bc4df8a7 100644 --- a/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableRTokenMetapoolTestSuite.test.ts.snap +++ b/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableRTokenMetapoolTestSuite.test.ts.snap @@ -4,26 +4,26 @@ exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper col exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting ERC20 Wrapper transfer 2`] = `385743`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `69638`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `485368`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `65170`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `480752`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `226883`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `594734`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `222001`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `589926`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `101429`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `478768`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `96961`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `474226`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `96620`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `544663`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `96620`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `536931`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `221998`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `713211`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `221998`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `713581`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `209488`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `701051`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `202220`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `693709`; diff --git a/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableTestSuite.test.ts.snap b/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableTestSuite.test.ts.snap index 5f891c6870..ffa84bf243 100644 --- a/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableTestSuite.test.ts.snap +++ b/test/plugins/individual-collateral/curve/crv/__snapshots__/CrvStableTestSuite.test.ts.snap @@ -4,26 +4,26 @@ exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functi exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting ERC20 Wrapper transfer 2`] = `369452`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `61895`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `61884`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `57427`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `57416`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `200033`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `199560`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `195151`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `194678`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `61895`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `61884`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `57427`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `57416`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `57086`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `57075`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `57086`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `57075`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `195148`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `194675`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `195148`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `194675`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `182161`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `181820`; -exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `174893`; +exports[`Collateral: CurveStableCollateral - CurveGaugeWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `174552`; diff --git a/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts b/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts index 5abe5c1ec6..9000295bb8 100644 --- a/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts +++ b/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts @@ -7,13 +7,14 @@ import { import { makeWeUSDFraxBP, mintWeUSDFraxBP, resetFork } from './helpers' import { ethers } from 'hardhat' import { ContractFactory, BigNumberish } from 'ethers' -import { expectUnpriced } from '../../../../utils/oracles' +import { expectDecayedPrice, expectExactPrice, expectUnpriced } from '../../../../utils/oracles' import { ERC20Mock, MockV3Aggregator, MockV3Aggregator__factory, TestICollateral, } from '../../../../../typechain' +import { advanceTime } from '../../../../utils/time' import { bn } from '../../../../../common/numbers' import { ZERO_ADDRESS, ONE_ADDRESS, MAX_UINT192 } from '../../../../../common/constants' import { expect } from 'chai' @@ -229,17 +230,53 @@ const collateralSpecificStatusTests = () => { // Set RTokenAsset to unpriced // Would be the price under a stale oracle timeout for a poorly-coded RTokenAsset await mockRTokenAsset.setPrice(0, MAX_UINT192) + await expectExactPrice(collateral.address, initialPrice) + + // Should decay after oracle timeout + await advanceTime(await collateral.oracleTimeout()) + await expectDecayedPrice(collateral.address) + + // Should be unpriced after price timeout + await advanceTime(await collateral.priceTimeout()) + await expectUnpriced(collateral.address) // refresh() should not revert await collateral.refresh() + }) - // Should be unpriced - await expectUnpriced(collateral.address) + it('Regression test -- refreshes inner RTokenAsset on refresh()', async () => { + const [collateral] = await deployCollateral({}) + const initialPrice = await collateral.price() + expect(initialPrice[0]).to.be.gt(0) + expect(initialPrice[1]).to.be.lt(MAX_UINT192) + + // Swap out eUSD's RTokenAsset with a mock one + const AssetMockFactory = await ethers.getContractFactory('AssetMock') + const mockRTokenAsset = await AssetMockFactory.deploy( + bn('1'), // unused + ONE_ADDRESS, // unused + bn('1'), // unused + eUSD, + bn('1'), // unused + bn('1') // unused + ) + const eUSDAssetRegistry = await ethers.getContractAt( + 'IAssetRegistry', + '0x9B85aC04A09c8C813c37de9B3d563C2D3F936162' + ) + await whileImpersonating('0xc8Ee187A5e5c9dC9b42414Ddf861FFc615446a2c', async (signer) => { + await eUSDAssetRegistry.connect(signer).swapRegistered(mockRTokenAsset.address) + }) + + // Set RTokenAsset price to stale + await mockRTokenAsset.setStale(true) + expect(await mockRTokenAsset.stale()).to.be.true + + // Refresh CurveStableRTokenMetapoolCollateral + await collateral.refresh() - // Lot price should be initial price - const lotP = await collateral.lotPrice() - expect(lotP[0]).to.eq(initialPrice[0]) - expect(lotP[1]).to.eq(initialPrice[1]) + // Stale should be false again + expect(await mockRTokenAsset.stale()).to.be.false }) } diff --git a/test/plugins/individual-collateral/curve/cvx/CvxStableTestSuite.test.ts b/test/plugins/individual-collateral/curve/cvx/CvxStableTestSuite.test.ts index c86ae829d6..8cbdd58345 100644 --- a/test/plugins/individual-collateral/curve/cvx/CvxStableTestSuite.test.ts +++ b/test/plugins/individual-collateral/curve/cvx/CvxStableTestSuite.test.ts @@ -9,7 +9,6 @@ import { ethers } from 'hardhat' import { ContractFactory, BigNumberish } from 'ethers' import { ERC20Mock, - IERC20, MockV3Aggregator, MockV3Aggregator__factory, TestICollateral, @@ -43,7 +42,6 @@ import { CRV, THREE_POOL_HOLDER, } from '../constants' -import { whileImpersonating } from '#/test/utils/impersonation' type Fixture = () => Promise @@ -413,53 +411,6 @@ const collateralSpecificStatusTests = () => { const finalRefPerTok = await multiFeedCollateral.refPerTok() expect(finalRefPerTok).to.equal(initialRefPerTok) }) - - it('handles shutdown correctly', async () => { - const fix = await makeW3PoolStable() - const [, alice, bob] = await ethers.getSigners() - const amount = fp('100') - const rewardPerBlock = bn('83197823300') - - const lpToken = ( - await ethers.getContractAt( - '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', - await fix.wrapper.curveToken() - ) - ) - const CRV = ( - await ethers.getContractAt( - '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', - '0xD533a949740bb3306d119CC777fa900bA034cd52' - ) - ) - await whileImpersonating(THREE_POOL_HOLDER, async (signer) => { - await lpToken.connect(signer).transfer(alice.address, amount.mul(2)) - }) - - await lpToken.connect(alice).approve(fix.wrapper.address, ethers.constants.MaxUint256) - await fix.wrapper.connect(alice).deposit(amount, alice.address) - - // let's shutdown! - await fix.wrapper.shutdown() - - const prevBalance = await CRV.balanceOf(alice.address) - await fix.wrapper.connect(alice).claimRewards() - expect(await CRV.balanceOf(alice.address)).to.be.eq(prevBalance.add(rewardPerBlock)) - - const prevBalanceBob = await CRV.balanceOf(bob.address) - - // transfer to bob - await fix.wrapper - .connect(alice) - .transfer(bob.address, await fix.wrapper.balanceOf(alice.address)) - - await fix.wrapper.connect(bob).claimRewards() - expect(await CRV.balanceOf(bob.address)).to.be.eq(prevBalanceBob.add(rewardPerBlock)) - - await expect(fix.wrapper.connect(alice).deposit(amount, alice.address)).to.be.reverted - await expect(fix.wrapper.connect(bob).withdraw(await fix.wrapper.balanceOf(bob.address))).to.not - .be.reverted - }) } /* diff --git a/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableMetapoolSuite.test.ts.snap b/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableMetapoolSuite.test.ts.snap index 7ccdd8462f..7920079d2a 100644 --- a/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableMetapoolSuite.test.ts.snap +++ b/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableMetapoolSuite.test.ts.snap @@ -4,26 +4,26 @@ exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collat exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting ERC20 Wrapper transfer 2`] = `172551`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `52705`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `52713`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `48237`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `48245`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `251539`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `251771`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `246657`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `246889`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `52705`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `52713`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `48237`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `48245`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `47896`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `79713`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `47896`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `79713`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `246654`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `246886`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `246654`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `246886`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `254250`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `254482`; -exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `246982`; +exports[`Collateral: CurveStableMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `247214`; diff --git a/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableRTokenMetapoolTestSuite.test.ts.snap b/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableRTokenMetapoolTestSuite.test.ts.snap index d4975bf94d..876202c6a6 100644 --- a/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableRTokenMetapoolTestSuite.test.ts.snap +++ b/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableRTokenMetapoolTestSuite.test.ts.snap @@ -4,26 +4,26 @@ exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting ERC20 Wrapper transfer 2`] = `175188`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `69638`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `485368`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `65170`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `480900`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `226883`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `594734`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `222001`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `589778`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `101429`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `478546`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `96961`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `474004`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `96620`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `544811`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `96620`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `536931`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `221998`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `713433`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `221998`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `713507`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `209488`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `701125`; -exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `202220`; +exports[`Collateral: CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `693635`; diff --git a/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableTestSuite.test.ts.snap b/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableTestSuite.test.ts.snap index 20e9558ee6..80285bbb17 100644 --- a/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableTestSuite.test.ts.snap +++ b/test/plugins/individual-collateral/curve/cvx/__snapshots__/CvxStableTestSuite.test.ts.snap @@ -4,26 +4,26 @@ exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral fun exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting ERC20 Wrapper transfer 2`] = `123705`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `61895`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 1`] = `61884`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `57427`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after full price timeout 2`] = `57416`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `200033`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 1`] = `199560`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `195151`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after hard default 2`] = `194678`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `61895`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `61884`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `57427`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `57416`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `57086`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 1`] = `57075`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `57086`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() after soft default 2`] = `57075`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `195148`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 1`] = `194675`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `195148`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during SOUND 2`] = `194675`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `182161`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 1`] = `181820`; -exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `174893`; +exports[`Collateral: CurveStableCollateral - ConvexStakingWrapper collateral functionality Gas Reporting refresh() during soft default 2`] = `174552`; diff --git a/test/plugins/individual-collateral/curve/cvx/helpers.ts b/test/plugins/individual-collateral/curve/cvx/helpers.ts index 77081254f6..a3bfbb93dc 100644 --- a/test/plugins/individual-collateral/curve/cvx/helpers.ts +++ b/test/plugins/individual-collateral/curve/cvx/helpers.ts @@ -71,14 +71,8 @@ export const makeW3PoolStable = async (): Promise => ) await curvePool.setVirtualPrice(await realCurvePool.get_virtual_price()) - // Deploy external cvxMining lib - const CvxMiningFactory = await ethers.getContractFactory('CvxMining') - const cvxMining = await CvxMiningFactory.deploy() - // Deploy Wrapper - const wrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { - libraries: { CvxMining: cvxMining.address }, - }) + const wrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper') const wrapper = await wrapperFactory.deploy() await wrapper.initialize(THREE_POOL_CVX_POOL_ID) @@ -124,14 +118,8 @@ export const makeWSUSDPoolStable = async (): Promise => { await realMetapool.balanceOf(MIM_THREE_POOL_HOLDER) ) - // Deploy external cvxMining lib - const CvxMiningFactory = await ethers.getContractFactory('CvxMining') - const cvxMining = await CvxMiningFactory.deploy() - // Deploy Wrapper - const wrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { - libraries: { CvxMining: cvxMining.address }, - }) + const wrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper') const wPool = await wrapperFactory.deploy() await wPool.initialize(MIM_THREE_POOL_POOL_ID) diff --git a/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts b/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts index 7ee5c7dc01..4d539a4a25 100644 --- a/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts @@ -12,6 +12,7 @@ import { PotMock, TestICollateral, } from '../../../../typechain' +import { pushOracleForward } from '../../../utils/oracles' import { bn, fp } from '../../../../common/numbers' import { ZERO_ADDRESS } from '../../../../common/constants' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' @@ -69,6 +70,10 @@ export const deployCollateral = async (opts: CollateralOpts = {}): Promise = () => Promise @@ -39,8 +42,7 @@ interface RSRFixture { rsr: ERC20Mock } -async function rsrFixture(): Promise { - const chainId = await getChainId(hre) +async function rsrFixture(chainId: number): Promise { const rsr: ERC20Mock = ( await ethers.getContractAt('ERC20Mock', networkConfig[chainId].tokens.RSR || '') ) @@ -72,9 +74,10 @@ export interface DefaultFixture extends RSRAndModuleFixture { export const getDefaultFixture = async function (salt: string) { const defaultFixture: Fixture = async function (): Promise { - const { rsr } = await rsrFixture() + let chainId = await getChainId(hre) + if (useEnv('FORK_NETWORK').toLowerCase() == 'base') chainId = 8453 + const { rsr } = await rsrFixture(chainId) const { gnosis } = await gnosisFixture() - const chainId = await getChainId(hre) if (!networkConfig[chainId]) { throw new Error(`Missing network configuration for ${hre.network.name}`) } diff --git a/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts b/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts index 8d0a4fe8e3..ea7c5554f2 100644 --- a/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts @@ -9,6 +9,7 @@ import { MockV3Aggregator__factory, TestICollateral, } from '../../../../typechain' +import { pushOracleForward } from '../../../utils/oracles' import { networkConfig } from '../../../../common/configuration' import { bn, fp } from '../../../../common/numbers' import { expect } from 'chai' @@ -128,6 +129,9 @@ all.forEach((curr: FTokenEnumeration) => { ) await collateral.deployed() + // Push forward chainlink feed + await pushOracleForward(opts.chainlinkFeed!) + // sometimes we are trying to test a negative test case and we want this to fail silently // fortunately this syntax fails silently because our tools are terrible await expect(collateral.refresh()) @@ -252,6 +256,7 @@ all.forEach((curr: FTokenEnumeration) => { itChecksTargetPerRefDefault: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, resetFork, collateralName: curr.testName, diff --git a/test/plugins/individual-collateral/flux-finance/__snapshots__/FTokenFiatCollateral.test.ts.snap b/test/plugins/individual-collateral/flux-finance/__snapshots__/FTokenFiatCollateral.test.ts.snap index b11dbcda01..da1cb92e3a 100644 --- a/test/plugins/individual-collateral/flux-finance/__snapshots__/FTokenFiatCollateral.test.ts.snap +++ b/test/plugins/individual-collateral/flux-finance/__snapshots__/FTokenFiatCollateral.test.ts.snap @@ -4,110 +4,110 @@ exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting ERC2 exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting ERC20 transfer 2`] = `105831`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `117361`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `117350`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `115692`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `115681`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after hard default 1`] = `140981`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after hard default 1`] = `140959`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after hard default 2`] = `139167`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after hard default 2`] = `139145`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `117361`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `117350`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `115692`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `115681`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after soft default 1`] = `115338`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after soft default 1`] = `139157`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after soft default 2`] = `115338`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() after soft default 2`] = `139083`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `139177`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `139155`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `139177`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `139155`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() during soft default 1`] = `141202`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() during soft default 1`] = `141106`; -exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() during soft default 2`] = `139459`; +exports[`Collateral: fDAI Collateral collateral functionality Gas Reporting refresh() during soft default 2`] = `139511`; exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting ERC20 transfer 1`] = `142854`; exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting ERC20 transfer 2`] = `105831`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `117553`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `117542`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `115884`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `115873`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after hard default 1`] = `141237`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after hard default 1`] = `141215`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after hard default 2`] = `139423`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after hard default 2`] = `139401`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `117553`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `117542`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `115884`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `115873`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after soft default 1`] = `115530`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after soft default 1`] = `139413`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after soft default 2`] = `115530`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() after soft default 2`] = `139339`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `139433`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `139411`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `139433`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `139411`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() during soft default 1`] = `141458`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() during soft default 1`] = `141362`; -exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() during soft default 2`] = `139789`; +exports[`Collateral: fFRAX Collateral collateral functionality Gas Reporting refresh() during soft default 2`] = `139693`; exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting ERC20 transfer 1`] = `142854`; exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting ERC20 transfer 2`] = `105831`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `125843`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `125832`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `124174`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `124163`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after hard default 1`] = `150019`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after hard default 1`] = `149997`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after hard default 2`] = `148135`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after hard default 2`] = `148183`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `125843`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `125832`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `124174`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `124163`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after soft default 1`] = `123820`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after soft default 1`] = `148121`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after soft default 2`] = `123820`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() after soft default 2`] = `148121`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `148215`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `148193`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `148145`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `148193`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() during soft default 1`] = `150096`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() during soft default 1`] = `150074`; -exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() during soft default 2`] = `148427`; +exports[`Collateral: fUSDC Collateral collateral functionality Gas Reporting refresh() during soft default 2`] = `148475`; exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting ERC20 transfer 1`] = `142854`; exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting ERC20 transfer 2`] = `105831`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `120491`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `120480`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `118822`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `118811`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after hard default 1`] = `144383`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after hard default 1`] = `144361`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after hard default 2`] = `142569`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after hard default 2`] = `142477`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `120491`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `120480`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `118822`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `118811`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after soft default 1`] = `118468`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after soft default 1`] = `142485`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after soft default 2`] = `118468`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() after soft default 2`] = `142415`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `142579`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `142487`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `142509`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `142557`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() during soft default 1`] = `144530`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() during soft default 1`] = `144438`; -exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() during soft default 2`] = `142861`; +exports[`Collateral: fUSDT Collateral collateral functionality Gas Reporting refresh() during soft default 2`] = `142839`; diff --git a/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts b/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts index d47e23919f..7ac77c01d1 100644 --- a/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts +++ b/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts @@ -12,6 +12,7 @@ import { TestICollateral, IsfrxEth, } from '../../../../typechain' +import { pushOracleForward } from '../../../utils/oracles' import { bn, fp } from '../../../../common/numbers' import { CollateralStatus } from '../../../../common/constants' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' @@ -84,6 +85,10 @@ export const deployCollateral = async (opts: CollateralOpts = {}): Promise {} // eslint-disable-next-line @typescript-eslint/no-empty-function const collateralSpecificStatusTests = () => { - it('does revenue hiding', async () => { + it('does revenue hiding correctly', async () => { const MockFactory = await ethers.getContractFactory('SfraxEthMock') const erc20 = (await MockFactory.deploy()) as SfraxEthMock let currentPPS = await (await ethers.getContractAt('IsfrxEth', SFRX_ETH)).pricePerShare() @@ -210,14 +215,24 @@ const collateralSpecificStatusTests = () => { }) // Should remain SOUND after a 1% decrease - await erc20.setPricePerShare(currentPPS.sub(currentPPS.div(100))) + let refPerTok = await collateral.refPerTok() + const newPPS = currentPPS.sub(currentPPS.div(100)) + await erc20.setPricePerShare(newPPS) await collateral.refresh() expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - // Should become DISABLED if drops more than that - await erc20.setPricePerShare(currentPPS.sub(currentPPS.div(99))) + // refPerTok should be unchanged + expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand + + // Should become DISABLED if drops another 1% + refPerTok = await collateral.refPerTok() + await erc20.setPricePerShare(newPPS.sub(newPPS.div(100))) await collateral.refresh() expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) + + // refPerTok should have fallen 1% + refPerTok = refPerTok.sub(refPerTok.div(100)) + expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand }) } @@ -244,6 +259,7 @@ const opts = { itChecksTargetPerRefDefault: it.skip, itChecksRefPerTokDefault: it.skip, itChecksPriceChanges: it, + itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it.skip, // implemnted in this file resetFork, collateralName: 'SFraxEthCollateral', diff --git a/test/plugins/individual-collateral/frax-eth/__snapshots__/SFrxEthTestSuite.test.ts.snap b/test/plugins/individual-collateral/frax-eth/__snapshots__/SFrxEthTestSuite.test.ts.snap index 30de100fa3..58a27e8317 100644 --- a/test/plugins/individual-collateral/frax-eth/__snapshots__/SFrxEthTestSuite.test.ts.snap +++ b/test/plugins/individual-collateral/frax-eth/__snapshots__/SFrxEthTestSuite.test.ts.snap @@ -4,14 +4,14 @@ exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting E exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting ERC20 transfer 2`] = `34204`; -exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `58995`; +exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() after full price timeout 1`] = `58984`; -exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `54258`; +exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() after full price timeout 2`] = `54247`; -exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `59695`; +exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `59684`; -exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `58026`; +exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `58015`; -exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `73831`; +exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() during SOUND 1`] = `73809`; -exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `73831`; +exports[`Collateral: SFraxEthCollateral collateral functionality Gas Reporting refresh() during SOUND 2`] = `73809`; diff --git a/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts b/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts index cf4038f9aa..43d09c95be 100644 --- a/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts @@ -193,6 +193,7 @@ const opts = { getExpectedPrice, itClaimsRewards: it.skip, itChecksTargetPerRefDefault: it, + itChecksNonZeroDefaultThreshold: it.skip, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itHasRevenueHiding: it.skip, diff --git a/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts b/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts index 94593665b0..1f4213ac61 100644 --- a/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts +++ b/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts @@ -12,6 +12,7 @@ import { TestICollateral, IWSTETH, } from '../../../../typechain' +import { pushOracleForward } from '../../../utils/oracles' import { bn, fp } from '../../../../common/numbers' import { ZERO_ADDRESS } from '../../../../common/constants' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' @@ -91,6 +92,11 @@ export const deployCollateral = async ( opts.targetPerRefChainlinkTimeout, { gasLimit: 2000000000 } ) + + // Push forward chainlink feed + await pushOracleForward(opts.chainlinkFeed!) + await pushOracleForward(opts.targetPerRefChainlinkFeed!) + await collateral.deployed() // sometimes we are trying to test a negative test case and we want this to fail silently // fortunately this syntax fails silently because our tools are terrible @@ -261,6 +267,7 @@ const opts = { itChecksTargetPerRefDefault: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, resetFork, collateralName: 'LidoStakedETH', diff --git a/test/plugins/individual-collateral/lido/__snapshots__/LidoStakedEthTestSuite.test.ts.snap b/test/plugins/individual-collateral/lido/__snapshots__/LidoStakedEthTestSuite.test.ts.snap index dab42bfd76..7abbfa8057 100644 --- a/test/plugins/individual-collateral/lido/__snapshots__/LidoStakedEthTestSuite.test.ts.snap +++ b/test/plugins/individual-collateral/lido/__snapshots__/LidoStakedEthTestSuite.test.ts.snap @@ -4,26 +4,26 @@ exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting ERC20 exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting ERC20 transfer 2`] = `34564`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after full price timeout 1`] = `88044`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after full price timeout 1`] = `88033`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after full price timeout 2`] = `83575`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after full price timeout 2`] = `83564`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after hard default 1`] = `132898`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after hard default 1`] = `132876`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after hard default 2`] = `125215`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after hard default 2`] = `125193`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `88044`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `88033`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `83575`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `83564`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after soft default 1`] = `83234`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after soft default 1`] = `83223`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after soft default 2`] = `83234`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() after soft default 2`] = `83223`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() during SOUND 1`] = `125212`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() during SOUND 1`] = `125190`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() during SOUND 2`] = `125212`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() during SOUND 2`] = `125190`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() during soft default 1`] = `129963`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() during soft default 1`] = `129941`; -exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() during soft default 2`] = `125494`; +exports[`Collateral: LidoStakedETH collateral functionality Gas Reporting refresh() during soft default 2`] = `125472`; diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts index 6ecee49770..5ffecb6582 100644 --- a/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts @@ -10,6 +10,7 @@ import { ethers } from 'hardhat' import collateralTests from '../collateralTests' import { getResetFork } from '../helpers' import { CollateralOpts } from '../pluginTestTypes' +import { pushOracleForward } from '../../../utils/oracles' import { DEFAULT_THRESHOLD, DELAY_UNTIL_DEFAULT, @@ -23,7 +24,7 @@ import { MorphoAaveCollateralFixtureContext, mintCollateralTo } from './mintColl import { setCode } from '@nomicfoundation/hardhat-network-helpers' import { whileImpersonating } from '#/utils/impersonation' import { whales } from '#/tasks/testing/upgrade-checker-utils/constants' -import { formatEther } from 'ethers/lib/utils' +import { advanceBlocks, advanceTime } from '#/utils/time' interface MAFiatCollateralOpts extends CollateralOpts { underlyingToken?: string @@ -34,7 +35,8 @@ interface MAFiatCollateralOpts extends CollateralOpts { const makeAaveFiatCollateralTestSuite = ( collateralName: string, - defaultCollateralOpts: MAFiatCollateralOpts + defaultCollateralOpts: MAFiatCollateralOpts, + specificTests = false ) => { const networkConfigToUse = networkConfig[31337] const deployCollateral = async (opts: MAFiatCollateralOpts = {}): Promise => { @@ -52,7 +54,6 @@ const makeAaveFiatCollateralTestSuite = ( morphoLens: networkConfigToUse.MORPHO_AAVE_LENS!, underlyingERC20: opts.underlyingToken!, poolToken: opts.poolToken!, - rewardsDistributor: networkConfigToUse.MORPHO_REWARDS_DISTRIBUTOR!, rewardToken: networkConfigToUse.tokens.MORPHO!, }) opts.erc20 = wrapperMock.address @@ -75,6 +76,9 @@ const makeAaveFiatCollateralTestSuite = ( ) await collateral.deployed() + // Push forward chainlink feed + await pushOracleForward(opts.chainlinkFeed!) + await expect(collateral.refresh()) return collateral @@ -99,7 +103,6 @@ const makeAaveFiatCollateralTestSuite = ( morphoLens: networkConfigToUse.MORPHO_AAVE_LENS!, underlyingERC20: opts.underlyingToken!, poolToken: opts.poolToken!, - rewardsDistributor: networkConfigToUse.MORPHO_REWARDS_DISTRIBUTOR!, rewardToken: networkConfigToUse.tokens.MORPHO!, }) @@ -193,7 +196,9 @@ const makeAaveFiatCollateralTestSuite = ( */ const collateralSpecificConstructorTests = () => { it('tokenised deposits can correctly claim rewards', async () => { - const morphoTokenOwner = '0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa' + const alice = hre.ethers.provider.getSigner(1) + const aliceAddress = await alice.getAddress() + const forkBlock = 17574117 const claimer = '0x05e818959c2Aa4CD05EDAe9A099c38e7Bdc377C6' const reset = getResetFork(forkBlock) @@ -206,42 +211,41 @@ const makeAaveFiatCollateralTestSuite = ( morphoLens: networkConfigToUse.MORPHO_AAVE_LENS!, underlyingERC20: defaultCollateralOpts.underlyingToken!, poolToken: defaultCollateralOpts.poolToken!, - rewardsDistributor: networkConfigToUse.MORPHO_REWARDS_DISTRIBUTOR!, rewardToken: networkConfigToUse.tokens.MORPHO!, }) + + const morphoTokenOwner = '0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa' const vaultCode = await ethers.provider.getCode(usdtVault.address) await setCode(claimer, vaultCode) const vaultWithClaimableRewards = usdtVault.attach(claimer) + await whileImpersonating(hre, morphoTokenOwner, async (signer) => { + const morphoTokenInst = await ethers.getContractAt( + 'IMorphoToken', + networkConfigToUse.tokens.MORPHO!, + signer + ) + + await morphoTokenInst + .connect(signer) + .setUserRole(vaultWithClaimableRewards.address, 0, true) + }) const erc20Factory = await ethers.getContractFactory('ERC20Mock') const underlyingERC20 = erc20Factory.attach(defaultCollateralOpts.underlyingToken!) const depositAmount = utils.parseUnits('1000', 6) - const user = hre.ethers.provider.getSigner(0) - const userAddress = await user.getAddress() - - expect( - formatEther(await vaultWithClaimableRewards.callStatic.rewardTokenBalance(userAddress)) - ).to.be.equal('0.0') - await whileImpersonating( hre, whales[defaultCollateralOpts.underlyingToken!.toLowerCase()], async (whaleSigner) => { - await underlyingERC20.connect(whaleSigner).approve(vaultWithClaimableRewards.address, 0) - await underlyingERC20 - .connect(whaleSigner) - .approve(vaultWithClaimableRewards.address, ethers.constants.MaxUint256) - await vaultWithClaimableRewards.connect(whaleSigner).mint(depositAmount, userAddress) + await underlyingERC20.connect(whaleSigner).transfer(aliceAddress, depositAmount) } ) - - expect( - formatEther( - await vaultWithClaimableRewards.callStatic.rewardTokenBalance(userAddress) - ).slice(0, '8.60295466891613'.length) - ).to.be.equal('8.60295466891613') - + await underlyingERC20.connect(alice).approve(vaultWithClaimableRewards.address, 0) + await underlyingERC20 + .connect(alice) + .approve(vaultWithClaimableRewards.address, ethers.constants.MaxUint256) + await vaultWithClaimableRewards.connect(alice).mint(depositAmount, aliceAddress) const morphoRewards = await ethers.getContractAt( 'IMorphoRewardsDistributor', networkConfigToUse.MORPHO_REWARDS_DISTRIBUTOR! @@ -261,47 +265,79 @@ const makeAaveFiatCollateralTestSuite = ( '0xea8c2ee8d43e37ceb7b0c04d59106eff88afbe3e911b656dec7caebd415ea696', ]) - expect( - formatEther( - await vaultWithClaimableRewards.callStatic.rewardTokenBalance(userAddress) - ).slice(0, '14.162082619942089'.length) - ).to.be.equal('14.162082619942089') + // sync needs to be called after a claim to start a new payout period + // new tokens will only be moved into pending after a _claimAssetRewards call + // which sync allows you to do without the other stuff that happens in claimRewards + await vaultWithClaimableRewards.sync() - // MORPHO is not a transferable token. - // POST Launch we could ask the Morpho team if our TokenVaults could get permission to transfer the MORPHO tokens. - // Otherwise owners of the TokenVault shares need to wait until the protocol enables the transfer function on the MORPHO token. + await advanceTime(hre, 86400 * 7) + await advanceBlocks(hre, 7200 * 7) + expect(await vaultWithClaimableRewards.connect(alice).claimRewards()) + expect( + await erc20Factory.attach(networkConfigToUse.tokens.MORPHO!).balanceOf(aliceAddress) + ).to.be.eq(bn('14162082619942089266')) + }) + it('Frontrunning claiming rewards is not economical', async () => { + const alice = hre.ethers.provider.getSigner(1) + const aliceAddress = await alice.getAddress() + const bob = hre.ethers.provider.getSigner(2) + const bobAddress = await bob.getAddress() - await whileImpersonating(hre, morphoTokenOwner, async (signer) => { - const morphoTokenInst = await ethers.getContractAt( - 'IMorphoToken', - networkConfigToUse.tokens.MORPHO!, - signer - ) + const MorphoTokenisedDepositFactory = await ethers.getContractFactory( + 'MorphoAaveV2TokenisedDeposit' + ) + const ERC20Factory = await ethers.getContractFactory('ERC20Mock') + const mockRewardsToken = await ERC20Factory.deploy('MockMorphoReward', 'MMrp') + const underlyingERC20 = ERC20Factory.attach(defaultCollateralOpts.underlyingToken!) - await morphoTokenInst - .connect(signer) - .setUserRole(vaultWithClaimableRewards.address, 0, true) + const vault = await MorphoTokenisedDepositFactory.deploy({ + morphoController: networkConfigToUse.MORPHO_AAVE_CONTROLLER!, + morphoLens: networkConfigToUse.MORPHO_AAVE_LENS!, + underlyingERC20: defaultCollateralOpts.underlyingToken!, + poolToken: defaultCollateralOpts.poolToken!, + rewardToken: mockRewardsToken.address, }) - const morphoTokenInst = await ethers.getContractAt( - 'IMorphoToken', - networkConfigToUse.tokens.MORPHO!, - user + const depositAmount = utils.parseUnits('1000', 6) + + await whileImpersonating( + hre, + whales[defaultCollateralOpts.underlyingToken!.toLowerCase()], + async (whaleSigner) => { + await underlyingERC20.connect(whaleSigner).transfer(aliceAddress, depositAmount) + await underlyingERC20.connect(whaleSigner).transfer(bobAddress, depositAmount.mul(10)) + } ) - expect(formatEther(await morphoTokenInst.balanceOf(userAddress))).to.be.equal('0.0') - await vaultWithClaimableRewards.claimRewards() + await underlyingERC20.connect(alice).approve(vault.address, ethers.constants.MaxUint256) + await vault.connect(alice).mint(depositAmount, aliceAddress) - expect( - formatEther(await vaultWithClaimableRewards.callStatic.rewardTokenBalance(userAddress)) - ).to.be.equal('0.0') + // Simulate inflation attack + await underlyingERC20.connect(bob).approve(vault.address, ethers.constants.MaxUint256) + await vault.connect(bob).mint(depositAmount.mul(10), bobAddress) - expect( - formatEther(await morphoTokenInst.balanceOf(userAddress)).slice( - 0, - '14.162082619942089'.length - ) - ).to.be.equal('14.162082619942089') + await mockRewardsToken.mint(vault.address, bn('1000000000000000000000')) + await vault.sync() + + await vault.connect(bob).claimRewards() + await vault.connect(bob).redeem(depositAmount.mul(10), bobAddress, bobAddress) + + // After the inflation attack + await advanceTime(hre, 86400 * 7) + await advanceBlocks(hre, 7200 * 7) + await vault.connect(alice).claimRewards() + + // Shown below is that it is no longer economical to inflate own shares + // bob only managed to steal approx 1/7200 * 90% of the reward because hardhat increments block by 1 + // in practise it would be 0 as inflation attacks typically flashloan assets. + expect(await mockRewardsToken.balanceOf(aliceAddress)).to.be.closeTo( + bn('999996993746993746995'), + bn('1e15') + ) + expect(await mockRewardsToken.balanceOf(bobAddress)).to.be.closeTo( + bn('1503126503126502'), + bn('1e12') + ) }) } @@ -312,7 +348,9 @@ const makeAaveFiatCollateralTestSuite = ( const opts = { deployCollateral, - collateralSpecificConstructorTests: collateralSpecificConstructorTests, + collateralSpecificConstructorTests: specificTests + ? collateralSpecificConstructorTests + : () => void 0, collateralSpecificStatusTests, beforeEachRewardsTest, makeCollateralFixtureContext, @@ -326,6 +364,7 @@ const makeAaveFiatCollateralTestSuite = ( itChecksTargetPerRefDefault: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, resetFork: getResetFork(FORK_BLOCK), collateralName, @@ -364,7 +403,8 @@ const makeOpts = ( const { tokens, chainlinkFeeds } = networkConfig[31337] makeAaveFiatCollateralTestSuite( 'MorphoAAVEV2FiatCollateral - USDT', - makeOpts(tokens.USDT!, tokens.aUSDT!, chainlinkFeeds.USDT!) + makeOpts(tokens.USDT!, tokens.aUSDT!, chainlinkFeeds.USDT!), + true // Only run specific tests once, since they are slow ) makeAaveFiatCollateralTestSuite( 'MorphoAAVEV2FiatCollateral - USDC', diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts index c745f73174..28614aff7d 100644 --- a/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts @@ -15,6 +15,7 @@ import { ethers } from 'hardhat' import collateralTests from '../collateralTests' import { getResetFork } from '../helpers' import { CollateralOpts } from '../pluginTestTypes' +import { pushOracleForward } from '../../../utils/oracles' import { DEFAULT_THRESHOLD, DELAY_UNTIL_DEFAULT, @@ -53,7 +54,6 @@ const makeAaveNonFiatCollateralTestSuite = ( morphoLens: configToUse.MORPHO_AAVE_LENS!, underlyingERC20: opts.underlyingToken!, poolToken: opts.poolToken!, - rewardsDistributor: configToUse.MORPHO_REWARDS_DISTRIBUTOR!, rewardToken: configToUse.tokens.MORPHO!, }) opts.erc20 = wrapperMock.address @@ -77,6 +77,10 @@ const makeAaveNonFiatCollateralTestSuite = ( )) as unknown as TestICollateral await collateral.deployed() + // Push forward chainlink feed + await pushOracleForward(opts.chainlinkFeed!) + await pushOracleForward(opts.targetPrRefFeed!) + await expect(collateral.refresh()) return collateral @@ -100,7 +104,6 @@ const makeAaveNonFiatCollateralTestSuite = ( morphoLens: configToUse.MORPHO_AAVE_LENS!, underlyingERC20: opts.underlyingToken!, poolToken: opts.poolToken!, - rewardsDistributor: configToUse.MORPHO_REWARDS_DISTRIBUTOR!, rewardToken: configToUse.tokens.MORPHO!, }) @@ -146,18 +149,18 @@ const makeAaveNonFiatCollateralTestSuite = ( ctx: MorphoAaveCollateralFixtureContext, pctDecrease: BigNumberish ) => { - const lastRound = await ctx.targetPrRefFeed!.latestRoundData() + const lastRound = await ctx.chainlinkFeed!.latestRoundData() const nextAnswer = lastRound.answer.sub(lastRound.answer.mul(pctDecrease).div(100)) - await ctx.targetPrRefFeed!.updateAnswer(nextAnswer) + await ctx.chainlinkFeed!.updateAnswer(nextAnswer) } const increaseTargetPerRef = async ( ctx: MorphoAaveCollateralFixtureContext, pctIncrease: BigNumberish ) => { - const lastRound = await ctx.targetPrRefFeed!.latestRoundData() + const lastRound = await ctx.chainlinkFeed!.latestRoundData() const nextAnswer = lastRound.answer.add(lastRound.answer.mul(pctIncrease).div(100)) - await ctx.targetPrRefFeed!.updateAnswer(nextAnswer) + await ctx.chainlinkFeed!.updateAnswer(nextAnswer) } const changeRefPerTok = async ( @@ -168,25 +171,17 @@ const makeAaveNonFiatCollateralTestSuite = ( await ctx.morphoWrapper.setExchangeRate(rate.add(rate.mul(percentChange).div(bn('100')))) } - // prettier-ignore const reduceRefPerTok = async ( ctx: MorphoAaveCollateralFixtureContext, pctDecrease: BigNumberish ) => { - await changeRefPerTok( - ctx, - bn(pctDecrease).mul(-1) - ) + await changeRefPerTok(ctx, bn(pctDecrease).mul(-1)) } - // prettier-ignore const increaseRefPerTok = async ( ctx: MorphoAaveCollateralFixtureContext, pctIncrease: BigNumberish ) => { - await changeRefPerTok( - ctx, - bn(pctIncrease) - ) + await changeRefPerTok(ctx, bn(pctIncrease)) } const getExpectedPrice = async (ctx: MorphoAaveCollateralFixtureContext): Promise => { @@ -196,11 +191,12 @@ const makeAaveNonFiatCollateralTestSuite = ( const clRptData = await ctx.targetPrRefFeed!.latestRoundData() const clRptDecimals = await ctx.targetPrRefFeed!.decimals() - const expctPrice = clData.answer - .mul(bn(10).pow(18 - clDecimals)) - .mul(clRptData.answer.mul(bn(10).pow(18 - clRptDecimals))) + const expectedPrice = clRptData.answer + .mul(bn(10).pow(18 - clRptDecimals)) + .mul(clData.answer.mul(bn(10).pow(18 - clDecimals))) .div(fp('1')) - return expctPrice + + return expectedPrice } /* @@ -212,6 +208,7 @@ const makeAaveNonFiatCollateralTestSuite = ( // eslint-disable-next-line @typescript-eslint/no-empty-function const collateralSpecificStatusTests = () => {} + // eslint-disable-next-line @typescript-eslint/no-empty-function const beforeEachRewardsTest = async () => {} const opts = { @@ -230,6 +227,7 @@ const makeAaveNonFiatCollateralTestSuite = ( itChecksTargetPerRefDefault: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, itIsPricedByPeg: true, resetFork: getResetFork(FORK_BLOCK), @@ -248,17 +246,17 @@ makeAaveNonFiatCollateralTestSuite('MorphoAAVEV2NonFiatCollateral - WBTC', { underlyingToken: configToUse.tokens.WBTC!, poolToken: configToUse.tokens.aWBTC!, priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: configToUse.chainlinkFeeds.BTC!, - targetPrRefFeed: configToUse.chainlinkFeeds.WBTC!, + chainlinkFeed: configToUse.chainlinkFeeds.WBTC!, + targetPrRefFeed: configToUse.chainlinkFeeds.BTC!, oracleTimeout: ORACLE_TIMEOUT, + refPerTokChainlinkTimeout: ORACLE_TIMEOUT.div(24), oracleError: ORACLE_ERROR, maxTradeVolume: fp('1e6'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, revenueHiding: fp('0'), - defaultPrice: parseUnits('30000', 8), - defaultRefPerTok: parseUnits('1', 8), - refPerTokChainlinkTimeout: PRICE_TIMEOUT, + defaultPrice: parseUnits('1', 8), + defaultRefPerTok: parseUnits('30000', 8), }) makeAaveNonFiatCollateralTestSuite('MorphoAAVEV2NonFiatCollateral - stETH', { @@ -266,15 +264,15 @@ makeAaveNonFiatCollateralTestSuite('MorphoAAVEV2NonFiatCollateral - stETH', { underlyingToken: configToUse.tokens.stETH!, poolToken: configToUse.tokens.astETH!, priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: configToUse.chainlinkFeeds.ETH!, - targetPrRefFeed: configToUse.chainlinkFeeds.stETHETH!, + chainlinkFeed: configToUse.chainlinkFeeds.stETHETH!, + targetPrRefFeed: configToUse.chainlinkFeeds.ETH!, oracleTimeout: ORACLE_TIMEOUT, + refPerTokChainlinkTimeout: ORACLE_TIMEOUT.div(24), oracleError: ORACLE_ERROR, maxTradeVolume: fp('1e6'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, revenueHiding: fp('0'), - defaultPrice: parseUnits('1800', 8), - defaultRefPerTok: parseUnits('1', 8), - refPerTokChainlinkTimeout: PRICE_TIMEOUT, + defaultPrice: parseUnits('1', 8), + defaultRefPerTok: parseUnits('1800', 8), }) diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts index 16dd346ae7..4bf7730685 100644 --- a/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts @@ -15,6 +15,7 @@ import { ethers } from 'hardhat' import collateralTests from '../collateralTests' import { getResetFork } from '../helpers' import { CollateralOpts } from '../pluginTestTypes' +import { pushOracleForward } from '../../../utils/oracles' import { DELAY_UNTIL_DEFAULT, FORK_BLOCK, @@ -48,7 +49,6 @@ const deployCollateral = async (opts: MAFiatCollateralOpts = {}): Promise { - return formatUnits( - await instances.tokenVault - .connect(owner) - .callStatic.rewardTokenBalance(await owner.getAddress()), - 18 - ) - }, claimRewards: async (owner: Signer) => { await instances.tokenVault.connect(owner).claimRewards() }, @@ -179,7 +175,8 @@ const execTestForToken = ({ type ITestContext = ReturnType extends Promise ? U : never let context: ITestContext - // const resetFork = getResetFork(17591000) + before(getResetFork(FORK_BLOCK)) + beforeEach(async () => { context = await loadFixture(beforeEachFn) }) @@ -297,9 +294,162 @@ const execTestForToken = ({ expect(postWithdrawalBalance).lt(parseFloat(orignalBalance)) }) - /** - * There is a test for claiming rewards in the MorphoAAVEFiatCollateral.test.ts - */ + it('linearly distributes rewards', async () => { + const { + users: { alice, bob, charlie }, + methods, + instances, + amountBN, + } = context + + await methods.deposit(bob, '1') + + // Enable transfers on Morpho + // ugh + await whileImpersonating( + '0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa', + async (whaleSigner) => { + await whaleSigner.sendTransaction({ + to: '0x9994e35db50125e0df82e4c2dde62496ce330999', + data: '0x4b5159daa9059cbb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + }) + await whaleSigner.sendTransaction({ + to: '0x9994e35db50125e0df82e4c2dde62496ce330999', + data: '0x4b5159da23b872dd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + }) + } + ) + + // Let's drop 700 MORPHO to the tokenVault + await whileImpersonating( + '0x6c27114E34173F8E4E7F4060a51EEb1f0120E241', + async (whaleSigner) => { + await instances.morpho + .connect(whaleSigner) + .transfer( + instances.tokenVault.address, + parseUnits('700', await instances.morpho.decimals()) + ) + } + ) + + // Account for rewards + await instances.tokenVault.sync() + + // Simulate 8 days.. + for (let i = 0; i < 8; i++) { + await advanceTime(hre, 24 * 60 * 60 - 1) + await methods.claimRewards(bob) + + if (i < 7) { + expect(await instances.morpho.balanceOf(await bob.getAddress())).to.be.closeTo( + BigNumber.from(i + 1) + .mul(100) + .mul(BigNumber.from(10).pow(await instances.morpho.decimals())), + bn('1e18') + ) + } else { + expect(await instances.morpho.balanceOf(await bob.getAddress())).to.be.closeTo( + BigNumber.from(7) + .mul(100) + .mul(BigNumber.from(10).pow(await instances.morpho.decimals())), + bn('1e18') + ) + } + } + }) + + it('linearly distributes rewards, even with multiple claims', async () => { + const { + users: { alice, bob, charlie }, + methods, + instances, + amountBN, + } = context + + await methods.deposit(bob, '1') + + // Enable transfers on Morpho + // ugh + await whileImpersonating( + '0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa', + async (whaleSigner) => { + await whaleSigner.sendTransaction({ + to: '0x9994e35db50125e0df82e4c2dde62496ce330999', + data: '0x4b5159daa9059cbb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + }) + await whaleSigner.sendTransaction({ + to: '0x9994e35db50125e0df82e4c2dde62496ce330999', + data: '0x4b5159da23b872dd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + }) + } + ) + + // Let's drop 700 MORPHO to the tokenVault + await whileImpersonating( + '0x6c27114E34173F8E4E7F4060a51EEb1f0120E241', + async (whaleSigner) => { + await instances.morpho + .connect(whaleSigner) + .transfer( + instances.tokenVault.address, + parseUnits('700', await instances.morpho.decimals()) + ) + } + ) + + // Account for rewards + await instances.tokenVault.sync() + + // Simulate 3 days.. + for (let i = 0; i < 3; i++) { + await advanceTime(hre, 24 * 60 * 60 - 1) + await methods.claimRewards(bob) + + expect(await instances.morpho.balanceOf(await bob.getAddress())).to.be.closeTo( + BigNumber.from(i + 1) + .mul(100) + .mul(BigNumber.from(10).pow(await instances.morpho.decimals())), + bn('1e18') + ) + } + + // Let's drop another 300 MORPHO to the tokenVault + await whileImpersonating( + '0x6c27114E34173F8E4E7F4060a51EEb1f0120E241', + async (whaleSigner) => { + await instances.morpho + .connect(whaleSigner) + .transfer( + instances.tokenVault.address, + parseUnits('300', await instances.morpho.decimals()) + ) + } + ) + + // Account for rewards + await instances.tokenVault.sync() + + for (let i = 3; i < 10; i++) { + await advanceTime(hre, 24 * 60 * 60 - 1) + await methods.claimRewards(bob) + + // console.log( + // 'MORPHO:', + // formatUnits( + // await instances.morpho.balanceOf(await bob.getAddress()), + // await instances.morpho.decimals() + // ) + // ) + + expect(await instances.morpho.balanceOf(await bob.getAddress())).to.be.closeTo( + BigNumber.from(i + 1) + .mul(100) + .mul(BigNumber.from(10).pow(await instances.morpho.decimals())), + bn('1e18') + ) + } + }) }) } diff --git a/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVEFiatCollateral.test.ts.snap b/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVEFiatCollateral.test.ts.snap index d55a1ae734..99f876bb27 100644 --- a/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVEFiatCollateral.test.ts.snap +++ b/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVEFiatCollateral.test.ts.snap @@ -4,82 +4,94 @@ exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality G exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting ERC20 transfer 2`] = `56781`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after full price timeout 1`] = `134222`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting ERC20 transfer 3`] = `88981`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after full price timeout 2`] = `129753`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting ERC20 transfer 4`] = `71881`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after hard default 1`] = `179834`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after full price timeout 1`] = `134211`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after hard default 2`] = `172151`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after full price timeout 2`] = `129742`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `134222`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after hard default 1`] = `179812`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `129753`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after hard default 2`] = `172129`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after soft default 1`] = `129412`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `134211`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after soft default 2`] = `129412`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `129742`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() during SOUND 1`] = `172148`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after soft default 1`] = `172067`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() during SOUND 2`] = `172148`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() after soft default 2`] = `172067`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() during soft default 1`] = `179699`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() during SOUND 1`] = `172126`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() during soft default 2`] = `172430`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() during SOUND 2`] = `172126`; + +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() during soft default 1`] = `179677`; + +exports[`Collateral: MorphoAAVEV2FiatCollateral - DAI collateral functionality Gas Reporting refresh() during soft default 2`] = `172408`; exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting ERC20 transfer 1`] = `73881`; exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting ERC20 transfer 2`] = `56781`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after full price timeout 1`] = `134425`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting ERC20 transfer 3`] = `88981`; + +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting ERC20 transfer 4`] = `71881`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after full price timeout 2`] = `129956`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after full price timeout 1`] = `134414`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after hard default 1`] = `180240`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after full price timeout 2`] = `129945`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after hard default 2`] = `172557`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after hard default 1`] = `180218`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `134425`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after hard default 2`] = `172535`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `129956`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `134414`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after soft default 1`] = `129615`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `129945`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after soft default 2`] = `129615`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after soft default 1`] = `172473`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() during SOUND 1`] = `172554`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() after soft default 2`] = `172473`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() during SOUND 2`] = `172554`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() during SOUND 1`] = `172532`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() during soft default 1`] = `180105`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() during SOUND 2`] = `172532`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() during soft default 2`] = `172836`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() during soft default 1`] = `180083`; + +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDC collateral functionality Gas Reporting refresh() during soft default 2`] = `172814`; exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting ERC20 transfer 1`] = `73881`; exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting ERC20 transfer 2`] = `56781`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after full price timeout 1`] = `133578`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting ERC20 transfer 3`] = `88981`; + +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting ERC20 transfer 4`] = `71881`; + +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after full price timeout 1`] = `133567`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after full price timeout 2`] = `129109`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after full price timeout 2`] = `129098`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after hard default 1`] = `178546`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after hard default 1`] = `178524`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after hard default 2`] = `170863`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after hard default 2`] = `170841`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `133578`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `133567`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `129109`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `129098`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after soft default 1`] = `128768`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after soft default 1`] = `170779`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after soft default 2`] = `128768`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() after soft default 2`] = `170779`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() during SOUND 1`] = `170860`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() during SOUND 1`] = `170838`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() during SOUND 2`] = `170860`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() during SOUND 2`] = `170838`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() during soft default 1`] = `178411`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() during soft default 1`] = `178389`; -exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() during soft default 2`] = `171142`; +exports[`Collateral: MorphoAAVEV2FiatCollateral - USDT collateral functionality Gas Reporting refresh() during soft default 2`] = `171120`; diff --git a/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVENonFiatCollateral.test.ts.snap b/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVENonFiatCollateral.test.ts.snap index de69e7ba61..26e77e6a88 100644 --- a/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVENonFiatCollateral.test.ts.snap +++ b/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVENonFiatCollateral.test.ts.snap @@ -4,54 +4,54 @@ exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionali exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting ERC20 transfer 2`] = `56781`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after full price timeout 1`] = `133645`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after full price timeout 1`] = `133634`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after full price timeout 2`] = `129176`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after full price timeout 2`] = `129165`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after hard default 1`] = `199843`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after hard default 1`] = `199810`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after hard default 2`] = `192160`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after hard default 2`] = `192127`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `182889`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `182878`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `178420`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `178409`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after soft default 1`] = `178079`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after soft default 1`] = `192065`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after soft default 2`] = `178079`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() after soft default 2`] = `192065`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() during SOUND 1`] = `192157`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() during SOUND 1`] = `192124`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() during SOUND 2`] = `192157`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() during SOUND 2`] = `192124`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() during soft default 1`] = `199708`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() during soft default 1`] = `199675`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() during soft default 2`] = `192439`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - WBTC collateral functionality Gas Reporting refresh() during soft default 2`] = `192406`; exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting ERC20 transfer 1`] = `73881`; exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting ERC20 transfer 2`] = `56781`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after full price timeout 1`] = `167277`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after full price timeout 1`] = `167266`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after full price timeout 2`] = `162808`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after full price timeout 2`] = `162797`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after hard default 1`] = `239107`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after hard default 1`] = `239074`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after hard default 2`] = `231424`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after hard default 2`] = `231391`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `222153`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `222142`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `217684`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `217673`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after soft default 1`] = `217343`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after soft default 1`] = `231329`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after soft default 2`] = `217343`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() after soft default 2`] = `231329`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() during SOUND 1`] = `231421`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() during SOUND 1`] = `231388`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() during SOUND 2`] = `231421`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() during SOUND 2`] = `231388`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() during soft default 1`] = `238972`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() during soft default 1`] = `238939`; -exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() during soft default 2`] = `231703`; +exports[`Collateral: MorphoAAVEV2NonFiatCollateral - stETH collateral functionality Gas Reporting refresh() during soft default 2`] = `231670`; diff --git a/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVESelfReferentialCollateral.test.ts.snap b/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVESelfReferentialCollateral.test.ts.snap index 9148485dab..a420cba2b6 100644 --- a/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVESelfReferentialCollateral.test.ts.snap +++ b/test/plugins/individual-collateral/morpho-aave/__snapshots__/MorphoAAVESelfReferentialCollateral.test.ts.snap @@ -4,18 +4,18 @@ exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral fun exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting ERC20 transfer 2`] = `56781`; -exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after full price timeout 1`] = `201563`; +exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after full price timeout 1`] = `201552`; -exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after full price timeout 2`] = `197094`; +exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after full price timeout 2`] = `197083`; -exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after hard default 1`] = `217785`; +exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after hard default 1`] = `217763`; -exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after hard default 2`] = `210102`; +exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after hard default 2`] = `210080`; -exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `201563`; +exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after oracle timeout 1`] = `201552`; -exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `197094`; +exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() after oracle timeout 2`] = `197083`; -exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() during SOUND 1`] = `210099`; +exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() during SOUND 1`] = `210077`; -exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() during SOUND 2`] = `210099`; +exports[`Collateral: MorphoAAVEV2SelfReferentialCollateral - WETH collateral functionality Gas Reporting refresh() during SOUND 2`] = `210077`; diff --git a/test/plugins/individual-collateral/pluginTestTypes.ts b/test/plugins/individual-collateral/pluginTestTypes.ts index 11b73fbaa1..34bfefbb20 100644 --- a/test/plugins/individual-collateral/pluginTestTypes.ts +++ b/test/plugins/individual-collateral/pluginTestTypes.ts @@ -100,6 +100,9 @@ export interface CollateralTestSuiteFixtures // toggle on or off: tests that focus on revenue hiding (off if plugin does not hide revenue) itHasRevenueHiding: Mocha.TestFunction | Mocha.PendingTestFunction + // toggle on or off: tests that check that defaultThreshold is not zero + itChecksNonZeroDefaultThreshold: Mocha.TestFunction | Mocha.PendingTestFunction + // does the peg price matter for the results of tryPrice()? itIsPricedByPeg?: boolean diff --git a/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts b/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts index 7b2cd8e9a5..d10488770e 100644 --- a/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts @@ -12,6 +12,7 @@ import { IReth, WETH9, } from '../../../../typechain' +import { pushOracleForward } from '../../../utils/oracles' import { bn, fp } from '../../../../common/numbers' import { ZERO_ADDRESS } from '../../../../common/constants' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' @@ -89,6 +90,11 @@ export const deployCollateral = async (opts: RethCollateralOpts = {}): Promise { oracleTimeouts: opts.oracleTimeouts, oracleErrors: opts.oracleErrors, lpToken: opts.lpToken, - } + }, + PRICE_PER_SHARE_HELPER ) await collateral.deployed() diff --git a/test/plugins/individual-collateral/yearnv2/constants.ts b/test/plugins/individual-collateral/yearnv2/constants.ts index 832ccbb813..2d480c5cb4 100644 --- a/test/plugins/individual-collateral/yearnv2/constants.ts +++ b/test/plugins/individual-collateral/yearnv2/constants.ts @@ -9,6 +9,8 @@ export const yvCurveUSDCcrvUSD = networkConfig['31337'].tokens.yvCurveUSDCcrvUSD export const USDP_USD_FEED = networkConfig['31337'].chainlinkFeeds.USDP as string export const CRV_USD_USD_FEED = networkConfig['31337'].chainlinkFeeds.crvUSD as string +export const PRICE_PER_SHARE_HELPER = '0x444443bae5bB8640677A8cdF94CB8879Fec948Ec' + export const YVUSDC_LP_TOKEN = '0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E' export const YVUSDP_LP_TOKEN = '0xCa978A0528116DDA3cbA9ACD3e68bc6191CA53D0' diff --git a/test/scenario/BadCollateralPlugin.test.ts b/test/scenario/BadCollateralPlugin.test.ts index ec2e04c0ee..9745c962b5 100644 --- a/test/scenario/BadCollateralPlugin.test.ts +++ b/test/scenario/BadCollateralPlugin.test.ts @@ -27,7 +27,7 @@ import { defaultFixtureNoBasket, IMPLEMENTATION, ORACLE_ERROR, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, REVENUE_HIDING, } from '../fixtures' @@ -104,7 +104,7 @@ describe(`Bad Collateral Plugin - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR, erc20: token0.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, diff --git a/test/scenario/ComplexBasket.test.ts b/test/scenario/ComplexBasket.test.ts index f55d5a3652..6b7479212c 100644 --- a/test/scenario/ComplexBasket.test.ts +++ b/test/scenario/ComplexBasket.test.ts @@ -34,7 +34,7 @@ import { defaultFixtureNoBasket, IMPLEMENTATION, ORACLE_ERROR, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, REVENUE_HIDING, } from '../fixtures' @@ -172,7 +172,7 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { ORACLE_ERROR, rsr.address, MAX_TRADE_VOLUME, - ORACLE_TIMEOUT + ORACLE_TIMEOUT_PRE_BUFFER ) ) await assetRegistry.connect(owner).swapRegistered(newRSRAsset.address) @@ -203,7 +203,7 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR.toString(), tokenAddress: usdToken.address, // DAI Token maxTradeVolume: MAX_TRADE_VOLUME.toString(), - oracleTimeout: ORACLE_TIMEOUT.toString(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER.toString(), targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD.toString(), delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), @@ -227,8 +227,8 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR.toString(), tokenAddress: eurToken.address, maxTradeVolume: MAX_TRADE_VOLUME.toString(), - oracleTimeout: ORACLE_TIMEOUT.toString(), - targetUnitOracleTimeout: ORACLE_TIMEOUT.toString(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER.toString(), + targetUnitOracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER.toString(), targetName: ethers.utils.formatBytes32String('EUR'), defaultThreshold: DEFAULT_THRESHOLD.toString(), delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), @@ -248,7 +248,7 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR.toString(), cToken: cUSDTokenVault.address, maxTradeVolume: MAX_TRADE_VOLUME.toString(), - oracleTimeout: ORACLE_TIMEOUT.toString(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER.toString(), targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD.toString(), delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), @@ -269,7 +269,7 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR.toString(), staticAToken: aUSDToken.address, maxTradeVolume: MAX_TRADE_VOLUME.toString(), - oracleTimeout: ORACLE_TIMEOUT.toString(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER.toString(), targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD.toString(), delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), @@ -293,8 +293,8 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { combinedOracleError: ORACLE_ERROR.toString(), tokenAddress: wbtc.address, maxTradeVolume: MAX_TRADE_VOLUME.toString(), - oracleTimeout: ORACLE_TIMEOUT.toString(), - targetUnitOracleTimeout: ORACLE_TIMEOUT.toString(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER.toString(), + targetUnitOracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER.toString(), targetName: ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD.toString(), delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), @@ -323,8 +323,8 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { combinedOracleError: ORACLE_ERROR.toString(), cToken: cWBTCVault.address, maxTradeVolume: MAX_TRADE_VOLUME.toString(), - oracleTimeout: ORACLE_TIMEOUT.toString(), - targetUnitOracleTimeout: ORACLE_TIMEOUT.toString(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER.toString(), + targetUnitOracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER.toString(), targetName: hre.ethers.utils.formatBytes32String('BTC'), defaultThreshold: DEFAULT_THRESHOLD.toString(), delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), @@ -349,7 +349,7 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR.toString(), tokenAddress: weth.address, maxTradeVolume: MAX_TRADE_VOLUME.toString(), - oracleTimeout: ORACLE_TIMEOUT.toString(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER.toString(), targetName: hre.ethers.utils.formatBytes32String('ETH'), noOutput: true, }) @@ -380,7 +380,7 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR.toString(), cToken: cETHVault.address, maxTradeVolume: MAX_TRADE_VOLUME.toString(), - oracleTimeout: ORACLE_TIMEOUT.toString(), + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER.toString(), targetName: hre.ethers.utils.formatBytes32String('ETH'), revenueHiding: REVENUE_HIDING.toString(), referenceERC20Decimals: bn(18).toString(), @@ -1598,8 +1598,8 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { // Running auctions will trigger recollateralization - cETHVault partial sale for weth // Will sell about 841K of cETHVault, expect to receive 8167 wETH (minimum) // We would still have about 438K to sell of cETHVault - let [, lotHigh] = await cETHVaultCollateral.lotPrice() - const sellAmtUnscaled = MAX_TRADE_VOLUME.mul(BN_SCALE_FACTOR).div(lotHigh) + let [low] = await cETHVaultCollateral.price() + const sellAmtUnscaled = MAX_TRADE_VOLUME.mul(BN_SCALE_FACTOR).div(low) const sellAmt = toBNDecimals(sellAmtUnscaled, 8) const sellAmtRemainder = (await cETHVault.balanceOf(backingManager.address)).sub(sellAmt) // Price for cETHVault = 1200 / 50 = $24 at rate 50% = $12 @@ -1744,8 +1744,8 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { // 13K wETH @ 1200 = 15,600,000 USD of value, in RSR ~= 156,000 RSR (@100 usd) // We exceed maxTradeVolume so we need two auctions - Will first sell 10M in value // Sells about 101K RSR, for 8167 WETH minimum - ;[, lotHigh] = await rsrAsset.lotPrice() - const sellAmtRSR1 = MAX_TRADE_VOLUME.mul(BN_SCALE_FACTOR).div(lotHigh) + ;[low] = await rsrAsset.price() + const sellAmtRSR1 = MAX_TRADE_VOLUME.mul(BN_SCALE_FACTOR).div(low) const buyAmtBidRSR1 = toMinBuyAmt( sellAmtRSR1, rsrPrice, diff --git a/test/scenario/MaxBasketSize.test.ts b/test/scenario/MaxBasketSize.test.ts index a3ab632140..f1380b63f7 100644 --- a/test/scenario/MaxBasketSize.test.ts +++ b/test/scenario/MaxBasketSize.test.ts @@ -28,7 +28,7 @@ import { defaultFixtureNoBasket, IMPLEMENTATION, ORACLE_ERROR, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, REVENUE_HIDING, } from '../fixtures' @@ -158,7 +158,7 @@ describe(`Max Basket Size - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR, erc20: erc20.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -198,7 +198,7 @@ describe(`Max Basket Size - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR, erc20: atoken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -245,7 +245,7 @@ describe(`Max Basket Size - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR, erc20: ctoken.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, diff --git a/test/scenario/NestedRTokens.test.ts b/test/scenario/NestedRTokens.test.ts index 6386b158fd..38b11aba25 100644 --- a/test/scenario/NestedRTokens.test.ts +++ b/test/scenario/NestedRTokens.test.ts @@ -22,7 +22,7 @@ import { DefaultFixture, IMPLEMENTATION, ORACLE_ERROR, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, REVENUE_HIDING, } from '../fixtures' @@ -119,7 +119,7 @@ describe(`Nested RTokens - P${IMPLEMENTATION}`, () => { oracleError: ORACLE_ERROR, erc20: staticATokenERC20.address, maxTradeVolume: one.config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, diff --git a/test/scenario/NontrivialPeg.test.ts b/test/scenario/NontrivialPeg.test.ts index 70e0fa263f..c247b1cf98 100644 --- a/test/scenario/NontrivialPeg.test.ts +++ b/test/scenario/NontrivialPeg.test.ts @@ -23,7 +23,7 @@ import { defaultFixtureNoBasket, IMPLEMENTATION, ORACLE_ERROR, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, } from '../fixtures' @@ -82,7 +82,7 @@ describe(`The peg (target/ref) should be arbitrary - P${IMPLEMENTATION}`, () => oracleError: ORACLE_ERROR, erc20: token1.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -124,7 +124,7 @@ describe(`The peg (target/ref) should be arbitrary - P${IMPLEMENTATION}`, () => oracleError: ORACLE_ERROR, erc20: token0.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, diff --git a/test/scenario/RevenueHiding.test.ts b/test/scenario/RevenueHiding.test.ts index 8b1cfa00fb..815ff2f7fb 100644 --- a/test/scenario/RevenueHiding.test.ts +++ b/test/scenario/RevenueHiding.test.ts @@ -25,7 +25,7 @@ import { defaultFixtureNoBasket, IMPLEMENTATION, ORACLE_ERROR, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, } from '../fixtures' @@ -116,7 +116,7 @@ describe(`RevenueHiding basket collateral (/w CTokenFiatCollateral) - P${IMPLEME oracleError: ORACLE_ERROR, erc20: cDAI.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('USD'), defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, diff --git a/test/scenario/SetProtocol.test.ts b/test/scenario/SetProtocol.test.ts index a2a67dd94a..a9021be240 100644 --- a/test/scenario/SetProtocol.test.ts +++ b/test/scenario/SetProtocol.test.ts @@ -25,7 +25,7 @@ import { defaultFixtureNoBasket, IMPLEMENTATION, ORACLE_ERROR, - ORACLE_TIMEOUT, + ORACLE_TIMEOUT_PRE_BUFFER, PRICE_TIMEOUT, } from '../fixtures' @@ -91,7 +91,7 @@ describe(`Linear combination of self-referential collateral - P${IMPLEMENTATION} oracleError: ORACLE_ERROR, erc20: token0.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('ETH'), defaultThreshold: bn(0), delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -106,7 +106,7 @@ describe(`Linear combination of self-referential collateral - P${IMPLEMENTATION} oracleError: ORACLE_ERROR, erc20: token1.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('MKR'), defaultThreshold: bn(0), delayUntilDefault: DELAY_UNTIL_DEFAULT, @@ -121,7 +121,7 @@ describe(`Linear combination of self-referential collateral - P${IMPLEMENTATION} oracleError: ORACLE_ERROR, erc20: token2.address, maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, + oracleTimeout: ORACLE_TIMEOUT_PRE_BUFFER, targetName: ethers.utils.formatBytes32String('COMP'), defaultThreshold: bn(0), delayUntilDefault: DELAY_UNTIL_DEFAULT, diff --git a/test/scenario/__snapshots__/MaxBasketSize.test.ts.snap b/test/scenario/__snapshots__/MaxBasketSize.test.ts.snap index 89a9ca08e6..22ad1e5662 100644 --- a/test/scenario/__snapshots__/MaxBasketSize.test.ts.snap +++ b/test/scenario/__snapshots__/MaxBasketSize.test.ts.snap @@ -1,19 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Max Basket Size - P1 ATokens/CTokens Should Issue/Redeem with max basket correctly 1`] = `12092382`; +exports[`Max Basket Size - P1 ATokens/CTokens Should Issue/Redeem with max basket correctly 1`] = `12082311`; -exports[`Max Basket Size - P1 ATokens/CTokens Should Issue/Redeem with max basket correctly 2`] = `9830758`; +exports[`Max Basket Size - P1 ATokens/CTokens Should Issue/Redeem with max basket correctly 2`] = `9823907`; -exports[`Max Basket Size - P1 ATokens/CTokens Should claim rewards correctly 1`] = `2281990`; +exports[`Max Basket Size - P1 ATokens/CTokens Should claim rewards correctly 1`] = `2436571`; -exports[`Max Basket Size - P1 ATokens/CTokens Should switch basket correctly 1`] = `13617164`; +exports[`Max Basket Size - P1 ATokens/CTokens Should switch basket correctly 1`] = `13653658`; -exports[`Max Basket Size - P1 ATokens/CTokens Should switch basket correctly 2`] = `20897690`; +exports[`Max Basket Size - P1 ATokens/CTokens Should switch basket correctly 2`] = `21271957`; -exports[`Max Basket Size - P1 Fiatcoins Should Issue/Redeem with max basket correctly 1`] = `10991870`; +exports[`Max Basket Size - P1 Fiatcoins Should Issue/Redeem with max basket correctly 1`] = `10984061`; -exports[`Max Basket Size - P1 Fiatcoins Should Issue/Redeem with max basket correctly 2`] = `8713158`; +exports[`Max Basket Size - P1 Fiatcoins Should Issue/Redeem with max basket correctly 2`] = `8720813`; -exports[`Max Basket Size - P1 Fiatcoins Should switch basket correctly 1`] = `6561504`; +exports[`Max Basket Size - P1 Fiatcoins Should switch basket correctly 1`] = `6592432`; -exports[`Max Basket Size - P1 Fiatcoins Should switch basket correctly 2`] = `15130053`; +exports[`Max Basket Size - P1 Fiatcoins Should switch basket correctly 2`] = `15036720`; diff --git a/test/utils/oracles.ts b/test/utils/oracles.ts index 30c290f242..2444878fe4 100644 --- a/test/utils/oracles.ts +++ b/test/utils/oracles.ts @@ -8,6 +8,13 @@ import { MAX_UINT192 } from '../../common/constants' const toleranceDivisor = bn('1e15') // 1 part in 1000 trillions +export const expectExactPrice = async (assetAddr: string, price: [BigNumber, BigNumber]) => { + const asset = await ethers.getContractAt('Asset', assetAddr) + const [lowPrice, highPrice] = await asset.price() + expect(lowPrice).to.equal(price[0]) + expect(highPrice).to.equal(price[1]) +} + // Expects a price around `avgPrice` assuming a consistent percentage oracle error // If near is truthy, allows a small error of 1 part in 1000 trillions export const expectPrice = async ( @@ -86,6 +93,15 @@ export const expectRTokenPrice = async ( expect(highPrice).to.be.gte(avgPrice) } +export const expectDecayedPrice = async (assetAddr: string) => { + const asset = await ethers.getContractAt('Asset', assetAddr) + const [lowPrice, highPrice] = await asset.price() + expect(lowPrice).to.be.gt(0) + expect(lowPrice).to.be.lt(await asset.savedLowPrice()) + expect(highPrice).to.be.gt(await asset.savedHighPrice()) + expect(highPrice).to.be.lt(MAX_UINT192) +} + // Expects an unpriced asset with low = 0 and high = FIX_MAX export const expectUnpriced = async (assetAddr: string) => { const asset = await ethers.getContractAt('Asset', assetAddr) diff --git a/test/utils/trades.ts b/test/utils/trades.ts index 433c9e453a..99e0f4c22f 100644 --- a/test/utils/trades.ts +++ b/test/utils/trades.ts @@ -1,9 +1,11 @@ +import { getStorageAt, setStorageAt } from '@nomicfoundation/hardhat-network-helpers' import { Decimal } from 'decimal.js' import { BigNumber } from 'ethers' import { ethers } from 'hardhat' import { expect } from 'chai' -import { TestITrading, GnosisTrade } from '../../typechain' +import { TestITrading, GnosisTrade, TestIBroker } from '../../typechain' import { bn, fp, divCeil, divRound } from '../../common/numbers' +import { IMPLEMENTATION, Implementation } from '../fixtures' export const expectTrade = async (trader: TestITrading, auctionInfo: Partial) => { if (!auctionInfo.sell) throw new Error('Must provide sell token to find trade') @@ -81,7 +83,6 @@ export const dutchBuyAmount = async ( assetInAddr: string, assetOutAddr: string, outAmount: BigNumber, - minTradeVolume: BigNumber, maxTradeSlippage: BigNumber ): Promise => { const assetIn = await ethers.getContractAt('IAsset', assetInAddr) @@ -119,3 +120,23 @@ export const dutchBuyAmount = async ( } else price = worstPrice return divCeil(outAmount.mul(price), fp('1')) } + +export const disableBatchTrade = async (broker: TestIBroker) => { + if (IMPLEMENTATION == Implementation.P1) { + const slot = await getStorageAt(broker.address, 205) + await setStorageAt(broker.address, 205, slot.replace(slot.slice(2, 14), '1'.padStart(12, '0'))) + } else { + const slot = await getStorageAt(broker.address, 56) + await setStorageAt(broker.address, 56, slot.replace(slot.slice(2, 42), '1'.padStart(40, '0'))) + } + expect(await broker.batchTradeDisabled()).to.equal(true) +} + +export const disableDutchTrade = async (broker: TestIBroker, erc20: string) => { + const mappingSlot = IMPLEMENTATION == Implementation.P1 ? bn('208') : bn('57') + const p = mappingSlot.toHexString().slice(2).padStart(64, '0') + const key = erc20.slice(2).padStart(64, '0') + const slot = ethers.utils.keccak256('0x' + key + p) + await setStorageAt(broker.address, slot, '0x' + '1'.padStart(64, '0')) + expect(await broker.dutchTradeDisabled(erc20)).to.equal(true) +}