diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5479cbbec9..def1110b17 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -183,7 +183,7 @@ jobs: restore-keys: | hardhat-network-fork-${{ runner.os }}- hardhat-network-fork- - - run: npx hardhat test ./test/plugins/individual-collateral/{aave-v3,compoundv3,curve/cvx}/*.test.ts + - run: npx hardhat test ./test/plugins/individual-collateral/{aave-v3,compoundv3,curve/cvx,mountain}/*.test.ts env: NODE_OPTIONS: '--max-old-space-size=32768' TS_NODE_SKIP_IGNORE: true @@ -314,3 +314,17 @@ jobs: FORK_NETWORK: mainnet FORK: 1 PROTO_IMPL: 1 + + registry-tests: + name: 'Registry Tests' + 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 + - run: yarn test:registries + env: + NODE_OPTIONS: '--max-old-space-size=32768' diff --git a/CHANGELOG.md b/CHANGELOG.md index 62146ba19e..68e35e97f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,40 +4,67 @@ This release prepares the core protocol for veRSR through the introduction of 3 registries (`DAOFeeRegistry`, `AssetPluginRegistry`, and `VersionRegistry`) and through restricting component upgrades to be handled by `Main`, where upgrade constraints can be enforced. -The release also expands collateral decimal support from 18 to 27. +The release also expands collateral decimal support from 18 to 21, with some caveats about minimum token value. See [docs/solidity-style.md](./docs/solidity-style.md#Collateral-decimals) for more details. + +Finally, it adds resistance to toxic issuance by charging more when the collateral is under peg. ## Upgrade Steps -TODO +Upgrade to 4.0.0 is expected to occur by spell. This section is still TODO, but some important notes for steps that should be hit: -Make sure distributor table sums to >10000. +- Distributor table must sum to >=10000 +- Opt RTokens into the issuance premium by default +- Upgrade all collateral plugins and RTokenAsset +- ... ## Core Protocol Contracts +All components: make Main the only component that can call `upgradeTo()` + - `AssetRegistry` - Prevent registering assets that are not in the `AssetPluginRegistry` - Add `validateCurrentAssets() view` -- `Broker` - - Make setters only callable by `Main` +- `BackingManager` + - Switch from sizing trades using the low price to the high price +- `BasketHandler` + - Add `issuancePremium() view returns (uint192)` + - Add `setIssuancePremiumEnabled(bool)`, callable by governance. Begins disabled by default for upgraded RTokens + - Add `quote(uint192 amount, bool applyIssuancePremium, RoundingMode rounding)` + - Modify `quote(uint192 amount, RoundingMode rounding)` to include the issuance premium + - Add `price(bool applyIssuancePremium)` + - Modify `price()` to include the issuance premium + - Remove `lotPrice()` + - Minor changes to require error strings +- `Deployer` + - Add `enableIssuancePremium` parameter to `IDeployer.DeploymentParams` - `Distributor` - Add `setDistributions()` function to parallel `setDistribution()` - - Take DAO fee out account in `distribute()` and `totals()` + - Take DAO fee into account in `distribute()` and `totals()` - Add new revenue share table invariant: must sum to >=10000 (for precision reasons) - `Main` - Add `versionRegistry()`/`assetPluginRegistry()`/`daoFeeRegistry()` getters - Add `setVersionRegistry()`/`setAssetPluginRegistry()`/`setDaoFeeRegistry()` setters - Add `upgradeMainTo()` + `upgradeRTokenTo()` functions to handle upgrade of Main + Components - Make Main the only caller that can upgrade Main +- `RevenueTrader` + - Switch from sizing trades using the low price to the high price ## Plugins ### Assets -No functional change. FLOOR rounding added explicitly to `shiftl_toFix` +- Support expanded from 18 to 21 decimals, with minimum collateral token value requirement of `$0.001` at-peg. +- FLOOR rounding added explicitly to `shiftl_toFix` everywhere + +#### Collateral + +Add `savedPegPrice` to `ICollateral` interface ### Trading -Small bugfix to `GnosisTrade`. Should prevent donated tokens from causing the trade to revert. +- `GnosisTrade` + - Change units of `worstCasePrice()` from {buyTok/sellTok} to {qBuyTok/qSellTok} + - Small fix to prevent donated tokens from being able to cause the trade to revert ### Facades diff --git a/audits/Reserve_June_Plugins_v1.pdf b/audits/Reserve_June_Plugins_v1.pdf new file mode 100644 index 0000000000..fc48a7a379 Binary files /dev/null and b/audits/Reserve_June_Plugins_v1.pdf differ diff --git a/audits/Reserve_PR_4_0_0_v1.pdf b/audits/Reserve_PR_4_0_0_v1.pdf new file mode 100644 index 0000000000..c22c9db747 Binary files /dev/null and b/audits/Reserve_PR_4_0_0_v1.pdf differ diff --git a/audits/individual-plugins/Reserve_ETH_Plus_LP_v1.pdf b/audits/individual-plugins/Reserve_ETH_Plus_LP_v1.pdf new file mode 100644 index 0000000000..56fd12f320 Binary files /dev/null and b/audits/individual-plugins/Reserve_ETH_Plus_LP_v1.pdf differ diff --git a/audits/individual-plugins/Reserve_MetaMorpho_plugins_v2.pdf b/audits/individual-plugins/Reserve_MetaMorpho_plugins_v2.pdf new file mode 100644 index 0000000000..f0541de3e9 Binary files /dev/null and b/audits/individual-plugins/Reserve_MetaMorpho_plugins_v2.pdf differ diff --git a/common/blockchain-utils.ts b/common/blockchain-utils.ts index bf34e006b4..a5440f8147 100644 --- a/common/blockchain-utils.ts +++ b/common/blockchain-utils.ts @@ -23,13 +23,13 @@ export const getChainId = async (hre: HardhatRuntimeEnvironment): Promise @@ -541,6 +545,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = { saArbUSDCn: '', // TODO our wrapper. remove from deployment script after placing here aArbUSDT: '0x6ab707aca953edaefbc4fd23ba73294241490620', saArbUSDT: '', // TODO our wrapper. remove from deployment script after placing here + USDM: '0x59d9356e565ab3a36dd77763fc0d87feaf85508c', + wUSDM: '0x57f5e098cad7a3d1eed53991d4d66c45c9af7812', }, chainlinkFeeds: { ARB: '0xb2A824043730FE05F3DA2efaFa1CBbe83fa548D6', @@ -550,6 +556,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { USDT: '0x3f3f5dF88dC9F13eac63DF89EC16ef6e7E25DdE7', RSR: '0xcfF9349ec6d027f20fC9360117fef4a1Ad38B488', crvUSD: '0x0a32255dd4BB6177C994bAAc73E0606fDD568f66', + wUSDM: '0xdC6720c996Fad27256c7fd6E0a271e2A4687eF18', }, GNOSIS_EASY_AUCTION: '0xcD033976a011F41D2AB6ef47984041568F818E73', // our deployment COMET_REWARDS: '0x88730d254A2f7e6AC8388c3198aFd694bA9f7fae', @@ -592,6 +599,7 @@ export interface IConfig { withdrawalLeak: BigNumber warmupPeriod: BigNumber reweightable: boolean + enableIssuancePremium: boolean tradingDelay: BigNumber batchAuctionLength: BigNumber dutchAuctionLength: BigNumber diff --git a/contracts/facade/facets/MaxIssuableFacet.sol b/contracts/facade/facets/MaxIssuableFacet.sol index c385f021a9..7581b617c3 100644 --- a/contracts/facade/facets/MaxIssuableFacet.sol +++ b/contracts/facade/facets/MaxIssuableFacet.sol @@ -5,6 +5,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../../interfaces/IBasketHandler.sol"; import "../../interfaces/IRToken.sol"; import "../../libraries/Fixed.sol"; +import "../../p1/BasketHandler.sol"; /** * @title MaxIssuableFacet @@ -21,7 +22,8 @@ contract MaxIssuableFacet { /// @return {qRTok} How many RToken `account` can issue given current holdings /// @custom:static-call function maxIssuable(IRToken rToken, address account) external returns (uint256) { - (address[] memory erc20s, ) = rToken.main().basketHandler().quote(FIX_ONE, FLOOR); + BasketHandlerP1 bh = BasketHandlerP1(address(rToken.main().basketHandler())); + (address[] memory erc20s, ) = bh.quote(FIX_ONE, FLOOR); uint256[] memory balances = new uint256[](erc20s.length); for (uint256 i = 0; i < erc20s.length; ++i) { balances[i] = IERC20(erc20s[i]).balanceOf(account); @@ -45,7 +47,7 @@ contract MaxIssuableFacet { main.assetRegistry().refresh(); // Get basket ERC20s - IBasketHandler bh = main.basketHandler(); + BasketHandlerP1 bh = BasketHandlerP1(address(main.basketHandler())); (address[] memory erc20s, uint256[] memory quantities) = bh.quote(FIX_ONE, CEIL); // Compute how many baskets we can mint with the collateral amounts diff --git a/contracts/facade/facets/ReadFacet.sol b/contracts/facade/facets/ReadFacet.sol index f83cb5ed68..aee240ab4f 100644 --- a/contracts/facade/facets/ReadFacet.sol +++ b/contracts/facade/facets/ReadFacet.sol @@ -45,7 +45,7 @@ contract ReadFacet { // Cache components IRToken rTok = rToken; - IBasketHandler bh = main.basketHandler(); + BasketHandlerP1 bh = BasketHandlerP1(address(main.basketHandler())); IAssetRegistry reg = main.assetRegistry(); // Poke Main @@ -69,9 +69,7 @@ contract ReadFacet { uint192 mid = (low + high) / 2; // {UoA} = {tok} * {UoA/Tok} - depositsUoA[i] = shiftl_toFix(deposits[i], -int8(asset.erc20Decimals()), FLOOR).mul( - mid - ); + depositsUoA[i] = shiftl_toFix(deposits[i], -int8(asset.erc20Decimals()), CEIL).mul(mid); } } @@ -93,7 +91,7 @@ contract ReadFacet { // Cache Components IRToken rTok = rToken; - IBasketHandler bh = main.basketHandler(); + BasketHandlerP1 bh = BasketHandlerP1(address(main.basketHandler())); // Poke Main main.assetRegistry().refresh(); @@ -175,7 +173,7 @@ contract ReadFacet { { uint256[] memory deposits; IAssetRegistry assetRegistry = rToken.main().assetRegistry(); - IBasketHandler basketHandler = rToken.main().basketHandler(); + BasketHandlerP1 basketHandler = BasketHandlerP1(address(rToken.main().basketHandler())); // solhint-disable-next-line no-empty-blocks try rToken.main().furnace().melt() {} catch {} // <3.1.0 RTokens may revert while frozen @@ -301,7 +299,8 @@ contract ReadFacet { /// @return tokens The ERC20s backing the RToken function basketTokens(IRToken rToken) external view returns (address[] memory tokens) { - (tokens, ) = rToken.main().basketHandler().quote(FIX_ONE, RoundingMode.FLOOR); + BasketHandlerP1 bh = BasketHandlerP1(address(rToken.main().basketHandler())); + (tokens, ) = bh.quote(FIX_ONE, RoundingMode.FLOOR); } /// Returns the backup configuration for a given targetName @@ -337,10 +336,11 @@ contract ReadFacet { uint192 uoaNeeded; // {UoA} uint192 uoaHeldInBaskets; // {UoA} { - (address[] memory basketERC20s, uint256[] memory quantities) = rToken - .main() - .basketHandler() - .quote(basketsNeeded, FLOOR); + BasketHandlerP1 bh = BasketHandlerP1(address(rToken.main().basketHandler())); + (address[] memory basketERC20s, uint256[] memory quantities) = bh.quote( + basketsNeeded, + FLOOR + ); IAssetRegistry reg = rToken.main().assetRegistry(); IBackingManager bm = rToken.main().backingManager(); diff --git a/contracts/fuzz/CollateralMock.sol b/contracts/fuzz/CollateralMock.sol index ec87388932..542c55222b 100644 --- a/contracts/fuzz/CollateralMock.sol +++ b/contracts/fuzz/CollateralMock.sol @@ -123,56 +123,40 @@ contract CollateralMock is OracleErrorMock, AppreciatingFiatCollateral { } } +// DEPRECATED // A CollateralMock that does not use decaying lotPrice()s, but instead just returns the last saved // value. Needed for DiffTest, because refresh() doesn't always happen in the same block on both P0 // and P1. -contract CollateralNoDecay is CollateralMock { - function lotPrice() - external - view - virtual - override(Asset, IAsset) - returns (uint192 lotLow, uint192 lotHigh) - { - try this.tryPrice() returns (uint192 low, uint192 high, uint192) { - // if the price feed is still functioning, use that - 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 (savedLowPrice, savedHighPrice); - } - } - - constructor( - // Collateral base-class arguments - IERC20Metadata erc20_, - uint192 maxTradeVolume_, - uint48 priceTimeout_, - uint192 oracleError_, - uint192 defaultThreshold_, - uint48 delayUntilDefault_, - bytes32 targetName_, - // Price Models - PriceModel memory refPerTokModel_, // Ref units per token - PriceModel memory targetPerRefModel_, // Target units per ref unit - PriceModel memory uoaPerTargetModel_, // Units-of-account per target unit - PriceModel memory deviationModel_, - uint192 revenueHiding - ) - CollateralMock( - erc20_, - maxTradeVolume_, - priceTimeout_, - oracleError_, - defaultThreshold_, - delayUntilDefault_, - targetName_, - refPerTokModel_, - targetPerRefModel_, - uoaPerTargetModel_, - deviationModel_, - revenueHiding - ) - {} -} +// contract CollateralNoDecay is CollateralMock { +// constructor( +// // Collateral base-class arguments +// IERC20Metadata erc20_, +// uint192 maxTradeVolume_, +// uint48 priceTimeout_, +// uint192 oracleError_, +// uint192 defaultThreshold_, +// uint48 delayUntilDefault_, +// bytes32 targetName_, +// // Price Models +// PriceModel memory refPerTokModel_, // Ref units per token +// PriceModel memory targetPerRefModel_, // Target units per ref unit +// PriceModel memory uoaPerTargetModel_, // Units-of-account per target unit +// PriceModel memory deviationModel_, +// uint192 revenueHiding +// ) +// CollateralMock( +// erc20_, +// maxTradeVolume_, +// priceTimeout_, +// oracleError_, +// defaultThreshold_, +// delayUntilDefault_, +// targetName_, +// refPerTokModel_, +// targetPerRefModel_, +// uoaPerTargetModel_, +// deviationModel_, +// revenueHiding +// ) +// {} +// } diff --git a/contracts/fuzz/FuzzP0.sol b/contracts/fuzz/FuzzP0.sol index 3dae97dba2..605975ac3c 100644 --- a/contracts/fuzz/FuzzP0.sol +++ b/contracts/fuzz/FuzzP0.sol @@ -129,7 +129,7 @@ contract RTokenP0Fuzz is IRTokenFuzz, RTokenP0 { ? basketsNeeded.muluDivu(amount, totalSupply()) // {BU * qRTok / qRTok} : uint192(amount); // {qRTok / qRTok} - return main.basketHandler().quote(baskets, roundingMode); + return main.basketHandler().quote(baskets, true, roundingMode); } function _msgSender() internal view virtual override returns (address) { diff --git a/contracts/fuzz/FuzzP1.sol b/contracts/fuzz/FuzzP1.sol index 94e6edb0f7..ce2132c2d3 100644 --- a/contracts/fuzz/FuzzP1.sol +++ b/contracts/fuzz/FuzzP1.sol @@ -467,7 +467,7 @@ contract RTokenP1Fuzz is IRTokenFuzz, RTokenP1 { ? basketsNeeded.muluDivu(amount, totalSupply()) // {BU * qRTok / qRTok} : uint192(amount); // {qRTok / qRTok} - return main.basketHandler().quote(baskets, roundingMode); + return main.basketHandler().quote(baskets, true, roundingMode); } function _msgSender() internal view virtual override returns (address) { diff --git a/contracts/fuzz/MainP0.sol b/contracts/fuzz/MainP0.sol index f936525389..131c7688c4 100644 --- a/contracts/fuzz/MainP0.sol +++ b/contracts/fuzz/MainP0.sol @@ -208,7 +208,7 @@ contract MainP0Fuzz is IMainFuzz, MainP0 { params.minTradeVolume ); - basketHandler.init(this, params.warmupPeriod, params.reweightable); + basketHandler.init(this, params.warmupPeriod, params.reweightable, params.enableIssuancePremium); rsrTrader.init(this, rsr, params.maxTradeSlippage, params.minTradeVolume); rTokenTrader.init( this, diff --git a/contracts/fuzz/MainP1.sol b/contracts/fuzz/MainP1.sol index dd99a9df6a..f54c5b4e9f 100644 --- a/contracts/fuzz/MainP1.sol +++ b/contracts/fuzz/MainP1.sol @@ -212,7 +212,7 @@ contract MainP1Fuzz is IMainFuzz, MainP1 { params.minTradeVolume ); - basketHandler.init(this, params.warmupPeriod, params.reweightable); + basketHandler.init(this, params.warmupPeriod, params.reweightable, params.enableIssuancePremium); rsrTrader.init(this, rsr, params.maxTradeSlippage, params.minTradeVolume); rTokenTrader.init( this, diff --git a/contracts/fuzz/Utils.sol b/contracts/fuzz/Utils.sol index 87246e85dc..b8ae3bde7e 100644 --- a/contracts/fuzz/Utils.sol +++ b/contracts/fuzz/Utils.sol @@ -30,7 +30,8 @@ function defaultParams() pure returns (DeploymentParams memory params) { issuanceThrottle: tParams, redemptionThrottle: tParams, warmupPeriod: 259200, - reweightable: false + reweightable: false, + enableIssuancePremium: true }); } diff --git a/contracts/fuzz/scenarios/ChaosOps.sol b/contracts/fuzz/scenarios/ChaosOps.sol index 6c66d922bc..ad3dca2120 100644 --- a/contracts/fuzz/scenarios/ChaosOps.sol +++ b/contracts/fuzz/scenarios/ChaosOps.sol @@ -937,6 +937,10 @@ contract ChaosOpsScenario { main.setLongFreeze(freeze); } + function setIssuancePremiumEnabled(uint256 seed) public { + BasketHandlerP1(address(main.basketHandler())).setIssuancePremiumEnabled((seed % 2) == 0); + } + // Grant/Revoke Roles function grantRole(uint8 which, uint8 userID) public { address user = main.someAddr(userID); diff --git a/contracts/fuzz/scenarios/NormalOps.sol b/contracts/fuzz/scenarios/NormalOps.sol index 6c230da1a3..b76722dcc1 100644 --- a/contracts/fuzz/scenarios/NormalOps.sol +++ b/contracts/fuzz/scenarios/NormalOps.sol @@ -625,6 +625,10 @@ contract NormalOpsScenario { ); } + function setIssuancePremiumEnabled(uint256 seed) public { + BasketHandlerP1(address(main.basketHandler())).setIssuancePremiumEnabled((seed % 2) == 0); + } + function resetStakes() public { main.stRSR().resetStakes(); } diff --git a/contracts/fuzz/scenarios/Rebalancing.sol b/contracts/fuzz/scenarios/Rebalancing.sol index b6ebfa5b0e..71215a2f3f 100644 --- a/contracts/fuzz/scenarios/Rebalancing.sol +++ b/contracts/fuzz/scenarios/Rebalancing.sol @@ -996,6 +996,10 @@ contract RebalancingScenario { main.setLongFreeze(freeze); } + function setIssuancePremiumEnabled(uint256 seed) public { + BasketHandlerP1(address(main.basketHandler())).setIssuancePremiumEnabled((seed % 2) == 0); + } + // Grant/Revoke Roles function grantRole(uint8 which, uint8 userID) public { address user = main.someAddr(userID); diff --git a/contracts/interfaces/IAsset.sol b/contracts/interfaces/IAsset.sol index 3196f9ce6e..4cc223a63d 100644 --- a/contracts/interfaces/IAsset.sol +++ b/contracts/interfaces/IAsset.sol @@ -32,13 +32,6 @@ interface IAsset is IRewardable { /// @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); - /// @return {tok} The balance of the ERC20 in whole tokens function bal(address account) external view returns (uint192); @@ -114,7 +107,7 @@ interface ICollateral is IAsset { /// @dev refresh() /// Refresh exchange rates and update default status. - /// VERY IMPORTANT: In any valid implemntation, status() MUST become DISABLED in refresh() if + /// VERY IMPORTANT: In any valid implementation, status() MUST become DISABLED in refresh() if /// refPerTok() has ever decreased since last call. /// @return The canonical name of this collateral's target unit. @@ -130,6 +123,9 @@ interface ICollateral is IAsset { /// @return {target/ref} Quantity of whole target units per whole reference unit in the peg function targetPerRef() external view returns (uint192); + + /// @return {target/ref} The peg price of the token during the last update + function savedPegPrice() external view returns (uint192); } // Used only in Testing. Strictly speaking a Collateral does not need to adhere to this interface diff --git a/contracts/interfaces/IBasketHandler.sol b/contracts/interfaces/IBasketHandler.sol index 44aeace135..315790554e 100644 --- a/contracts/interfaces/IBasketHandler.sol +++ b/contracts/interfaces/IBasketHandler.sol @@ -43,6 +43,11 @@ interface IBasketHandler is IComponent { /// @param newVal The new warmup period event WarmupPeriodSet(uint48 oldVal, uint48 newVal); + /// Emitted when the issuance premium logic is changed + /// @param oldVal The old value of enableIssuancePremium + /// @param newVal The new value of enableIssuancePremium + event EnableIssuancePremiumSet(bool oldVal, bool newVal); + /// Emitted when the status of a basket has changed /// @param oldStatus The previous basket status /// @param newStatus The new basket status @@ -57,19 +62,20 @@ interface IBasketHandler is IComponent { function init( IMain main_, uint48 warmupPeriod_, - bool reweightable_ + bool reweightable_, + bool enableIssuancePremium_ ) external; - /// Set the prime basket - /// For an index RToken (reweightable = true), use forceSetPrimeBasket to skip normalization + /// Set the prime basket, checking target amounts are constant /// @param erc20s The collateral tokens for the new prime basket /// @param targetAmts The target amounts (in) {target/BU} for the new prime basket /// required range: 1e9 values; absolute range irrelevant. /// @custom:governance function setPrimeBasket(IERC20[] calldata erc20s, uint192[] calldata targetAmts) external; - /// Set the prime basket without normalizing targetAmts by the UoA of the current basket - /// Works the same as setPrimeBasket for non-index RTokens (reweightable = false) + /// Set the prime basket, skipping any constant target amount checks if RToken is reweightable + /// Warning: Reweightable RTokens SHOULD use a spell to execute this function to avoid + /// accidentally changing the UoA value of the RToken. /// @param erc20s The collateral tokens for the new prime basket /// @param targetAmts The target amounts (in) {target/BU} for the new prime basket /// required range: 1e9 values; absolute range irrelevant. @@ -110,29 +116,33 @@ interface IBasketHandler is IComponent { /// @return If the basket is ready to issue and trade function isReady() external view returns (bool); + /// Returns basket quantity rounded up, wihout any issuance premium /// @param erc20 The ERC20 token contract for the asset - /// @return {tok/BU} The whole token quantity of token in the reference basket + /// @return {tok/BU} The redemption quantity of token in the reference basket, rounded up /// Returns 0 if erc20 is not registered or not in the basket /// Returns FIX_MAX (in lieu of +infinity) if Collateral.refPerTok() is 0. /// Otherwise, returns (token's basket.refAmts / token's Collateral.refPerTok()) function quantity(IERC20 erc20) external view returns (uint192); + /// Returns basket quantity rounded up, wihout any issuance premium /// Like quantity(), but unsafe because it DOES NOT CONFIRM THAT THE ASSET IS CORRECT /// @param erc20 The ERC20 token contract for the asset /// @param asset The registered asset plugin contract for the erc20 - /// @return {tok/BU} The whole token quantity of token in the reference basket + /// @return {tok/BU} The redemption quantity of token in the reference basket, rounded up /// Returns 0 if erc20 is not registered or not in the basket /// Returns FIX_MAX (in lieu of +infinity) if Collateral.refPerTok() is 0. /// Otherwise, returns (token's basket.refAmts / token's Collateral.refPerTok()) function quantityUnsafe(IERC20 erc20, IAsset asset) external view returns (uint192); /// @param amount {BU} + /// @param applyIssuancePremium Whether to apply the issuance premium /// @return erc20s The addresses of the ERC20 tokens in the reference basket /// @return quantities {qTok} The quantity of each ERC20 token to issue `amount` baskets - function quote(uint192 amount, RoundingMode rounding) - external - view - returns (address[] memory erc20s, uint256[] memory quantities); + function quote( + uint192 amount, + bool applyIssuancePremium, + RoundingMode rounding + ) external view returns (address[] memory erc20s, uint256[] memory quantities); /// Return the redemption value of `amount` BUs for a linear combination of historical baskets /// @param basketNonces An array of basket nonces to do redemption from @@ -152,16 +162,10 @@ interface IBasketHandler is IComponent { /// Should not revert /// low should be nonzero when BUs are worth selling + /// @param applyIssuancePremium Whether to apply the issuance premium to the high price /// @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); + function price(bool applyIssuancePremium) external view returns (uint192 low, uint192 high); /// @return timestamp The timestamp at which the basket was last set function timestamp() external view returns (uint48); @@ -190,4 +194,8 @@ interface TestIBasketHandler is IBasketHandler { function warmupPeriod() external view returns (uint48); function setWarmupPeriod(uint48 val) external; + + function enableIssuancePremium() external view returns (bool); + + function setIssuancePremiumEnabled(bool val) external; } diff --git a/contracts/interfaces/IDeployer.sol b/contracts/interfaces/IDeployer.sol index 0a1e1ff6b5..149394fc39 100644 --- a/contracts/interfaces/IDeployer.sol +++ b/contracts/interfaces/IDeployer.sol @@ -39,6 +39,7 @@ struct DeploymentParams { // === BasketHandler === uint48 warmupPeriod; // {s} how long to wait until issuance/trading after regaining SOUND bool reweightable; // whether the target amounts in the prime basket can change + bool enableIssuancePremium; // whether to enable the issuance premium // // === BackingManager === uint48 tradingDelay; // {s} how long to wait until starting auctions after switching basket diff --git a/contracts/interfaces/IDistributor.sol b/contracts/interfaces/IDistributor.sol index 2a8e751183..32de4b401e 100644 --- a/contracts/interfaces/IDistributor.sol +++ b/contracts/interfaces/IDistributor.sol @@ -46,7 +46,7 @@ interface IDistributor is IComponent { function setDistribution(address dest, RevenueShare calldata share) external; /// @custom:governance - function setDistributions(address[] calldata dest, RevenueShare[] calldata share) external; + function setDistributions(address[] calldata dests, RevenueShare[] calldata shares) external; /// Distribute the `erc20` token across all revenue destinations /// Only callable by RevenueTraders diff --git a/contracts/mocks/DeployerMock.sol b/contracts/mocks/DeployerMock.sol index e8beb8b07c..d6e57d03d9 100644 --- a/contracts/mocks/DeployerMock.sol +++ b/contracts/mocks/DeployerMock.sol @@ -16,6 +16,10 @@ contract DeployerMock is Versioned { function implementations() external view returns (Implementations memory) { return _implementations; } + + function version() public pure virtual override returns (string memory) { + return "V1"; // make different from '4.0.0' on purpose + } } contract DeployerMockV2 is DeployerMock { diff --git a/contracts/p0/AssetRegistry.sol b/contracts/p0/AssetRegistry.sol index ad4239cd84..a0e8bded18 100644 --- a/contracts/p0/AssetRegistry.sol +++ b/contracts/p0/AssetRegistry.sol @@ -176,9 +176,9 @@ contract AssetRegistryP0 is ComponentP0, IAssetRegistry { function _reserveGas() private view returns (uint256) { uint256 gas = gasleft(); require( - gas > GAS_FOR_DISABLE_BASKET + GAS_FOR_BH_QTY, + gas > (64 * GAS_FOR_BH_QTY) / 63 + GAS_FOR_DISABLE_BASKET, "not enough gas to unregister safely" ); - return gas - GAS_FOR_DISABLE_BASKET; + return GAS_FOR_BH_QTY; } } diff --git a/contracts/p0/BasketHandler.sol b/contracts/p0/BasketHandler.sol index d525fd673d..c469dfa795 100644 --- a/contracts/p0/BasketHandler.sol +++ b/contracts/p0/BasketHandler.sol @@ -153,6 +153,8 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { // Whether the total weights of the target basket can be changed bool public reweightable; // immutable after init + bool public enableIssuancePremium; + // ==== Invariants ==== // basket is a valid Basket: // basket.erc20s is a valid collateral array and basket.erc20s == keys(basket.refAmts) @@ -166,12 +168,14 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { function init( IMain main_, uint48 warmupPeriod_, - bool reweightable_ + bool reweightable_, + bool enableIssuancePremium_ ) external initializer { __Component_init(main_); setWarmupPeriod(warmupPeriod_); reweightable = reweightable_; // immutable thereafter + enableIssuancePremium = enableIssuancePremium_; // Set last status to DISABLED (default) lastStatus = CollateralStatus.DISABLED; @@ -237,7 +241,7 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { } } - /// Set the prime basket + /// Set the prime basket, checking target amounts are constant /// @param erc20s The collateral for the new prime basket /// @param targetAmts The target amounts (in) {target/BU} for the new prime basket /// @custom:governance @@ -245,10 +249,12 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { external governance { - _setPrimeBasket(erc20s, targetAmts, true); + _setPrimeBasket(erc20s, targetAmts, false); } - /// Set the prime basket without reweighting targetAmts by UoA of the current basket + /// Set the prime basket, skipping any constant target amount checks if RToken is reweightable + /// Warning: Reweightable RTokens SHOULD use a spell to execute this function to avoid + /// accidentally changing the UoA value of the RToken. /// @param erc20s The collateral for the new prime basket /// @param targetAmts The target amounts (in) {target/BU} for the new prime basket /// @custom:governance @@ -256,13 +262,13 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { external governance { - _setPrimeBasket(erc20s, targetAmts, false); + _setPrimeBasket(erc20s, targetAmts, true); } /// Set the prime basket in the basket configuration, in terms of erc20s and target amounts /// @param erc20s The collateral for the new prime basket /// @param targetAmts The target amounts (in) {target/BU} for the new prime basket - /// @param normalize True iff targetAmts should be normalized by UoA to the reference basket + /// @param disableTargetAmountCheck If true, skips the `requireConstantConfigTargets()` check /// @custom:governance // checks: // caller is OWNER @@ -278,18 +284,18 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { function _setPrimeBasket( IERC20[] calldata erc20s, uint192[] memory targetAmts, - bool normalize + bool disableTargetAmountCheck ) internal { - require(erc20s.length > 0, "empty basket"); - require(erc20s.length == targetAmts.length, "len mismatch"); + require(erc20s.length > 0, "invalid lengths"); + require(erc20s.length == targetAmts.length, "invalid lengths"); requireValidCollArray(erc20s); - if (!reweightable && config.erc20s.length > 0) { + if ( + (!reweightable || (reweightable && !disableTargetAmountCheck)) && + config.erc20s.length != 0 + ) { // Require targets remain constant requireConstantConfigTargets(erc20s, targetAmts); - } else if (normalize && config.erc20s.length > 0) { - // Normalize targetAmts based on UoA value of reference basket - targetAmts = normalizeByPrice(erc20s, targetAmts); } // Clean up previous basket config @@ -307,8 +313,8 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { // This is a nice catch to have, but in general it is possible for // an ERC20 in the prime basket to have its asset unregistered. require(reg.toAsset(erc20s[i]).isCollateral(), "erc20 is not collateral"); - require(0 < targetAmts[i], "invalid target amount; must be nonzero"); - require(targetAmts[i] <= MAX_TARGET_AMT, "invalid target amount; too large"); + require(0 < targetAmts[i], "invalid target amount"); + require(targetAmts[i] <= MAX_TARGET_AMT, "invalid target amount"); config.erc20s.push(erc20s[i]); config.targetAmts[erc20s[i]] = targetAmts[i]; @@ -333,8 +339,8 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { uint256 max, IERC20[] calldata erc20s ) external governance { - require(max <= MAX_BACKUP_ERC20S, "max too large"); - require(erc20s.length <= MAX_BACKUP_ERC20S, "erc20s too large"); + require(max <= MAX_BACKUP_ERC20S, "too large"); + require(erc20s.length <= MAX_BACKUP_ERC20S, "too large"); requireValidCollArray(erc20s); BackupConfig storage conf = config.backups[targetName]; conf.max = max; @@ -377,19 +383,21 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { (block.timestamp >= lastStatusTimestamp + warmupPeriod); } + /// Basket quantity rounded up, without any issuance premium /// @param erc20 The token contract to check for quantity for - /// @return {tok/BU} The token-quantity of an ERC20 token in the basket. + /// @return {tok/BU} The redemption token-quantity of an ERC20 token in the basket. // Returns 0 if erc20 is not registered or not in the basket // Returns FIX_MAX (in lieu of +infinity) if Collateral.refPerTok() is 0. // Otherwise returns (token's basket.refAmts / token's Collateral.refPerTok()) function quantity(IERC20 erc20) public view returns (uint192) { try main.assetRegistry().toColl(erc20) returns (ICollateral coll) { - return _quantity(erc20, coll); + return _quantity(erc20, coll, CEIL); } catch { return FIX_ZERO; } } + /// Basket quantity rounded up, without any issuance premium /// Like quantity(), but unsafe because it DOES NOT CONFIRM THAT THE ASSET IS CORRECT /// @param erc20 The ERC20 token contract for the asset /// @param asset The registered asset plugin contract for the erc20 @@ -399,66 +407,94 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { // Otherwise returns (token's basket.refAmts / token's Collateral.refPerTok()) function quantityUnsafe(IERC20 erc20, IAsset asset) public view returns (uint192) { if (!asset.isCollateral()) return FIX_ZERO; - return _quantity(erc20, ICollateral(address(asset))); + return _quantity(erc20, ICollateral(address(asset)), CEIL); + } + + /// @param coll A collateral that has had refresh() called on it this timestamp + /// @return {1} The multiplier to charge on issuance quantities for a collateral + function issuancePremium(ICollateral coll) public view returns (uint192) { + if (!enableIssuancePremium || coll.lastSave() != block.timestamp) return FIX_ONE; + + try coll.savedPegPrice() returns (uint192 pegPrice) { + uint192 targetPerRef = coll.targetPerRef(); // {target/ref} + if (pegPrice == 0 || pegPrice >= targetPerRef) return FIX_ONE; + + // {1} = {target/ref} / {target/ref} + return targetPerRef.safeDiv(pegPrice, CEIL); + } catch { + // if savedPegPrice() does not exist on the collateral the error bytes are 0 len + return FIX_ONE; + } } + /// Returns the quantity of collateral token in a BU /// @param erc20 The token contract /// @param coll The registered collateral plugin contract - /// @return {tok/BU} The token-quantity of an ERC20 token in the basket. + /// @return q {tok/BU} The token-quantity of an ERC20 token in the basket // Returns 0 if coll is not in the basket // Returns FIX_MAX (in lieu of +infinity) if Collateral.refPerTok() is 0. // Otherwise returns (token's basket.refAmts / token's Collateral.refPerTok()) - function _quantity(IERC20 erc20, ICollateral coll) internal view returns (uint192) { + function _quantity( + IERC20 erc20, + ICollateral coll, + RoundingMode rounding + ) internal view returns (uint192) { uint192 refPerTok = coll.refPerTok(); if (refPerTok == 0) return FIX_MAX; // {tok/BU} = {ref/BU} / {ref/tok} - return basket.refAmts[erc20].div(refPerTok, CEIL); + return basket.refAmts[erc20].div(refPerTok, rounding); } + /// Returns the price of a BU (including issuance premium) + /// Included for backwards compatibility with <4.0.0 /// Should not revert /// @return low {UoA/BU} The lower end of the price estimate /// @return high {UoA/BU} The upper end of the price estimate // returns sum(quantity(erc20) * price(erc20) for erc20 in basket.erc20s) function price() external view returns (uint192 low, uint192 high) { - return _price(false); + return price(true); } /// 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) - function lotPrice() external view returns (uint192 lotLow, uint192 lotHigh) { - return _price(true); - } - - /// Returns the price of a BU, using the lot prices if `useLotPrice` is true - /// @return low {UoA/BU} The lower end of the lot price estimate - /// @return high {UoA/BU} The upper end of the lot price estimate - function _price(bool useLotPrice) internal view returns (uint192 low, uint192 high) { + /// @param applyIssuancePremium Whether to apply the issuance premium to the high price + /// @return low {UoA/BU} The lower end of the price estimate + /// @return high {UoA/BU} The upper end of the price estimate + // returns sum(quantity(erc20) * price(erc20) for erc20 in basket.erc20s) + function price(bool applyIssuancePremium) public view returns (uint192 low, uint192 high) { IAssetRegistry reg = main.assetRegistry(); uint256 low256; uint256 high256; for (uint256 i = 0; i < basket.erc20s.length; i++) { - uint192 qty = quantity(basket.erc20s[i]); - if (qty == 0) continue; + try main.assetRegistry().toColl(basket.erc20s[i]) returns (ICollateral coll) { + uint192 qty = _quantity(basket.erc20s[i], coll, CEIL); + if (qty == 0) continue; - (uint192 lowP, uint192 highP) = useLotPrice - ? reg.toAsset(basket.erc20s[i]).lotPrice() - : reg.toAsset(basket.erc20s[i]).price(); + (uint192 lowP, uint192 highP) = reg.toAsset(basket.erc20s[i]).price(); - low256 += qty.safeMul(lowP, RoundingMode.FLOOR); + low256 += qty.safeMul(lowP, FLOOR); - if (high256 < FIX_MAX) { - if (highP == FIX_MAX) { - high256 = FIX_MAX; - } else { - high256 += qty.safeMul(highP, RoundingMode.CEIL); + if (high256 < FIX_MAX) { + if (highP == FIX_MAX) { + high256 = FIX_MAX; + continue; + } + + if (applyIssuancePremium) { + uint192 premium = issuancePremium(coll); // {1} always CEIL + + // {tok} = {tok} * {1} + if (premium > FIX_ONE) qty = qty.safeMul(premium, CEIL); + } + + high256 += qty.safeMul(highP, CEIL); } + } catch (bytes memory errData) { + // see: docs/solidity-style.md#Catching-Empty-Data + if (errData.length == 0) revert(); // solhint-disable-line reason-string + continue; } } @@ -467,8 +503,10 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { high = high256 >= FIX_MAX ? FIX_MAX : uint192(high256); } - /// Return the current issuance/redemption value of `amount` BUs + /// Return the current issuance/redemption quantities for `amount` BUs + /// Included for backwards compatibility with <4.0.0 /// @param amount {BU} + /// @param rounding If CEIL, apply issuance premium /// @return erc20s The backing collateral erc20s /// @return quantities {qTok} ERC20 token quantities equal to `amount` BUs // Returns (erc20s, [quantity(e) * amount {as qTok} for e in erc20s]) @@ -477,14 +515,40 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { view returns (address[] memory erc20s, uint256[] memory quantities) { + return quote(amount, rounding == CEIL, rounding); + } + + /// @param amount {BU} + /// @param applyIssuancePremium Whether to apply the issuance premium + /// @return erc20s The backing collateral erc20s + /// @return quantities {qTok} ERC20 token quantities equal to `amount` BUs + // Returns (erc20s, [quantity(e) * amount {as qTok} for e in erc20s]) + function quote( + uint192 amount, + bool applyIssuancePremium, + RoundingMode rounding + ) public view returns (address[] memory erc20s, uint256[] memory quantities) { + IAssetRegistry assetRegistry = main.assetRegistry(); erc20s = new address[](basket.erc20s.length); quantities = new uint256[](basket.erc20s.length); for (uint256 i = 0; i < basket.erc20s.length; ++i) { erc20s[i] = address(basket.erc20s[i]); + ICollateral coll = assetRegistry.toColl(IERC20(erc20s[i])); - // {qTok} = {tok/BU} * {BU} * {tok} * {qTok/tok} - quantities[i] = quantity(basket.erc20s[i]).safeMul(amount, rounding).shiftl_toUint( + // {tok} = {tok/BU} * {BU} + uint192 q = _quantity(basket.erc20s[i], coll, rounding).safeMul(amount, rounding); + + // Prevent toxic issuance by charging more when collateral is under peg + if (applyIssuancePremium) { + uint192 premium = issuancePremium(coll); // {1} always CEIL + + // {tok} = {tok} * {1} + if (premium > FIX_ONE) q = q.safeMul(premium, rounding); + } + + // {qTok} = {tok} * {qTok/tok} + quantities[i] = q.shiftl_toUint( int8(IERC20Metadata(address(basket.erc20s[i])).decimals()), rounding ); @@ -503,7 +567,7 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { uint192[] memory portions, uint192 amount ) external view returns (address[] memory erc20s, uint256[] memory quantities) { - require(basketNonces.length == portions.length, "bad portions len"); + require(basketNonces.length == portions.length, "invalid lengths"); IERC20[] memory erc20sAll = new IERC20[](main.assetRegistry().size()); ICollateral[] memory collsAll = new ICollateral[](erc20sAll.length); @@ -571,8 +635,6 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { quantities[i] = amount .safeMulDiv(refAmtsAll[i], collsAll[i].refPerTok(), FLOOR) .shiftl_toUint(int8(collsAll[i].erc20Decimals()), FLOOR); - // marginally more penalizing than its sibling calculation that uses _quantity() - // because does not intermediately CEIL as part of the division } } @@ -591,14 +653,9 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { ICollateral coll = main.assetRegistry().toColl(basket.erc20s[i]); if (coll.status() == CollateralStatus.DISABLED) return BasketRange(FIX_ZERO, FIX_MAX); - uint192 refPerTok = coll.refPerTok(); - // If refPerTok is 0, then we have zero of coll's reference unit. - // We know that basket.refAmts[basket.erc20s[i]] > 0, so we have no baskets. - if (refPerTok == 0) return BasketRange(FIX_ZERO, FIX_MAX); - - // {tok/BU} = {ref/BU} / {ref/tok}. 0-division averted by condition above. - uint192 q = basket.refAmts[basket.erc20s[i]].div(refPerTok, CEIL); - // q > 0 because q = (n).div(_, CEIL) and n > 0 + // {tok/BU} + uint192 q = _quantity(basket.erc20s[i], coll, CEIL); + if (q == FIX_MAX) return BasketRange(FIX_ZERO, FIX_MAX); // {BU} = {tok} / {tok/BU} uint192 inBUs = coll.bal(account).div(q); @@ -616,6 +673,12 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { warmupPeriod = val; } + /// @custom:governance + function setIssuancePremiumEnabled(bool val) public governance { + emit EnableIssuancePremiumSet(enableIssuancePremium, val); + enableIssuancePremium = val; + } + /* _switchBasket computes basket' from three inputs: - the basket configuration (config: BasketConfig) - the function (isGood: erc20 -> bool), implemented here by goodCollateral() @@ -839,46 +902,6 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { require(_targetAmts.length() == 0, "missing target weights"); } - /// Normalize the target amounts to maintain constant UoA value with the current config - /// @return newTargetAmts {target/BU} The new target amounts for the normalized basket - function normalizeByPrice(IERC20[] calldata erc20s, uint192[] memory targetAmts) - private - returns (uint192[] memory newTargetAmts) - { - main.poke(); - require(status() == CollateralStatus.SOUND, "unsound basket"); - uint256 len = erc20s.length; // assumes erc20s.length == targetAmts.length - - // Compute current basket price - (uint192 low, uint192 high) = _price(false); // {UoA/BU} - assert(low > 0 && high < FIX_MAX); // implied by SOUND status - uint192 p = low.plus(high).divu(2, FLOOR); // {UoA/BU} - - // Compute would-be new price - uint192 newP; // {UoA/BU} - for (uint256 i = 0; i < len; ++i) { - ICollateral coll = main.assetRegistry().toColl(erc20s[i]); // reverts if unregistered - require(coll.status() == CollateralStatus.SOUND, "unsound new collateral"); - - (low, high) = coll.price(); // {UoA/tok} - require(low > 0 && high < FIX_MAX, "invalid price"); - - // {UoA/BU} += {target/BU} * {UoA/tok} / ({target/ref} * {ref/tok}) - newP += targetAmts[i].mulDiv( - low.plus(high).divu(2, FLOOR), - coll.targetPerRef().mul(coll.refPerTok(), CEIL), - FLOOR - ); - } - - // Scale targetAmts by the price ratio - newTargetAmts = new uint192[](len); - for (uint256 i = 0; i < len; ++i) { - // {target/BU} = {target/BU} * {UoA/BU} / {UoA/BU} - newTargetAmts[i] = targetAmts[i].mulDiv(p, newP, CEIL); - } - } - /// Good collateral is registered, collateral, SOUND, has the expected targetName, /// and not a system token or 0 addr function goodCollateral(bytes32 targetName, IERC20 erc20) private view returns (bool) { diff --git a/contracts/p0/Broker.sol b/contracts/p0/Broker.sol index dab883d2d1..6f0db15f62 100644 --- a/contracts/p0/Broker.sol +++ b/contracts/p0/Broker.sol @@ -232,7 +232,7 @@ contract BrokerP0 is ComponentP0, IBroker { ); require(dutchAuctionLength > 0, "dutch auctions not enabled"); require( - priceNotDecayed(req.sell) && priceNotDecayed(req.buy), + pricedAtTimestamp(req.sell) && pricedAtTimestamp(req.buy), "dutch auctions require live prices" ); @@ -249,8 +249,8 @@ contract BrokerP0 is ComponentP0, 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 true iff the asset has been priced at this timestamp, or it's the RTokenAsset + function pricedAtTimestamp(IAsset asset) private view returns (bool) { return asset.lastSave() == block.timestamp || address(asset.erc20()) == address(main.rToken()); } diff --git a/contracts/p0/Deployer.sol b/contracts/p0/Deployer.sol index e26569974a..f812830044 100644 --- a/contracts/p0/Deployer.sol +++ b/contracts/p0/Deployer.sol @@ -95,7 +95,12 @@ contract DeployerP0 is IDeployer, Versioned { ); // Init Basket Handler - main.basketHandler().init(main, params.warmupPeriod, params.reweightable); + main.basketHandler().init( + main, + params.warmupPeriod, + params.reweightable, + params.enableIssuancePremium + ); // Init Revenue Traders main.rsrTrader().init(main, rsr, params.maxTradeSlippage, params.minTradeVolume); diff --git a/contracts/p0/Distributor.sol b/contracts/p0/Distributor.sol index 5e4fd609e2..ade5d11df2 100644 --- a/contracts/p0/Distributor.sol +++ b/contracts/p0/Distributor.sol @@ -47,10 +47,6 @@ contract DistributorP0 is ComponentP0, IDistributor { /// Set RevenueShares for destinations. Destinations `FURNACE` and `ST_RSR` refer to /// main.furnace() and main.stRSR(). /// @custom:governance - // checks: invariants hold in post-state - // effects: - // destinations' = dests - // distribution' = shares function setDistributions(address[] calldata dests, RevenueShare[] calldata shares) external governance diff --git a/contracts/p0/RToken.sol b/contracts/p0/RToken.sol index 3a4b476aff..c30757f96b 100644 --- a/contracts/p0/RToken.sol +++ b/contracts/p0/RToken.sol @@ -109,7 +109,11 @@ contract RTokenP0 is ComponentP0, ERC20PermitUpgradeable, IRToken { ? basketsNeeded.muluDivu(amount, totalSupply(), CEIL) // {BU * qRTok / qRTok} : shiftl_toFix(amount, -int8(decimals())); // {qRTok / qRTok} - (address[] memory erc20s, uint256[] memory deposits) = basketHandler.quote(baskets, CEIL); + (address[] memory erc20s, uint256[] memory deposits) = basketHandler.quote( + baskets, + true, + CEIL + ); address issuer = _msgSender(); for (uint256 i = 0; i < erc20s.length; i++) { @@ -150,6 +154,7 @@ contract RTokenP0 is ComponentP0, ERC20PermitUpgradeable, IRToken { (address[] memory erc20s, uint256[] memory amounts) = main.basketHandler().quote( baskets, + false, FLOOR ); diff --git a/contracts/p0/mixins/TradingLib.sol b/contracts/p0/mixins/TradingLib.sol index c9b2daf8e9..19ffe10f58 100644 --- a/contracts/p0/mixins/TradingLib.sol +++ b/contracts/p0/mixins/TradingLib.sol @@ -59,9 +59,18 @@ library TradingLibP0 { minTradeVolume ); - // Cap sell amount - uint192 maxSell = maxTradeSize(trade.sell, trade.buy, trade.prices.sellLow); // {sellTok} - uint192 s = trade.sellAmount > maxSell ? maxSell : trade.sellAmount; // {sellTok} + // Cap sell amount using the high price + // Under price decay trade.prices.sellHigh can become up to 3x the savedHighPrice before + // becoming FIX_MAX after the full price timeout + uint192 s = trade.sellAmount; + if (trade.prices.sellHigh != FIX_MAX) { + // {sellTok} + uint192 maxSell = maxTradeSize(trade.sell, trade.buy, trade.prices.sellHigh); + require(maxSell > 1, "trade sizing error"); + if (s > maxSell) s = maxSell; + } else { + require(trade.prices.sellLow == 0, "trade pricing error"); + } // Calculate equivalent buyAmount within [0, FIX_MAX] // {buyTok} = {sellTok} * {1} * {UoA/sellTok} / {UoA/buyTok} @@ -269,7 +278,7 @@ library TradingLibP0 { view returns (BasketRange memory range) { - (uint192 buPriceLow, uint192 buPriceHigh) = ctx.bh.price(); // {UoA/BU} + (uint192 buPriceLow, uint192 buPriceHigh) = ctx.bh.price(false); // {UoA/BU} // Cap ctx.basketsHeld.top if (ctx.basketsHeld.top > ctx.rToken.basketsNeeded()) { diff --git a/contracts/p1/AssetRegistry.sol b/contracts/p1/AssetRegistry.sol index 620849d244..c9031e315b 100644 --- a/contracts/p1/AssetRegistry.sol +++ b/contracts/p1/AssetRegistry.sol @@ -252,11 +252,15 @@ contract AssetRegistryP1 is ComponentP1, IAssetRegistry { function _reserveGas() private view returns (uint256) { uint256 gas = gasleft(); + // Call to quantity() restricts gas that is passed along to 63 / 64 of gasleft(). + // Therefore gasleft() must be greater than 64 * GAS_FOR_BH_QTY / 63 + // GAS_FOR_DISABLE_BASKET is a buffer which can be considerably lower without + // security implications. require( - gas > GAS_FOR_DISABLE_BASKET + GAS_FOR_BH_QTY, + gas > (64 * GAS_FOR_BH_QTY) / 63 + GAS_FOR_DISABLE_BASKET, "not enough gas to unregister safely" ); - return gas - GAS_FOR_DISABLE_BASKET; + return GAS_FOR_BH_QTY; } /** diff --git a/contracts/p1/BackingManager.sol b/contracts/p1/BackingManager.sol index 72cccf71d2..d28b12484e 100644 --- a/contracts/p1/BackingManager.sol +++ b/contracts/p1/BackingManager.sol @@ -236,6 +236,7 @@ contract BackingManagerP1 is TradingP1, IBackingManager { for (uint256 i = 0; i < length; ++i) { IAsset asset = assetRegistry.toAsset(erc20s[i]); + // Use same quantity-rounding as BasketHandler.basketsHeldBy() // {tok} = {BU} * {tok/BU} uint192 req = needed.mul(basketHandler.quantity(erc20s[i]), CEIL); uint192 bal = asset.bal(address(this)); @@ -287,6 +288,7 @@ contract BackingManagerP1 is TradingP1, IBackingManager { 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]); + // quantities round up, without any issuance premium } ctx.bals = new uint192[](reg.erc20s.length); for (uint256 i = 0; i < reg.erc20s.length; ++i) { diff --git a/contracts/p1/BasketHandler.sol b/contracts/p1/BasketHandler.sol index 0ddf7e6718..ef600b8e5c 100644 --- a/contracts/p1/BasketHandler.sol +++ b/contracts/p1/BasketHandler.sol @@ -13,6 +13,8 @@ import "../libraries/Fixed.sol"; import "./mixins/BasketLib.sol"; import "./mixins/Component.sol"; +// solhint-disable max-states-count + /** * @title BasketHandler * @notice Handles the basket configuration, definition, and evolution over time. @@ -89,6 +91,9 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { uint48 public lastCollateralized; // {basketNonce} most recent full collateralization // === + // Added in 4.0.0 + + bool public enableIssuancePremium; // ==== Invariants ==== // basket is a valid Basket: @@ -103,7 +108,8 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { function init( IMain main_, uint48 warmupPeriod_, - bool reweightable_ + bool reweightable_, + bool enableIssuancePremium_ ) external initializer { __Component_init(main_); @@ -115,6 +121,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { setWarmupPeriod(warmupPeriod_); reweightable = reweightable_; // immutable thereafter + enableIssuancePremium = enableIssuancePremium_; // Set last status to DISABLED (default) lastStatus = CollateralStatus.DISABLED; @@ -136,7 +143,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { emit BasketSet(nonce, basket.erc20s, refAmts, true); disabled = true; - trackStatus(); + trackStatus(); // does NOT interact with collateral plugins or tokens } /// Switch the basket, only callable directly by governance or after a default @@ -163,6 +170,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { /// Track basket status and collateralization changes // effects: lastStatus' = status(), and lastStatusTimestamp' = current timestamp + /// @dev Does NOT interact with collateral plugins or tokens when basket is disabled /// @custom:refresher function trackStatus() public { // Historical context: This is not the ideal naming for this function but it allowed @@ -182,26 +190,28 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { } } - /// Set the prime basket + /// Set the prime basket, checking target amounts are constant /// @param erc20s The collateral for the new prime basket /// @param targetAmts The target amounts (in) {target/BU} for the new prime basket /// @custom:governance function setPrimeBasket(IERC20[] calldata erc20s, uint192[] calldata targetAmts) external { - _setPrimeBasket(erc20s, targetAmts, true); + _setPrimeBasket(erc20s, targetAmts, false); } - /// Set the prime basket without reweighting targetAmts by UoA of the current basket + /// Set the prime basket, skipping any constant target amount checks if RToken is reweightable + /// Warning: Reweightable RTokens SHOULD use a spell to execute this function to avoid + /// accidentally changing the UoA value of the RToken. /// @param erc20s The collateral for the new prime basket /// @param targetAmts The target amounts (in) {target/BU} for the new prime basket /// @custom:governance function forceSetPrimeBasket(IERC20[] calldata erc20s, uint192[] calldata targetAmts) external { - _setPrimeBasket(erc20s, targetAmts, false); + _setPrimeBasket(erc20s, targetAmts, true); } /// Set the prime basket in the basket configuration, in terms of erc20s and target amounts /// @param erc20s The collateral for the new prime basket /// @param targetAmts The target amounts (in) {target/BU} for the new prime basket - /// @param normalize True iff targetAmts should be normalized by UoA to the reference basket + /// @param disableTargetAmountCheck If true, skips the `requireConstantConfigTargets()` check /// @custom:governance // checks: // caller is OWNER @@ -217,14 +227,16 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { function _setPrimeBasket( IERC20[] calldata erc20s, uint192[] memory targetAmts, - bool normalize + bool disableTargetAmountCheck ) internal { requireGovernanceOnly(); - require(erc20s.length != 0, "empty basket"); - require(erc20s.length == targetAmts.length, "len mismatch"); + require(erc20s.length != 0 && erc20s.length == targetAmts.length, "invalid lengths"); requireValidCollArray(erc20s); - if (!reweightable && config.erc20s.length != 0) { + if ( + (!reweightable || (reweightable && !disableTargetAmountCheck)) && + config.erc20s.length != 0 + ) { // Require targets remain constant BasketLibP1.requireConstantConfigTargets( assetRegistry, @@ -233,20 +245,6 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { erc20s, targetAmts ); - } else if (normalize && config.erc20s.length != 0) { - // Confirm reference basket is SOUND - assetRegistry.refresh(); // will set lastStatus - require(lastStatus == CollateralStatus.SOUND, "unsound basket"); - - // Normalize targetAmts based on UoA value of reference basket - (uint192 low, uint192 high) = _price(false); - assert(low != 0 && high != FIX_MAX); // implied by SOUND status - targetAmts = BasketLibP1.normalizeByPrice( - assetRegistry, - erc20s, - targetAmts, - (low + high + 1) / 2 - ); } // Clean up previous basket config @@ -263,8 +261,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { // This is a nice catch to have, but in general it is possible for // an ERC20 in the prime basket to have its asset unregistered. require(assetRegistry.toAsset(erc20s[i]).isCollateral(), "erc20 is not collateral"); - require(0 < targetAmts[i], "invalid target amount; must be nonzero"); - require(targetAmts[i] <= MAX_TARGET_AMT, "invalid target amount; too large"); + require(0 < targetAmts[i] && targetAmts[i] <= MAX_TARGET_AMT, "invalid target amount"); config.erc20s.push(erc20s[i]); config.targetAmts[erc20s[i]] = targetAmts[i]; @@ -290,8 +287,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { IERC20[] calldata erc20s ) external { requireGovernanceOnly(); - require(max <= MAX_BACKUP_ERC20S, "max too large"); - require(erc20s.length <= MAX_BACKUP_ERC20S, "erc20s too large"); + require(max <= MAX_BACKUP_ERC20S && erc20s.length <= MAX_BACKUP_ERC20S, "too large"); requireValidCollArray(erc20s); BackupConfig storage conf = config.backups[targetName]; conf.max = max; @@ -300,7 +296,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { for (uint256 i = 0; i < erc20s.length; ++i) { // This is a nice catch to have, but in general it is possible for // an ERC20 in the backup config to have its asset altered. - require(assetRegistry.toAsset(erc20s[i]).isCollateral(), "erc20 is not collateral"); + assetRegistry.toColl(erc20s[i]); // reverts if not collateral conf.erc20s.push(erc20s[i]); } emit BackupConfigSet(targetName, max, erc20s); @@ -338,6 +334,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { (block.timestamp >= lastStatusTimestamp + warmupPeriod); } + /// Basket quantity rounded up, without any issuance premium /// @param erc20 The token contract to check for quantity for /// @return {tok/BU} The token-quantity of an ERC20 token in the basket. // Returns 0 if erc20 is not registered or not in the basket @@ -345,12 +342,13 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { // Otherwise returns (token's basket.refAmts / token's Collateral.refPerTok()) function quantity(IERC20 erc20) public view returns (uint192) { try assetRegistry.toColl(erc20) returns (ICollateral coll) { - return _quantity(erc20, coll); + return _quantity(erc20, coll, CEIL); } catch { return FIX_ZERO; } } + /// Basket quantity rounded up, without any issuance premium /// Like quantity(), but unsafe because it DOES NOT CONFIRM THAT THE ASSET IS CORRECT /// @param erc20 The ERC20 token contract for the asset /// @param asset The registered asset plugin contract for the erc20 @@ -360,66 +358,97 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { // Otherwise returns (token's basket.refAmts / token's Collateral.refPerTok()) function quantityUnsafe(IERC20 erc20, IAsset asset) public view returns (uint192) { if (!asset.isCollateral()) return FIX_ZERO; - return _quantity(erc20, ICollateral(address(asset))); + return _quantity(erc20, ICollateral(address(asset)), CEIL); + } + + /// @param coll A collateral that has had refresh() called on it this timestamp + /// @return {1} The multiplier to charge on issuance quantities for a collateral + function issuancePremium(ICollateral coll) public view returns (uint192) { + // `coll` does not need validation + if (!enableIssuancePremium || coll.lastSave() != block.timestamp) return FIX_ONE; + + // Use try-catch for safety since `savedPegPrice()` was only added in 4.0.0 to ICollateral + try coll.savedPegPrice() returns (uint192 pegPrice) { + if (pegPrice == 0) return FIX_ONE; + uint192 targetPerRef = coll.targetPerRef(); // {target/ref} + if (pegPrice >= targetPerRef) return FIX_ONE; + + // {1} = {target/ref} / {target/ref} + return targetPerRef.safeDiv(pegPrice, CEIL); + } catch { + // if savedPegPrice() does not exist on the collateral the error bytes are 0 len + return FIX_ONE; + } } + /// Returns the quantity of collateral token in a BU /// @param erc20 The token contract /// @param coll The registered collateral plugin contract - /// @return {tok/BU} The token-quantity of an ERC20 token in the basket. + /// @return {tok/BU} The token-quantity of an ERC20 token in the basket // Returns 0 if coll is not in the basket // Returns FIX_MAX (in lieu of +infinity) if Collateral.refPerTok() is 0. // Otherwise returns (token's basket.refAmts / token's Collateral.refPerTok()) - function _quantity(IERC20 erc20, ICollateral coll) internal view returns (uint192) { + function _quantity( + IERC20 erc20, + ICollateral coll, + RoundingMode rounding + ) internal view returns (uint192) { uint192 refPerTok = coll.refPerTok(); if (refPerTok == 0) return FIX_MAX; // {tok/BU} = {ref/BU} / {ref/tok} - return basket.refAmts[erc20].div(refPerTok, CEIL); + return basket.refAmts[erc20].div(refPerTok, rounding); } + /// Returns the price of a BU (including issuance premium) + /// Included for backwards compatibility with <4.0.0 /// Should not revert /// @return low {UoA/BU} The lower end of the price estimate /// @return high {UoA/BU} The upper end of the price estimate // returns sum(quantity(erc20) * price(erc20) for erc20 in basket.erc20s) function price() external view returns (uint192 low, uint192 high) { - return _price(false); + return price(true); } + /// Returns the price of a BU /// 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) - function lotPrice() external view returns (uint192 lotLow, uint192 lotHigh) { - return _price(true); - } - - /// Returns the price of a BU, using the lot prices if `useLotPrice` is true - /// @param useLotPrice Whether to use lotPrice() or price() + /// @param applyIssuancePremium Whether to apply the issuance premium to the high price /// @return low {UoA/BU} The lower end of the price estimate /// @return high {UoA/BU} The upper end of the price estimate - function _price(bool useLotPrice) internal view returns (uint192 low, uint192 high) { + // returns sum(quantity(erc20) * price(erc20) for erc20 in basket.erc20s) + function price(bool applyIssuancePremium) public view returns (uint192 low, uint192 high) { uint256 low256; uint256 high256; uint256 len = basket.erc20s.length; for (uint256 i = 0; i < len; ++i) { - uint192 qty = quantity(basket.erc20s[i]); - if (qty == 0) continue; + try assetRegistry.toColl(basket.erc20s[i]) returns (ICollateral coll) { + uint192 qty = _quantity(basket.erc20s[i], coll, CEIL); + if (qty == 0) continue; - (uint192 lowP, uint192 highP) = useLotPrice - ? assetRegistry.toAsset(basket.erc20s[i]).lotPrice() - : assetRegistry.toAsset(basket.erc20s[i]).price(); + (uint192 lowP, uint192 highP) = coll.price(); - low256 += qty.safeMul(lowP, RoundingMode.FLOOR); + low256 += qty.safeMul(lowP, FLOOR); - if (high256 < FIX_MAX) { - if (highP == FIX_MAX) { - high256 = FIX_MAX; - } else { - high256 += qty.safeMul(highP, RoundingMode.CEIL); + if (high256 < FIX_MAX) { + if (highP == FIX_MAX) { + high256 = FIX_MAX; + continue; + } + + if (applyIssuancePremium) { + uint192 premium = issuancePremium(coll); // {1} always CEIL + + // {tok} = {tok} * {1} + if (premium > FIX_ONE) qty = qty.safeMul(premium, CEIL); + } + + high256 += qty.safeMul(highP, CEIL); } + } catch (bytes memory errData) { + // see: docs/solidity-style.md#Catching-Empty-Data + if (errData.length == 0) revert(); // solhint-disable-line reason-string + continue; } } @@ -428,9 +457,9 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { high = high256 >= FIX_MAX ? FIX_MAX : uint192(high256); } - /// Return the current issuance/redemption value of `amount` BUs - /// Any approvals needed to issue RTokens should be set to the values returned by this function - /// @dev Subset of logic of quoteCustomRedemption; more gas efficient for current nonce + /// Return the current issuance/redemption quantities for `amount` BUs + /// Included for backwards compatibility with <4.0.0 + /// @param rounding If CEIL, apply issuance premium /// @param amount {BU} /// @return erc20s The backing collateral erc20s /// @return quantities {qTok} ERC20 token quantities equal to `amount` BUs @@ -440,6 +469,21 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { view returns (address[] memory erc20s, uint256[] memory quantities) { + return quote(amount, rounding == CEIL, rounding); + } + + /// Return the current issuance/redemption quantities for `amount` BUs + /// @dev Subset of logic of quoteCustomRedemption; more gas efficient for current nonce + /// @param amount {BU} + /// @param applyIssuancePremium Whether to apply the issuance premium + /// @return erc20s The backing collateral erc20s + /// @return quantities {qTok} ERC20 token quantities equal to `amount` BUs + // Returns (erc20s, [quantity(e) * amount {as qTok} for e in erc20s]) + function quote( + uint192 amount, + bool applyIssuancePremium, + RoundingMode rounding + ) public view returns (address[] memory erc20s, uint256[] memory quantities) { uint256 length = basket.erc20s.length; erc20s = new address[](length); quantities = new uint256[](length); @@ -448,10 +492,22 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { erc20s[i] = address(basket.erc20s[i]); ICollateral coll = assetRegistry.toColl(IERC20(erc20s[i])); - // {qTok} = {tok/BU} * {BU} * {tok} * {qTok/tok} - quantities[i] = _quantity(basket.erc20s[i], coll) - .safeMul(amount, rounding) - .shiftl_toUint(int8(IERC20Metadata(address(basket.erc20s[i])).decimals()), rounding); + // {tok} = {tok/BU} * {BU} + uint192 q = _quantity(basket.erc20s[i], coll, rounding).safeMul(amount, rounding); + + // Prevent toxic issuance by charging more when collateral is under peg + if (applyIssuancePremium) { + uint192 premium = issuancePremium(coll); // {1} always CEIL by definition + + // {tok} = {tok} * {1} + if (premium > FIX_ONE) q = q.safeMul(premium, rounding); + } + + // {qTok} = {tok} * {qTok/tok} + quantities[i] = q.shiftl_toUint( + int8(IERC20Metadata(address(basket.erc20s[i])).decimals()), + rounding + ); } } @@ -467,7 +523,7 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { uint192[] memory portions, uint192 amount ) external view returns (address[] memory erc20s, uint256[] memory quantities) { - require(basketNonces.length == portions.length, "bad portions len"); + require(basketNonces.length == portions.length, "invalid lengths"); IERC20[] memory erc20sAll = new IERC20[](assetRegistry.size()); ICollateral[] memory collsAll = new ICollateral[](erc20sAll.length); @@ -541,8 +597,6 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { quantities[i] = amount .safeMulDiv(refAmtsAll[i], collsAll[i].refPerTok(), FLOOR) .shiftl_toUint(int8(collsAll[i].erc20Decimals()), FLOOR); - // marginally more penalizing than its sibling calculation that uses _quantity() - // because does not intermediately CEIL as part of the division } } @@ -562,14 +616,10 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { ICollateral coll = assetRegistry.toColl(basket.erc20s[i]); if (coll.status() == CollateralStatus.DISABLED) return BasketRange(FIX_ZERO, FIX_MAX); - uint192 refPerTok = coll.refPerTok(); - // If refPerTok is 0, then we have zero of coll's reference unit. - // We know that basket.refAmts[basket.erc20s[i]] > 0, so we have no baskets. - if (refPerTok == 0) return BasketRange(FIX_ZERO, FIX_MAX); + // {tok/BU} + uint192 q = _quantity(basket.erc20s[i], coll, CEIL); + if (q == FIX_MAX) return BasketRange(FIX_ZERO, FIX_MAX); - // {tok/BU} = {ref/BU} / {ref/tok}. 0-division averted by condition above. - uint192 q = basket.refAmts[basket.erc20s[i]].div(refPerTok, CEIL); - // q > 0 because q = (n).div(_, CEIL) and n > 0 // {BU} = {tok} / {tok/BU} uint192 inBUs = coll.bal(account).div(q); baskets.bottom = fixMin(baskets.bottom, inBUs); @@ -587,6 +637,13 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { warmupPeriod = val; } + /// @custom:governance + function setIssuancePremiumEnabled(bool val) public { + requireGovernanceOnly(); + emit EnableIssuancePremiumSet(enableIssuancePremium, val); + enableIssuancePremium = val; + } + // === Private === // contract-size-saver diff --git a/contracts/p1/Broker.sol b/contracts/p1/Broker.sol index 1d47d2761d..d6a177c854 100644 --- a/contracts/p1/Broker.sol +++ b/contracts/p1/Broker.sol @@ -276,7 +276,7 @@ contract BrokerP1 is ComponentP1, IBroker { ); require(dutchAuctionLength != 0, "dutch auctions not enabled"); require( - priceNotDecayed(req.sell) && priceNotDecayed(req.buy), + pricedAtTimestamp(req.sell) && pricedAtTimestamp(req.buy), "dutch auctions require live prices" ); @@ -294,8 +294,8 @@ 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 true iff the asset has been priced at this timestamp, or it's the RTokenAsset + function pricedAtTimestamp(IAsset asset) private view returns (bool) { return asset.lastSave() == block.timestamp || address(asset.erc20()) == address(rToken); } diff --git a/contracts/p1/Deployer.sol b/contracts/p1/Deployer.sol index bf5608a52f..073c7c19e4 100644 --- a/contracts/p1/Deployer.sol +++ b/contracts/p1/Deployer.sol @@ -192,7 +192,12 @@ contract DeployerP1 is IDeployer, Versioned { ); // Init Basket Handler - components.basketHandler.init(main, params.warmupPeriod, params.reweightable); + components.basketHandler.init( + main, + params.warmupPeriod, + params.reweightable, + params.enableIssuancePremium + ); // Init Revenue Traders components.rsrTrader.init(main, rsr, params.maxTradeSlippage, params.minTradeVolume); diff --git a/contracts/p1/Distributor.sol b/contracts/p1/Distributor.sol index b7f66d888f..8040eeb03f 100644 --- a/contracts/p1/Distributor.sol +++ b/contracts/p1/Distributor.sol @@ -52,12 +52,13 @@ contract DistributorP1 is ComponentP1, IDistributor { /// Set the RevenueShare for destination `dest`. Destinations `FURNACE` and `ST_RSR` refer to /// main.furnace() and main.stRSR(). + /// Consider calling `BackingManager.forwardRevenue()` before to ensure fair past distribution /// @custom:governance // checks: invariants hold in post-state // effects: // destinations' = destinations.add(dest) // distribution' = distribution.set(dest, share) - function setDistribution(address dest, RevenueShare memory share) external governance { + function setDistribution(address dest, RevenueShare calldata share) external governance { // solhint-disable-next-line no-empty-blocks try main.rsrTrader().distributeTokenToBuy() {} catch {} // solhint-disable-next-line no-empty-blocks @@ -71,11 +72,12 @@ contract DistributorP1 is ComponentP1, IDistributor { /// Set RevenueShares for destinations. Destinations `FURNACE` and `ST_RSR` refer to /// main.furnace() and main.stRSR(). + /// Consider calling `BackingManager.forwardRevenue()` before to ensure fair past distribution /// @custom:governance // checks: invariants hold in post-state // effects: - // destinations' = dests - // distribution' = shares + // destinations' = destinations.add(dests[i]) for i < dests.length + // distribution' = distribution.set(dests[i], shares[i]) for i < dests.length function setDistributions(address[] calldata dests, RevenueShare[] calldata shares) external governance @@ -174,11 +176,10 @@ contract DistributorP1 is ComponentP1, IDistributor { DAOFeeRegistry daoFeeRegistry = main.daoFeeRegistry(); if (address(daoFeeRegistry) != address(0)) { - // DAO Fee - if (isRSR) { + if (totalShares > paidOutShares) { (address recipient, , ) = main.daoFeeRegistry().getFeeDetails(address(rToken)); - if (recipient != address(0) && tokensPerShare * (totalShares - paidOutShares) > 0) { + if (recipient != address(0)) { IERC20Upgradeable(address(erc20)).safeTransferFrom( caller, recipient, @@ -215,6 +216,7 @@ contract DistributorP1 is ComponentP1, IDistributor { .daoFeeRegistry() .getFeeDetails(address(rToken)); + // Small DAO fees <1% not recommended; ~10% precision due to rounding at 0.1% fee if (feeRecipient != address(0) && feeNumerator != 0) { revTotals.rsrTotal += uint24( (feeNumerator * uint256(revTotals.rTokenTotal + revTotals.rsrTotal)) / diff --git a/contracts/p1/Main.sol b/contracts/p1/Main.sol index 45321d0bfa..efdc2efd89 100644 --- a/contracts/p1/Main.sol +++ b/contracts/p1/Main.sol @@ -105,6 +105,7 @@ contract MainP1 is Versioned, Initializable, Auth, ComponentRegistry, UUPSUpgrad ); _upgradeProxy(address(this), address(implementation.main)); + require(keccak256(abi.encodePacked(this.version())) == versionHash, "upgrade mismatch"); } function upgradeRTokenTo( diff --git a/contracts/p1/RToken.sol b/contracts/p1/RToken.sol index da801bded8..316241e129 100644 --- a/contracts/p1/RToken.sol +++ b/contracts/p1/RToken.sol @@ -135,8 +135,10 @@ contract RTokenP1 is ComponentP1, ERC20PermitUpgradeable, IRToken { : _safeWrap(amount); emit Issuance(issuer, recipient, amount, amtBaskets); + // Get quote from BasketHandler including issuance premium (address[] memory erc20s, uint256[] memory deposits) = basketHandler.quote( amtBaskets, + true, CEIL ); @@ -201,7 +203,11 @@ contract RTokenP1 is ComponentP1, ERC20PermitUpgradeable, IRToken { uint192 baskets = _scaleDown(caller, amount); emit Redemption(caller, recipient, amount, baskets); - (address[] memory erc20s, uint256[] memory amounts) = basketHandler.quote(baskets, FLOOR); + (address[] memory erc20s, uint256[] memory amounts) = basketHandler.quote( + baskets, + false, + FLOOR + ); // === Interactions === diff --git a/contracts/p1/mixins/BasketLib.sol b/contracts/p1/mixins/BasketLib.sol index 40906d1cd3..39dd5036b1 100644 --- a/contracts/p1/mixins/BasketLib.sol +++ b/contracts/p1/mixins/BasketLib.sol @@ -345,6 +345,7 @@ library BasketLibP1 { } /// Normalize the target amounts to maintain constant UoA value with the current config + /// @dev Unused; left in for future use in reweightable RToken forceSetPrimeBasket() spell /// @param price {UoA/BU} Price of the reference basket (point estimate) /// @return newTargetAmts {target/BU} The new target amounts for the normalized basket function normalizeByPrice( diff --git a/contracts/p1/mixins/RecollateralizationLib.sol b/contracts/p1/mixins/RecollateralizationLib.sol index 7d0f6eef71..a03a49279e 100644 --- a/contracts/p1/mixins/RecollateralizationLib.sol +++ b/contracts/p1/mixins/RecollateralizationLib.sol @@ -113,7 +113,7 @@ library RecollateralizationLibP1 { // 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} + (uint192 buPriceLow, uint192 buPriceHigh) = ctx.bh.price(false); // {UoA/BU} require(buPriceLow != 0 && buPriceHigh != FIX_MAX, "BUs unpriced"); uint192 basketsNeeded = ctx.rToken.basketsNeeded(); // {BU} @@ -269,6 +269,8 @@ library RecollateralizationLibP1 { // Prefer selling assets in this order: DISABLED -> SOUND -> IFFY. // 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. + // Warning: If the trading algorithm is changed to trade unpriced (0, FIX_MAX) assets it can + // result in losses in GnosisTrade. Unpriced assets should not be sold in rebalancing. function nextTradePair( TradingContext memory ctx, Registry memory reg, diff --git a/contracts/p1/mixins/TradeLib.sol b/contracts/p1/mixins/TradeLib.sol index f02c14f5d0..e989ea81e9 100644 --- a/contracts/p1/mixins/TradeLib.sol +++ b/contracts/p1/mixins/TradeLib.sol @@ -58,9 +58,18 @@ library TradeLib { minTradeVolume ); - // Cap sell amount - uint192 maxSell = maxTradeSize(trade.sell, trade.buy, trade.prices.sellLow); // {sellTok} - uint192 s = trade.sellAmount > maxSell ? maxSell : trade.sellAmount; // {sellTok} + // Cap sell amount using the high price + // Under price decay trade.prices.sellHigh can become up to 3x the savedHighPrice before + // becoming FIX_MAX after the full price timeout + uint192 s = trade.sellAmount; + if (trade.prices.sellHigh != FIX_MAX) { + // {sellTok} + uint192 maxSell = maxTradeSize(trade.sell, trade.buy, trade.prices.sellHigh); + require(maxSell > 1, "trade sizing error"); + if (s > maxSell) s = maxSell; + } else { + require(trade.prices.sellLow == 0, "trade pricing error"); + } // Calculate equivalent buyAmount within [0, FIX_MAX] // {buyTok} = {sellTok} * {1} * {UoA/sellTok} / {UoA/buyTok} diff --git a/contracts/plugins/assets/AppreciatingFiatCollateral.sol b/contracts/plugins/assets/AppreciatingFiatCollateral.sol index 85822345ae..4c6b9b76b8 100644 --- a/contracts/plugins/assets/AppreciatingFiatCollateral.sol +++ b/contracts/plugins/assets/AppreciatingFiatCollateral.sol @@ -45,6 +45,7 @@ abstract contract AppreciatingFiatCollateral is FiatCollateral { /// Can revert, used by other contract functions in order to catch errors /// Should not return FIX_MAX for low /// Should only return FIX_MAX for high if low is 0 + /// Should NOT be manipulable by MEV /// @dev Override this when pricing is more complicated than just a single oracle /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate @@ -103,6 +104,7 @@ abstract contract AppreciatingFiatCollateral is FiatCollateral { if (high != FIX_MAX) { savedLowPrice = low; savedHighPrice = high; + savedPegPrice = pegPrice; lastSave = uint48(block.timestamp); } else { // must be unpriced diff --git a/contracts/plugins/assets/Asset.sol b/contracts/plugins/assets/Asset.sol index 54f70c70de..c0f6d2b4cb 100644 --- a/contracts/plugins/assets/Asset.sol +++ b/contracts/plugins/assets/Asset.sol @@ -69,6 +69,7 @@ contract Asset is IAsset, VersionedAsset { /// Can revert, used by other contract functions in order to catch errors /// Should not return FIX_MAX for low /// Should only return FIX_MAX for high if low is 0 + /// Should NOT be manipulable by MEV /// @dev The third (unused) variable is only here for compatibility with Collateral /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate diff --git a/contracts/plugins/assets/EURFiatCollateral.sol b/contracts/plugins/assets/EURFiatCollateral.sol index 23f7e23aac..145e46272e 100644 --- a/contracts/plugins/assets/EURFiatCollateral.sol +++ b/contracts/plugins/assets/EURFiatCollateral.sol @@ -36,6 +36,7 @@ contract EURFiatCollateral is FiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate /// @return pegPrice {target/ref} diff --git a/contracts/plugins/assets/FiatCollateral.sol b/contracts/plugins/assets/FiatCollateral.sol index 91666dca0b..191beb6dad 100644 --- a/contracts/plugins/assets/FiatCollateral.sol +++ b/contracts/plugins/assets/FiatCollateral.sol @@ -60,6 +60,8 @@ contract FiatCollateral is ICollateral, Asset { // {target/ref} The top of the peg uint192 public pegTop; // fuzz needs this to not be immutable! + uint192 public savedPegPrice; // {target/ref} The peg price of the token during the last update + /// @param config.chainlinkFeed Feed units: {UoA/ref} constructor(CollateralConfig memory config) Asset( @@ -96,6 +98,7 @@ contract FiatCollateral is ICollateral, Asset { /// Can revert, used by other contract functions in order to catch errors /// Should not return FIX_MAX for low /// Should only return FIX_MAX for high if low is 0 + /// Should NOT be manipulable by MEV /// @dev Override this when pricing is more complicated than just a single oracle /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate @@ -137,6 +140,7 @@ contract FiatCollateral is ICollateral, Asset { if (high != FIX_MAX) { savedLowPrice = low; savedHighPrice = high; + savedPegPrice = pegPrice; lastSave = uint48(block.timestamp); } else { // must be unpriced diff --git a/contracts/plugins/assets/L2LSDCollateral.sol b/contracts/plugins/assets/L2LSDCollateral.sol index 81047c456d..4167bea7cd 100644 --- a/contracts/plugins/assets/L2LSDCollateral.sol +++ b/contracts/plugins/assets/L2LSDCollateral.sol @@ -70,6 +70,7 @@ abstract contract L2LSDCollateral is AppreciatingFiatCollateral { if (high != FIX_MAX) { savedLowPrice = low; savedHighPrice = high; + savedPegPrice = pegPrice; lastSave = uint48(block.timestamp); } else { // must be unpriced diff --git a/contracts/plugins/assets/NonFiatCollateral.sol b/contracts/plugins/assets/NonFiatCollateral.sol index 6b74a4d20c..c1a2464a30 100644 --- a/contracts/plugins/assets/NonFiatCollateral.sol +++ b/contracts/plugins/assets/NonFiatCollateral.sol @@ -36,6 +36,7 @@ contract NonFiatCollateral is FiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate /// @return pegPrice {target/ref} diff --git a/contracts/plugins/assets/RTokenAsset.sol b/contracts/plugins/assets/RTokenAsset.sol index ac2fb001e6..5325b166eb 100644 --- a/contracts/plugins/assets/RTokenAsset.sol +++ b/contracts/plugins/assets/RTokenAsset.sol @@ -53,10 +53,16 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { /// less RSR overcollateralization in % terms than the average (weighted) oracleError. /// This arises from the use of oracleErrors inside of `basketRange()` and inside /// `basketHandler.price()`. When `range.bottom == range.top` then there is no compounding. + /// @dev This method should not be relied upon to provide precise bounds for secondary market + /// prices. It is a "reasonable" estimate of the range the RToken is expected to trade in + /// given what the protocol knows about its internal state, but strictly speaking RTokens + /// can trade outside this range for periods of time (ie increased demand during IFFY state) + /// It is therefore NOT recommended to rely on this pricing method to price RTokens + /// in lending markets or anywhere where secondary market price is the central concern. /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate function tryPrice() external view virtual returns (uint192 low, uint192 high) { - (uint192 lowBUPrice, uint192 highBUPrice) = basketHandler.price(); // {UoA/BU} + (uint192 lowBUPrice, uint192 highBUPrice) = basketHandler.price(true); // {UoA/BU} require(lowBUPrice != 0 && highBUPrice != FIX_MAX, "invalid price"); assert(lowBUPrice <= highBUPrice); // not obviously true just by inspection @@ -66,8 +72,8 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { if (supply == 0) return (lowBUPrice, highBUPrice); - // The RToken's price is not symmetric like other assets! - // range.bottom is lower because of the slippage from the shortfall + // The RToken's basket range is not symmetric! + // range.bottom is additionally lower because of the slippage from the shortfall BasketRange memory range = basketRange(); // {BU} // {UoA/tok} = {BU} * {UoA/BU} / {tok} @@ -87,7 +93,7 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { } /// Should not revert - /// @dev See `tryPrice` caveat about possible compounding error in calculating price + /// @dev See `tryPrice` caveats /// @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) { diff --git a/contracts/plugins/assets/SelfReferentialCollateral.sol b/contracts/plugins/assets/SelfReferentialCollateral.sol index fb3eb2b92b..06007e07cb 100644 --- a/contracts/plugins/assets/SelfReferentialCollateral.sol +++ b/contracts/plugins/assets/SelfReferentialCollateral.sol @@ -20,6 +20,7 @@ contract SelfReferentialCollateral is FiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate /// @return pegPrice {target/ref} diff --git a/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol b/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol index ccce6ef615..871d0e248e 100644 --- a/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol +++ b/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol @@ -41,6 +41,7 @@ contract AnkrStakedEthCollateral is AppreciatingFiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @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 diff --git a/contracts/plugins/assets/cbeth/CBETHCollateral.sol b/contracts/plugins/assets/cbeth/CBETHCollateral.sol index 74e96eb1fa..87cf8e4d24 100644 --- a/contracts/plugins/assets/cbeth/CBETHCollateral.sol +++ b/contracts/plugins/assets/cbeth/CBETHCollateral.sol @@ -41,6 +41,7 @@ contract CBEthCollateral is AppreciatingFiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @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 diff --git a/contracts/plugins/assets/cbeth/CBETHCollateralL2.sol b/contracts/plugins/assets/cbeth/CBETHCollateralL2.sol index 1d30070013..67fa176e9a 100644 --- a/contracts/plugins/assets/cbeth/CBETHCollateralL2.sol +++ b/contracts/plugins/assets/cbeth/CBETHCollateralL2.sol @@ -50,6 +50,7 @@ contract CBEthCollateralL2 is L2LSDCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @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 diff --git a/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol b/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol index 859e5edc1b..e2fdcd5f53 100644 --- a/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol +++ b/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol @@ -37,6 +37,7 @@ contract CTokenNonFiatCollateral is CTokenFiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate /// @return pegPrice {target/ref} diff --git a/contracts/plugins/assets/compoundv2/CTokenSelfReferentialCollateral.sol b/contracts/plugins/assets/compoundv2/CTokenSelfReferentialCollateral.sol index 8766be3d71..7a358abf46 100644 --- a/contracts/plugins/assets/compoundv2/CTokenSelfReferentialCollateral.sol +++ b/contracts/plugins/assets/compoundv2/CTokenSelfReferentialCollateral.sol @@ -41,6 +41,7 @@ contract CTokenSelfReferentialCollateral is AppreciatingFiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate /// @return pegPrice {target/ref} diff --git a/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol b/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol index 416b5d5f36..42784057f5 100644 --- a/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol +++ b/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol @@ -84,6 +84,7 @@ contract CTokenV3Collateral is AppreciatingFiatCollateral { if (high != FIX_MAX) { savedLowPrice = low; savedHighPrice = high; + savedPegPrice = pegPrice; lastSave = uint48(block.timestamp); } else { // must be unpriced diff --git a/contracts/plugins/assets/curve/CurveAppreciatingRTokenFiatCollateral.sol b/contracts/plugins/assets/curve/CurveAppreciatingRTokenFiatCollateral.sol index e74f88aa4c..887371ee0c 100644 --- a/contracts/plugins/assets/curve/CurveAppreciatingRTokenFiatCollateral.sol +++ b/contracts/plugins/assets/curve/CurveAppreciatingRTokenFiatCollateral.sol @@ -71,7 +71,7 @@ contract CurveAppreciatingRTokenFiatCollateral is CurveStableCollateral { } // Check for soft default + save prices - try this.tryPrice() returns (uint192 low, uint192 high, uint192) { + try this.tryPrice() returns (uint192 low, uint192 high, uint192 pegPrice) { // {UoA/tok}, {UoA/tok}, {UoA/tok} // (0, 0) is a valid price; (0, FIX_MAX) is unpriced @@ -79,6 +79,7 @@ contract CurveAppreciatingRTokenFiatCollateral is CurveStableCollateral { if (high != FIX_MAX) { savedLowPrice = low; savedHighPrice = high; + savedPegPrice = pegPrice; lastSave = uint48(block.timestamp); } else { // must be unpriced diff --git a/contracts/plugins/assets/curve/CurveRecursiveCollateral.sol b/contracts/plugins/assets/curve/CurveRecursiveCollateral.sol index c1cf7461c6..3e593e2c15 100644 --- a/contracts/plugins/assets/curve/CurveRecursiveCollateral.sol +++ b/contracts/plugins/assets/curve/CurveRecursiveCollateral.sol @@ -48,9 +48,10 @@ contract CurveRecursiveCollateral is CurveStableCollateral { /// Can revert, used by other contract functions in order to catch errors /// Should not return FIX_MAX for low /// Should only return FIX_MAX for high if low is 0 + /// Should NOT be manipulable by MEV /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate - /// @return {target/ref} Unused. Always 0 + /// @return pegPrice {target/ref} The actual price observed in the peg function tryPrice() external view @@ -59,7 +60,7 @@ contract CurveRecursiveCollateral is CurveStableCollateral { returns ( uint192 low, uint192 high, - uint192 + uint192 pegPrice ) { // This pricing method is MEV-resistant, but only gives a lower-bound @@ -69,6 +70,7 @@ contract CurveRecursiveCollateral is CurveStableCollateral { // Get reference token price (uint192 refLow, uint192 refHigh) = this.tokenPrice(0); // reference token + pegPrice = refLow.plus(refHigh).divu(2, FLOOR); // Multiply by the underlyingRefPerTok() uint192 rate = underlyingRefPerTok(); @@ -76,7 +78,6 @@ contract CurveRecursiveCollateral is CurveStableCollateral { high = refHigh.mul(rate, CEIL); assert(low <= high); // not obviously true by inspection - return (low, high, 0); } /// Should not revert @@ -122,7 +123,7 @@ contract CurveRecursiveCollateral is CurveStableCollateral { // === Check for soft default === // Check for soft default + save prices - try this.tryPrice() returns (uint192 low, uint192 high, uint192) { + try this.tryPrice() returns (uint192 low, uint192 high, uint192 pegPrice) { // {UoA/tok}, {UoA/tok}, {UoA/tok} // (0, 0) is a valid price; (0, FIX_MAX) is unpriced @@ -130,6 +131,7 @@ contract CurveRecursiveCollateral is CurveStableCollateral { if (high < FIX_MAX) { savedLowPrice = low; savedHighPrice = high; + savedPegPrice = pegPrice; lastSave = uint48(block.timestamp); } else { // must be unpriced diff --git a/contracts/plugins/assets/curve/CurveStableCollateral.sol b/contracts/plugins/assets/curve/CurveStableCollateral.sol index bcf46a6dd0..577c1db300 100644 --- a/contracts/plugins/assets/curve/CurveStableCollateral.sol +++ b/contracts/plugins/assets/curve/CurveStableCollateral.sol @@ -54,10 +54,11 @@ contract CurveStableCollateral is AppreciatingFiatCollateral, PoolTokens { /// Can revert, used by other contract functions in order to catch errors /// Should not return FIX_MAX for low /// Should only return FIX_MAX for high if low is 0 + /// Should NOT be manipulable by MEV /// @dev Override this when pricing is more complicated than just a single pool /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate - /// @return {target/ref} Unused. Always 0 + /// @return pegPrice {target/ref} The actual price observed in the peg function tryPrice() external view @@ -66,7 +67,7 @@ contract CurveStableCollateral is AppreciatingFiatCollateral, PoolTokens { returns ( uint192 low, uint192 high, - uint192 + uint192 pegPrice ) { // Assumption: the pool is balanced @@ -86,6 +87,7 @@ contract CurveStableCollateral is AppreciatingFiatCollateral, PoolTokens { // 1. A moving average metric (unavailable in the cases we care about) // 2. Mapping oracle prices to expected pool balances using precise knowledge about // the shape of the trading curve. (maybe we can do this in the future) + // TODO update this approach to be MEV-resistant // {UoA} (uint192 aumLow, uint192 aumHigh) = totalBalancesValue(); @@ -99,7 +101,8 @@ contract CurveStableCollateral is AppreciatingFiatCollateral, PoolTokens { high = aumHigh.div(supply, CEIL); assert(low <= high); // not obviously true just by inspection - return (low, high, 0); + pegPrice = 0; // can't deduce from MEV-manipulable pricing unfortunately + // no issuance premium! more dangerous to be used inside RTokens as a result } /// Should not revert @@ -125,7 +128,7 @@ contract CurveStableCollateral is AppreciatingFiatCollateral, PoolTokens { } // Check for soft default + save prices - try this.tryPrice() returns (uint192 low, uint192 high, uint192) { + try this.tryPrice() returns (uint192 low, uint192 high, uint192 pegPrice) { // {UoA/tok}, {UoA/tok}, {UoA/tok} // (0, 0) is a valid price; (0, FIX_MAX) is unpriced @@ -133,6 +136,7 @@ contract CurveStableCollateral is AppreciatingFiatCollateral, PoolTokens { if (high != FIX_MAX) { savedLowPrice = low; savedHighPrice = high; + savedPegPrice = pegPrice; lastSave = uint48(block.timestamp); } else { // must be unpriced diff --git a/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol b/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol index 3a49b1bd77..fa5d3b9a1d 100644 --- a/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol +++ b/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol @@ -68,6 +68,7 @@ contract CurveStableMetapoolCollateral is CurveStableCollateral { /// Can revert, used by other contract functions in order to catch errors /// Should not return FIX_MAX for low /// Should only return FIX_MAX for high if low is 0 + /// Should NOT be manipulable by MEV /// @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 @@ -99,6 +100,7 @@ contract CurveStableMetapoolCollateral is CurveStableCollateral { // 1. A moving average metric (unavailable in the cases we care about) // 2. Mapping oracle prices to expected pool balances using precise knowledge about // the shape of the trading curve. (maybe we can do this in the future) + // TODO update this approach to be MEV-resistant // {UoA/pairedTok} (uint192 lowPaired, uint192 highPaired) = tryPairedPrice(); @@ -108,7 +110,11 @@ contract CurveStableMetapoolCollateral is CurveStableCollateral { (uint192 aumLow, uint192 aumHigh) = _metapoolBalancesValue(lowPaired, highPaired); // {tok} -- FLOOR - uint192 supply = shiftl_toFix(metapoolToken.totalSupply(), -int8(metapoolToken.decimals())); + uint192 supply = shiftl_toFix( + metapoolToken.totalSupply(), + -int8(metapoolToken.decimals()), + FLOOR + ); // We can always assume that the total supply is sufficiently non-zero // {UoA/tok} = {UoA} / {tok} @@ -116,12 +122,14 @@ contract CurveStableMetapoolCollateral is CurveStableCollateral { high = aumHigh.div(supply, CEIL); assert(low <= high); // not obviously true just by inspection - return (low, high, 0); + pegPrice = 0; // can't deduce from MEV-manipulable pricing unfortunately + // no issuance premium! more dangerous to be used inside RTokens as a result } /// Can revert, used by `_anyDepeggedOutsidePool()` /// Should not return FIX_MAX for low /// Should only return FIX_MAX for high if low is 0 + /// Should NOT be manipulable by MEV /// @return lowPaired {UoA/pairedTok} The low price estimate of the paired token /// @return highPaired {UoA/pairedTok} The high price estimate of the paired token function tryPairedPrice() public view virtual returns (uint192 lowPaired, uint192 highPaired) { @@ -170,7 +178,11 @@ contract CurveStableMetapoolCollateral is CurveStableCollateral { (uint192 underlyingAumLow, uint192 underlyingAumHigh) = totalBalancesValue(); // {tokUnderlying} -- FLOOR - uint192 underlyingSupply = shiftl_toFix(lpToken.totalSupply(), -int8(lpToken.decimals())); + uint192 underlyingSupply = shiftl_toFix( + lpToken.totalSupply(), + -int8(lpToken.decimals()), + FLOOR + ); // We can always assume that the underlying supply is sufficiently non-zero // {UoA/tokUnderlying} = {UoA} / {tokUnderlying} @@ -178,14 +190,22 @@ contract CurveStableMetapoolCollateral is CurveStableCollateral { uint192 underlyingHigh = underlyingAumHigh.div(underlyingSupply, CEIL); // {tokUnderlying} -- FLOOR - uint192 balUnderlying = shiftl_toFix(metapoolToken.balances(1), -int8(lpToken.decimals())); + uint192 balUnderlying = shiftl_toFix( + metapoolToken.balances(1), + -int8(lpToken.decimals()), + FLOOR + ); // {UoA} = {UoA/tokUnderlying} * {tokUnderlying} aumLow = underlyingLow.mul(balUnderlying, FLOOR); aumHigh = underlyingHigh.mul(balUnderlying, CEIL); // {pairedTok} -- FLOOR - uint192 pairedBal = shiftl_toFix(metapoolToken.balances(0), -int8(pairedToken.decimals())); + uint192 pairedBal = shiftl_toFix( + metapoolToken.balances(0), + -int8(pairedToken.decimals()), + FLOOR + ); // Add-in contribution from pairedTok // {UoA} = {UoA} + {UoA/pairedTok} * {pairedTok} diff --git a/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol b/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol index 50300f0765..63c5024b52 100644 --- a/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol +++ b/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol @@ -73,7 +73,7 @@ contract CurveStableRTokenMetapoolCollateral is CurveStableMetapoolCollateral { } // Check for soft default + save prices - try this.tryPrice() returns (uint192 low, uint192 high, uint192) { + try this.tryPrice() returns (uint192 low, uint192 high, uint192 pegPrice) { // {UoA/tok}, {UoA/tok}, {UoA/tok} // (0, 0) is a valid price; (0, FIX_MAX) is unpriced @@ -81,6 +81,7 @@ contract CurveStableRTokenMetapoolCollateral is CurveStableMetapoolCollateral { if (high != FIX_MAX) { savedLowPrice = low; savedHighPrice = high; + savedPegPrice = pegPrice; lastSave = uint48(block.timestamp); } else { // must be unpriced diff --git a/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol b/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol index c5ee972b48..550e896a9c 100644 --- a/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol +++ b/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol @@ -47,6 +47,7 @@ contract SFraxEthCollateral is AppreciatingFiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate /// @return pegPrice {target/ref} FIX_ONE until an oracle becomes available diff --git a/contracts/plugins/assets/lido/L2LidoStakedEthCollateral.sol b/contracts/plugins/assets/lido/L2LidoStakedEthCollateral.sol index d66bcb8e86..ee3b9d533a 100644 --- a/contracts/plugins/assets/lido/L2LidoStakedEthCollateral.sol +++ b/contracts/plugins/assets/lido/L2LidoStakedEthCollateral.sol @@ -68,6 +68,7 @@ contract L2LidoStakedEthCollateral is AppreciatingFiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @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 diff --git a/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol b/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol index 08e465790d..e25cef35b2 100644 --- a/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol +++ b/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol @@ -44,6 +44,7 @@ contract LidoStakedEthCollateral is AppreciatingFiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @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 diff --git a/contracts/plugins/assets/meta-morpho/MetaMorphoSelfReferentialCollateral.sol b/contracts/plugins/assets/meta-morpho/MetaMorphoSelfReferentialCollateral.sol index 8db21fd6fe..d691d5d885 100644 --- a/contracts/plugins/assets/meta-morpho/MetaMorphoSelfReferentialCollateral.sol +++ b/contracts/plugins/assets/meta-morpho/MetaMorphoSelfReferentialCollateral.sol @@ -29,6 +29,7 @@ contract MetaMorphoSelfReferentialCollateral is ERC4626FiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate /// @return pegPrice {target/ref} diff --git a/contracts/plugins/assets/morpho-aave/MorphoNonFiatCollateral.sol b/contracts/plugins/assets/morpho-aave/MorphoNonFiatCollateral.sol index 658309b15a..1710441b05 100644 --- a/contracts/plugins/assets/morpho-aave/MorphoNonFiatCollateral.sol +++ b/contracts/plugins/assets/morpho-aave/MorphoNonFiatCollateral.sol @@ -39,6 +39,7 @@ contract MorphoNonFiatCollateral is MorphoFiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @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 diff --git a/contracts/plugins/assets/morpho-aave/MorphoSelfReferentialCollateral.sol b/contracts/plugins/assets/morpho-aave/MorphoSelfReferentialCollateral.sol index 37a021b364..0a75dc6afb 100644 --- a/contracts/plugins/assets/morpho-aave/MorphoSelfReferentialCollateral.sol +++ b/contracts/plugins/assets/morpho-aave/MorphoSelfReferentialCollateral.sol @@ -10,8 +10,6 @@ import { shiftl_toFix, FIX_ONE, FLOOR, FixLib, CEIL } from "../../../libraries/F // solhint-enable max-line-length -// solhint-enable max-line-length - /** * @title MorphoSelfReferentialCollateral * @notice Collateral plugin for a Morpho pool with self referential collateral, like WETH @@ -41,6 +39,7 @@ contract MorphoSelfReferentialCollateral is AppreciatingFiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate /// @return pegPrice {target/ref} diff --git a/contracts/plugins/assets/mountain/README.md b/contracts/plugins/assets/mountain/README.md new file mode 100644 index 0000000000..03ebfc35f0 --- /dev/null +++ b/contracts/plugins/assets/mountain/README.md @@ -0,0 +1,31 @@ +# Mountain USDM Collateral Plugin + +## Summary + +This plugin allows `wUSDM` holders to use their tokens as collateral in the Reserve Protocol. + +`wUSDM` is an unowned, immutable, ERC4626-wrapper around the USDM token. + +Since it is ERC4626, the redeemable USDM amount can be obtained by dividing `wUSDM.totalAssets()` by `wUSDM.totalSupply()`. + +`USDM` contract: + +`wUSDM` contract: + +## Oracles - Important! + +A Chronicle oracle is available for `wUSDM`, Even though Chronicle oracles provide a compatible interface for reading prices, they require the reading contract to be **whitelisted** by Chronicle. It is important to provide the Chronicle team the collateral plugin address as soon as it is deployed to the network so they can whitelist it. This has to be done **before** the plugin is used by any RToken. + +## Implementation + +### Units + +| tok | ref | target | UoA | +| ----- | ---- | ------ | --- | +| wUSDM | USDM | USD | USD | + +### Functions + +#### refPerTok {ref/tok} + +`return shiftl_toFix(erc4626.convertToAssets(oneShare), -refDecimals)` diff --git a/contracts/plugins/assets/mountain/USDMCollateral.sol b/contracts/plugins/assets/mountain/USDMCollateral.sol new file mode 100644 index 0000000000..b49b93cbb7 --- /dev/null +++ b/contracts/plugins/assets/mountain/USDMCollateral.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "../../../libraries/Fixed.sol"; +import "../ERC4626FiatCollateral.sol"; + +/** + * @title USDM Collateral + * @notice Collateral plugin for USDM (Mountain Protocol) + * tok = wUSDM (ERC4626 vault) + * ref = USDM + * tar = USD + * UoA = USD + * + * Note: Uses a Chronicle Oracle, which requires the plugin address to be whitelisted + */ + +contract USDMCollateral is ERC4626FiatCollateral { + using OracleLib for AggregatorV3Interface; + using FixLib for uint192; + + // solhint-disable no-empty-blocks + + /// @param config.chainlinkFeed - {UoA/tok} - Chronicle oracle - Requires whitelisting! + constructor(CollateralConfig memory config, uint192 revenueHiding) + ERC4626FiatCollateral(config, revenueHiding) + { + require(config.defaultThreshold != 0, "defaultThreshold zero"); + } + + /// Can revert, used by other contract functions in order to catch errors + /// Should not return FIX_MAX for low + /// Should only return FIX_MAX for high if low is 0 + /// Should NOT be manipulable by MEV + /// @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 + function tryPrice() + external + view + virtual + override + returns ( + uint192 low, + uint192 high, + uint192 pegPrice + ) + { + // {UoA/tok} + uint192 p = chainlinkFeed.price(oracleTimeout); + uint192 err = p.mul(oracleError, CEIL); + + low = p - err; + high = p + err; + // assert(low <= high); obviously true just by inspection + + // {target/ref} = {UoA/ref} = {UoA/tok} / {ref/tok} + pegPrice = p.div(underlyingRefPerTok()); + } +} diff --git a/contracts/plugins/assets/mountain/vendor/IChronicle.sol b/contracts/plugins/assets/mountain/vendor/IChronicle.sol new file mode 100644 index 0000000000..66989bd7b6 --- /dev/null +++ b/contracts/plugins/assets/mountain/vendor/IChronicle.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// Toll (whitelist) +interface IToll { + /// @notice Thrown by protected function if caller not tolled. + /// @param caller The caller's address. + error NotTolled(address caller); + + /// @notice Emitted when toll granted to address. + /// @param caller The caller's address. + /// @param who The address toll got granted to. + event TollGranted(address indexed caller, address indexed who); + + /// @notice Grants address `who` toll. + /// @dev Only callable by auth'ed address. + /// @param who The address to grant toll. + function kiss(address who) external; + + /// @notice Renounces address `who`'s toll. + /// @dev Only callable by auth'ed address. + /// @param who The address to renounce toll. + function diss(address who) external; + + /// @notice Returns whether address `who` is tolled. + /// @param who The address to check. + /// @return True if `who` is tolled, false otherwise. + function tolled(address who) external view returns (bool); +} + +/** + * @title IChronicle + * + * @notice Interface for Chronicle Protocol's oracle products + */ +interface IChronicle is IToll { + /// @notice Returns the oracle's current value. + /// @dev Reverts if no value set. + /// @return value The oracle's current value. + function read() external view returns (uint256 value); + + /// @notice Returns the oracle's current value and its age. + /// @dev Reverts if no value set. + /// @return value The oracle's current value. + /// @return age The value's age. + function readWithAge() external view returns (uint256 value, uint256 age); + + /// Chainlink compatibility + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} diff --git a/contracts/plugins/assets/rocket-eth/RethCollateral.sol b/contracts/plugins/assets/rocket-eth/RethCollateral.sol index 76074559fc..11cb1d1af9 100644 --- a/contracts/plugins/assets/rocket-eth/RethCollateral.sol +++ b/contracts/plugins/assets/rocket-eth/RethCollateral.sol @@ -40,6 +40,7 @@ contract RethCollateral is AppreciatingFiatCollateral { } /// Can revert, used by other contract functions in order to catch errors + /// Should NOT be manipulable by MEV /// @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 diff --git a/contracts/plugins/assets/yearnv2/YearnV2CurveFiatCollateral.sol b/contracts/plugins/assets/yearnv2/YearnV2CurveFiatCollateral.sol index 7644f58759..3443c68b80 100644 --- a/contracts/plugins/assets/yearnv2/YearnV2CurveFiatCollateral.sol +++ b/contracts/plugins/assets/yearnv2/YearnV2CurveFiatCollateral.sol @@ -46,6 +46,7 @@ contract YearnV2CurveFiatCollateral is CurveStableCollateral { /// Can revert, used by other contract functions in order to catch errors /// Should not return FIX_MAX for low /// Should only return FIX_MAX for high if low is 0 + /// Should NOT be manipulable by MEV /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate /// @return {target/ref} Unused. Always 0 @@ -64,7 +65,7 @@ contract YearnV2CurveFiatCollateral is CurveStableCollateral { (uint192 aumLow, uint192 aumHigh) = totalBalancesValue(); // {LP token} - uint192 supply = shiftl_toFix(lpToken.totalSupply(), -int8(lpToken.decimals())); + uint192 supply = shiftl_toFix(lpToken.totalSupply(), -int8(lpToken.decimals()), FLOOR); // We can always assume that the total supply is non-zero // {UoA/LP token} = {UoA} / {LP token} diff --git a/contracts/plugins/mocks/AppreciatingMockDecimalsCollateral.sol b/contracts/plugins/mocks/AppreciatingMockDecimalsCollateral.sol index c4d5ec0b53..7c8c56d577 100644 --- a/contracts/plugins/mocks/AppreciatingMockDecimalsCollateral.sol +++ b/contracts/plugins/mocks/AppreciatingMockDecimalsCollateral.sol @@ -8,6 +8,7 @@ import { OracleLib } from "../assets/OracleLib.sol"; import { AppreciatingMockDecimals } from "./AppreciatingMockDecimals.sol"; import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { shiftl_toFix } from "../../libraries/Fixed.sol"; /** @@ -16,6 +17,8 @@ import { shiftl_toFix } from "../../libraries/Fixed.sol"; contract AppreciatingMockDecimalsCollateral is AppreciatingFiatCollateral { int8 private immutable refDecimals; + IERC20 private immutable rewardToken; + /// config.erc20 must be an AppreciatingMockDecimals token /// @param config.chainlinkFeed Feed units: {UoA/ref} /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide @@ -25,10 +28,19 @@ contract AppreciatingMockDecimalsCollateral is AppreciatingFiatCollateral { AppreciatingMockDecimals appToken = AppreciatingMockDecimals(address(config.erc20)); refDecimals = int8(uint8(IERC20Metadata(appToken.underlying()).decimals())); require(refDecimals > 18, "only decimals > 18"); + rewardToken = IERC20(address(AppreciatingMockDecimals(address(erc20)).rewardToken())); } /// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens function underlyingRefPerTok() public view override returns (uint192) { return shiftl_toFix(AppreciatingMockDecimals(address(erc20)).rate(), -refDecimals); } + + /// Claim rewards earned by holding a balance of the ERC20 token + /// @custom:delegate-call + function claimRewards() external virtual override(Asset, IRewardable) { + uint256 _bal = rewardToken.balanceOf(address(this)); + IRewardable(address(erc20)).claimRewards(); + emit RewardsClaimed(rewardToken, rewardToken.balanceOf(address(this)) - _bal); + } } diff --git a/contracts/plugins/mocks/BadCollateralPlugin.sol b/contracts/plugins/mocks/BadCollateralPlugin.sol index b6038eab2b..954484f888 100644 --- a/contracts/plugins/mocks/BadCollateralPlugin.sol +++ b/contracts/plugins/mocks/BadCollateralPlugin.sol @@ -55,6 +55,7 @@ contract BadCollateralPlugin is ATokenFiatCollateral { // Save prices savedLowPrice = low; savedHighPrice = high; + savedPegPrice = pegPrice; lastSave = uint48(block.timestamp); } diff --git a/contracts/plugins/mocks/CTokenMock.sol b/contracts/plugins/mocks/CTokenMock.sol index 0d31d5666e..78666d8e33 100644 --- a/contracts/plugins/mocks/CTokenMock.sol +++ b/contracts/plugins/mocks/CTokenMock.sol @@ -41,7 +41,7 @@ contract CTokenMock is ERC20Mock { } function exchangeRateStored() external view returns (uint256) { - if (revertExchangeRateStored) { + if (revertExchangeRateStored) { revert("reverting exchange rate stored"); } return _exchangeRate; diff --git a/contracts/plugins/mocks/ChainlinkMock.sol b/contracts/plugins/mocks/ChainlinkMock.sol index 17cd3b6edd..a77a432d4c 100644 --- a/contracts/plugins/mocks/ChainlinkMock.sol +++ b/contracts/plugins/mocks/ChainlinkMock.sol @@ -52,7 +52,12 @@ contract MockV3Aggregator is AggregatorV3Interface { } // used by Frax oracle - function addRoundData(bool isBadData, uint104 low, uint104 high, uint40 timestamp) public { + function addRoundData( + bool isBadData, + uint104 low, + uint104 high, + uint40 timestamp + ) public { latestAnswer = int104(low + high) / 2; latestTimestamp = block.timestamp; latestRound++; diff --git a/contracts/plugins/mocks/FraxAggregator.sol b/contracts/plugins/mocks/FraxAggregator.sol index e3b867dd49..9bb33bc2d3 100644 --- a/contracts/plugins/mocks/FraxAggregator.sol +++ b/contracts/plugins/mocks/FraxAggregator.sol @@ -6,6 +6,15 @@ import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; interface FraxAggregatorV3Interface is AggregatorV3Interface { function priceSource() external view returns (address); + function getPrices() + external + view + returns ( + bool _isBadData, + uint256 _priceLow, + uint256 _priceHigh + ); + function addRoundData( bool _isBadData, uint104 _priceLow, diff --git a/contracts/plugins/mocks/InvalidRefPerTokCollateral.sol b/contracts/plugins/mocks/InvalidRefPerTokCollateral.sol index 7861f6aeff..8c41902248 100644 --- a/contracts/plugins/mocks/InvalidRefPerTokCollateral.sol +++ b/contracts/plugins/mocks/InvalidRefPerTokCollateral.sol @@ -33,7 +33,7 @@ contract InvalidRefPerTokCollateralMock is AppreciatingFiatCollateral { exposedReferencePrice = hiddenReferencePrice; // Check for soft default + save prices - try this.tryPrice() returns (uint192 low, uint192 high, uint192) { + 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 @@ -41,6 +41,7 @@ contract InvalidRefPerTokCollateralMock is AppreciatingFiatCollateral { if (high != FIX_MAX) { savedLowPrice = low; savedHighPrice = high; + savedPegPrice = pegPrice; lastSave = uint48(block.timestamp); } } catch (bytes memory errData) { diff --git a/contracts/plugins/mocks/MockRoleRegistry.sol b/contracts/plugins/mocks/MockRoleRegistry.sol new file mode 100644 index 0000000000..976128f495 --- /dev/null +++ b/contracts/plugins/mocks/MockRoleRegistry.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +contract MockRoleRegistry { + function isOwner(address) public pure returns (bool) { + return true; + } + + function isEmergencyCouncil(address) public pure returns (bool) { + return true; + } + + function isOwnerOrEmergencyCouncil(address) public pure returns (bool) { + return true; + } +} diff --git a/contracts/plugins/mocks/RTokenCollateral.sol b/contracts/plugins/mocks/RTokenCollateral.sol index 0e5cca0eca..f10f01287a 100644 --- a/contracts/plugins/mocks/RTokenCollateral.sol +++ b/contracts/plugins/mocks/RTokenCollateral.sol @@ -34,6 +34,8 @@ contract RTokenCollateral is RTokenAsset, ICollateral { // targetName: The canonical name of this collateral's target unit. bytes32 public immutable targetName; + uint192 public savedPegPrice; // {target/ref} The peg price of the token during the last update + /// @param maxTradeVolume_ {UoA} The max trade volume, in UoA constructor( IRToken erc20_, diff --git a/contracts/plugins/mocks/UnpricedPlugins.sol b/contracts/plugins/mocks/UnpricedPlugins.sol index 4d2d20bda8..2f3df4595c 100644 --- a/contracts/plugins/mocks/UnpricedPlugins.sol +++ b/contracts/plugins/mocks/UnpricedPlugins.sol @@ -58,7 +58,11 @@ contract UnpricedFiatCollateralMock is FiatCollateral { view virtual override - returns (uint192 low, uint192 high, uint192 pegPrice) + returns ( + uint192 low, + uint192 high, + uint192 pegPrice + ) { // If unpriced is marked, return 0, FIX_MAX if (unpriced) return (0, FIX_MAX, 0); diff --git a/contracts/plugins/trading/GnosisTrade.sol b/contracts/plugins/trading/GnosisTrade.sol index ca0625e5f2..aa2800bde5 100644 --- a/contracts/plugins/trading/GnosisTrade.sol +++ b/contracts/plugins/trading/GnosisTrade.sol @@ -14,6 +14,8 @@ import "../../mixins/Versioned.sol"; // Modifications to this contract's state must only ever be made when status=PENDING! /// Trade contract against the Gnosis EasyAuction mechanism +/// Limitations on decimals due to Gnosis Auction limitations: +/// - At 21 decimals the amount of buy tokens cannot exceed ~8e7 else the trade will not settle contract GnosisTrade is ITrade, Versioned { using FixLib for uint192; using SafeERC20Upgradeable for IERC20Upgradeable; @@ -47,7 +49,7 @@ contract GnosisTrade is ITrade, Versioned { uint256 public initBal; // {qSellTok}, this trade's balance of `sell` when init() was called uint192 public sellAmount; // {sellTok}, quantity of whole tokens being sold, != 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 + uint192 public worstCasePrice; // D27{qBuyTok/qSellTok}, the worst price we expect to get // We expect Gnosis Auction either to meet or beat worstCasePrice, or to return the `sell` // tokens. If we actually *get* a worse clearing that worstCasePrice, we consider it an error in // our trading scheme and call broker.reportViolation() @@ -91,7 +93,7 @@ contract GnosisTrade is ITrade, Versioned { sell = req.sell.erc20(); buy = req.buy.erc20(); - sellAmount = shiftl_toFix(req.sellAmount, -int8(sell.decimals())); // {sellTok} + sellAmount = shiftl_toFix(req.sellAmount, -int8(sell.decimals()), FLOOR); // {sellTok} initBal = sell.balanceOf(address(this)); // {qSellTok} require(initBal >= req.sellAmount, "unfunded trade"); @@ -103,11 +105,9 @@ contract GnosisTrade is ITrade, Versioned { gnosis = gnosis_; endTime = uint48(block.timestamp) + batchAuctionLength; - // {buyTok/sellTok} - worstCasePrice = divuu(req.minBuyAmount, req.sellAmount).shiftl( - int8(sell.decimals()) - int8(buy.decimals()), - FLOOR - ); + // D27{qBuyTok/qSellTok} + worstCasePrice = shiftl_toFix(req.minBuyAmount, 9).divu(req.sellAmount, FLOOR); + // cannot overflow; cannot round to 0 unless minBuyAmount is itself 0 // Downsize our sell amount to adjust for fee // {qSellTok} = {qSellTok} * {1} / {1} @@ -157,6 +157,7 @@ contract GnosisTrade is ITrade, Versioned { } /// Settle trade, transfer tokens to trader, and report bad trade if needed + /// @dev boughtAmt can be manipulated upwards; soldAmt upwards /// @custom:interaction reentrancy-safe b/c state-locking // checks: // state is OPEN @@ -211,15 +212,9 @@ contract GnosisTrade is ITrade, Versioned { uint256 adjustedSoldAmt = Math.max(soldAmt, 1); uint256 adjustedBuyAmt = boughtAmt + 1; - // {buyTok/sellTok} - uint192 clearingPrice = divuu(adjustedBuyAmt, adjustedSoldAmt).shiftl( - int8(sell.decimals()) - int8(buy.decimals()), - FLOOR - ); - - if (clearingPrice.lt(worstCasePrice)) { - broker.reportViolation(); - } + // D27{buyTok/sellTok} + uint192 clearingPrice = shiftl_toFix(adjustedBuyAmt, 9).divu(adjustedSoldAmt, FLOOR); + if (clearingPrice.lt(worstCasePrice)) broker.reportViolation(); } } diff --git a/contracts/registry/AssetPluginRegistry.sol b/contracts/registry/AssetPluginRegistry.sol index 253009841f..6967e17dd6 100644 --- a/contracts/registry/AssetPluginRegistry.sol +++ b/contracts/registry/AssetPluginRegistry.sol @@ -3,29 +3,35 @@ pragma solidity 0.8.19; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { VersionRegistry } from "./VersionRegistry.sol"; +import { RoleRegistry } from "./RoleRegistry.sol"; /** * @title Asset Plugin Registry * @notice A tiny contract for tracking asset plugins */ -contract AssetPluginRegistry is Ownable { +contract AssetPluginRegistry { VersionRegistry public versionRegistry; + RoleRegistry public roleRegistry; // versionHash => asset => isValid - mapping(bytes32 => mapping(address => bool)) public isValidAsset; + mapping(bytes32 => mapping(address => bool)) private _isValidAsset; + mapping(address => bool) public isDeprecated; error AssetPluginRegistry__InvalidAsset(); + error AssetPluginRegistry__InvalidCaller(); error AssetPluginRegistry__InvalidVersion(); error AssetPluginRegistry__LengthMismatch(); event AssetPluginRegistryUpdated(bytes32 versionHash, address asset, bool validity); - constructor(address _versionRegistry) Ownable() { + constructor(address _versionRegistry) { versionRegistry = VersionRegistry(_versionRegistry); - - _transferOwnership(versionRegistry.owner()); + roleRegistry = versionRegistry.roleRegistry(); } - function registerAsset(address _asset, bytes32[] calldata validForVersions) external onlyOwner { + function registerAsset(address _asset, bytes32[] calldata validForVersions) external { + if (!roleRegistry.isOwner(msg.sender)) { + revert AssetPluginRegistry__InvalidCaller(); + } if (_asset == address(0)) { revert AssetPluginRegistry__InvalidAsset(); } @@ -36,7 +42,7 @@ contract AssetPluginRegistry is Ownable { revert AssetPluginRegistry__InvalidVersion(); } - isValidAsset[versionHash][_asset] = true; + _isValidAsset[versionHash][_asset] = true; emit AssetPluginRegistryUpdated(versionHash, _asset, true); } @@ -46,7 +52,10 @@ contract AssetPluginRegistry is Ownable { address _asset, bytes32[] calldata _versionHashes, bool[] calldata _validities - ) external onlyOwner { + ) external { + if (!roleRegistry.isOwner(msg.sender)) { + revert AssetPluginRegistry__InvalidCaller(); + } if (_versionHashes.length != _validities.length) { revert AssetPluginRegistry__LengthMismatch(); } @@ -61,7 +70,7 @@ contract AssetPluginRegistry is Ownable { revert AssetPluginRegistry__InvalidVersion(); } - isValidAsset[versionHash][_asset] = _validities[i]; + _isValidAsset[versionHash][_asset] = _validities[i]; emit AssetPluginRegistryUpdated(versionHash, _asset, _validities[i]); } @@ -71,7 +80,10 @@ contract AssetPluginRegistry is Ownable { bytes32 _versionHash, address[] calldata _assets, bool[] calldata _validities - ) external onlyOwner { + ) external { + if (!roleRegistry.isOwner(msg.sender)) { + revert AssetPluginRegistry__InvalidCaller(); + } if (_assets.length != _validities.length) { revert AssetPluginRegistry__LengthMismatch(); } @@ -86,9 +98,25 @@ contract AssetPluginRegistry is Ownable { revert AssetPluginRegistry__InvalidAsset(); } - isValidAsset[_versionHash][asset] = _validities[i]; + _isValidAsset[_versionHash][asset] = _validities[i]; emit AssetPluginRegistryUpdated(_versionHash, asset, _validities[i]); } } + + function deprecateAsset(address _asset) external { + if (!roleRegistry.isOwnerOrEmergencyCouncil(msg.sender)) { + revert AssetPluginRegistry__InvalidCaller(); + } + + isDeprecated[_asset] = true; + } + + function isValidAsset(bytes32 _versionHash, address _asset) external view returns (bool) { + if (!isDeprecated[_asset]) { + return _isValidAsset[_versionHash][_asset]; + } + + return false; + } } diff --git a/contracts/registry/DAOFeeRegistry.sol b/contracts/registry/DAOFeeRegistry.sol index 420b13ddb7..8e7b9adeb9 100644 --- a/contracts/registry/DAOFeeRegistry.sol +++ b/contracts/registry/DAOFeeRegistry.sol @@ -1,12 +1,14 @@ // SPDX-License-Identifier: BlueOak-1.0.0 pragma solidity 0.8.19; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { RoleRegistry } from "./RoleRegistry.sol"; -uint256 constant MAX_FEE_NUMERATOR = 15_00; // max 15% DAO fee +uint256 constant MAX_FEE_NUMERATOR = 15_00; // Max DAO Fee: 15% uint256 constant FEE_DENOMINATOR = 100_00; -contract DAOFeeRegistry is Ownable { +contract DAOFeeRegistry { + RoleRegistry public roleRegistry; + address private feeRecipient; uint256 private defaultFeeNumerator; // 0% @@ -16,33 +18,55 @@ contract DAOFeeRegistry is Ownable { error DAOFeeRegistry__FeeRecipientAlreadySet(); error DAOFeeRegistry__InvalidFeeRecipient(); error DAOFeeRegistry__InvalidFeeNumerator(); + error DAOFeeRegistry__InvalidRoleRegistry(); + error DAOFeeRegistry__InvalidCaller(); event FeeRecipientSet(address indexed feeRecipient); event DefaultFeeNumeratorSet(uint256 defaultFeeNumerator); event RTokenFeeNumeratorSet(address indexed rToken, uint256 feeNumerator, bool isActive); - constructor(address owner_) Ownable() { - _transferOwnership(owner_); // Ownership to DAO - feeRecipient = owner_; // DAO as initial fee recipient + modifier onlyOwner() { + if (!roleRegistry.isOwner(msg.sender)) { + revert DAOFeeRegistry__InvalidCaller(); + } + _; + } + + constructor(RoleRegistry _roleRegistry, address _feeRecipient) { + if (address(_roleRegistry) == address(0)) { + revert DAOFeeRegistry__InvalidRoleRegistry(); + } + + roleRegistry = _roleRegistry; + feeRecipient = _feeRecipient; } function setFeeRecipient(address feeRecipient_) external onlyOwner { - if (feeRecipient_ == address(0)) revert DAOFeeRegistry__InvalidFeeRecipient(); - if (feeRecipient_ == feeRecipient) revert DAOFeeRegistry__FeeRecipientAlreadySet(); + if (feeRecipient_ == address(0)) { + revert DAOFeeRegistry__InvalidFeeRecipient(); + } + if (feeRecipient_ == feeRecipient) { + revert DAOFeeRegistry__FeeRecipientAlreadySet(); + } feeRecipient = feeRecipient_; emit FeeRecipientSet(feeRecipient_); } function setDefaultFeeNumerator(uint256 feeNumerator_) external onlyOwner { - if (feeNumerator_ > MAX_FEE_NUMERATOR) revert DAOFeeRegistry__InvalidFeeNumerator(); + if (feeNumerator_ > MAX_FEE_NUMERATOR) { + revert DAOFeeRegistry__InvalidFeeNumerator(); + } defaultFeeNumerator = feeNumerator_; emit DefaultFeeNumeratorSet(defaultFeeNumerator); } + /// @dev A fee below 1% not recommended due to poor precision in the Distributor function setRTokenFeeNumerator(address rToken, uint256 feeNumerator_) external onlyOwner { - if (feeNumerator_ > MAX_FEE_NUMERATOR) revert DAOFeeRegistry__InvalidFeeNumerator(); + if (feeNumerator_ > MAX_FEE_NUMERATOR) { + revert DAOFeeRegistry__InvalidFeeNumerator(); + } rTokenFeeNumerator[rToken] = feeNumerator_; rTokenFeeSet[rToken] = true; diff --git a/contracts/registry/RoleRegistry.sol b/contracts/registry/RoleRegistry.sol new file mode 100644 index 0000000000..305eae49c0 --- /dev/null +++ b/contracts/registry/RoleRegistry.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +// solhint-disable-next-line max-line-length +import { AccessControlEnumerable } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; + +/** + * @title RoleRegistry + * @notice Contract to manage roles for RToken <> DAO interactions + */ +contract RoleRegistry is AccessControlEnumerable { + bytes32 public constant EMERGENCY_COUNCIL = keccak256("EMERGENCY_COUNCIL"); + + constructor() { + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + function isOwner(address account) public view returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, account); + } + + function isEmergencyCouncil(address account) public view returns (bool) { + return hasRole(EMERGENCY_COUNCIL, account); + } + + function isOwnerOrEmergencyCouncil(address account) public view returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, account) || hasRole(EMERGENCY_COUNCIL, account); + } +} diff --git a/contracts/registry/VersionRegistry.sol b/contracts/registry/VersionRegistry.sol index b132f5d370..dad73d302b 100644 --- a/contracts/registry/VersionRegistry.sol +++ b/contracts/registry/VersionRegistry.sol @@ -1,32 +1,44 @@ // SPDX-License-Identifier: BlueOak-1.0.0 pragma solidity 0.8.19; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IDeployer, Implementations } from "../interfaces/IDeployer.sol"; +import { RoleRegistry } from "./RoleRegistry.sol"; /** * @title VersionRegistry * @notice A tiny contract for tracking deployment versions + * All versions registered are expected to include veRSR, so effectively 4.0.0+. */ -contract VersionRegistry is Ownable { +contract VersionRegistry { mapping(bytes32 => IDeployer) public deployments; mapping(bytes32 => bool) public isDeprecated; bytes32 private latestVersion; + RoleRegistry public roleRegistry; error VersionRegistry__ZeroAddress(); error VersionRegistry__InvalidRegistration(); error VersionRegistry__AlreadyDeprecated(); + error VersionRegistry__InvalidRoleRegistry(); + error VersionRegistry__InvalidCaller(); event VersionRegistered(bytes32 versionHash, IDeployer deployer); event VersionDeprecated(bytes32 versionHash); - constructor(address owner_) Ownable() { - _transferOwnership(owner_); + constructor(RoleRegistry _roleRegistry) { + if (address(_roleRegistry) == address(0)) { + revert VersionRegistry__ZeroAddress(); + } + + roleRegistry = _roleRegistry; } /// Register a deployer address, keyed by version. /// @param deployer The deployer contract address for the version to be added. - function registerVersion(IDeployer deployer) external onlyOwner { + function registerVersion(IDeployer deployer) external { + if (!roleRegistry.isOwner(msg.sender)) { + revert VersionRegistry__InvalidCaller(); + } + if (address(deployer) == address(0)) { revert VersionRegistry__ZeroAddress(); } @@ -44,7 +56,11 @@ contract VersionRegistry is Ownable { emit VersionRegistered(versionHash, deployer); } - function deprecateVersion(bytes32 versionHash) external onlyOwner { + function deprecateVersion(bytes32 versionHash) external { + if (!roleRegistry.isOwnerOrEmergencyCouncil(msg.sender)) { + revert VersionRegistry__InvalidCaller(); + } + if (isDeprecated[versionHash]) { revert VersionRegistry__AlreadyDeprecated(); } diff --git a/docs/collateral.md b/docs/collateral.md index 9a98938c24..d72e63010c 100644 --- a/docs/collateral.md +++ b/docs/collateral.md @@ -42,18 +42,11 @@ interface IAsset is IRewardable { function refresh() external; /// Should not revert - /// low should be nonzero when the asset might be worth selling + /// 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); - /// @return {tok} The balance of the ERC20 in whole tokens function bal(address account) external view returns (uint192); @@ -113,6 +106,9 @@ interface ICollateral is IAsset { /// @return {target/ref} Quantity of whole target units per whole reference unit in the peg function targetPerRef() external view returns (uint192); + + /// @return {target/ref} The peg price of the token during the last update + function savedPegPrice() external view returns (uint192); } ``` @@ -225,6 +221,10 @@ When implementing Revenue Hiding, the `price` function should NOT hide revenue; ## Important Properties for Collateral Plugins +### Oracles must not be plausibly manipulable + +It must not be possible to manipulate the oracles a collateral relies on, cheaply. In particular (though not limited to): it should not be possible to manipulate price within the block. + ### Reuse of Collateral Plugins Collateral plugins should be safe to reuse by many different Reserve Protocol instances. So: @@ -250,6 +250,15 @@ To use a rebasing token as collateral backing, the rebasing ERC20 needs to be re There is a simple ERC20 wrapper that can be easily extended at [RewardableERC20Wrapper.sol](../contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol). You may add additional logic by extending `_afterDeposit()` or `_beforeWithdraw()`. +### Token decimals should be <= 21 + +The protocol currently supports collateral tokens with up to 21 decimals. There are some caveats to know about: + +- Tokens with 21 decimals must be worth at least `$1` at-peg +- Tokens with 18 decimals must be worth at least `$0.001` at-peg + +These constraints only apply to pricing when the collateral is SOUND; when the collateral status is IFFY or DISABLED the price is allowed to fall below these thresholds. + ### `refresh()` should never revert 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`. @@ -372,13 +381,9 @@ Under no price data, the low estimate shoulddecay downwards and high estimate up Should return `(0, FIX_MAX)` if pricing data is _completely_ unavailable or stale. -Should be gas-efficient. +Should NOT return `(>0, FIX_MAX)`: if the high price is FIX_MAX then the low price must be 0. -### lotPrice() `{UoA/tok}` - -Deprecated. Phased out in 3.1.0, but left on interface for backwards compatibility. - -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. +Should be gas-efficient. ### refPerTok() `{ref/tok}` @@ -409,6 +414,14 @@ The target name is just a bytes32 serialization of the target unit string. Here For a collateral plugin that uses a novel target unit, get the targetName with `ethers.utils.formatBytes32String(unitName)`. +### savedPegPrice() `{target/ref}` + +A return value of 0 indicates _no_ issuance premium should be applied to this collateral during de-peg. Collateral that return 0 are more dangerous to be used inside RTokens as a result. + +Should never revert. + +Should be gas-efficient. + ## Practical Advice from Previous Work In most cases [Fiat Collateral](../contracts/plugins/assets/FiatCollateral.sol) or [AppreciatingFiatCollateral.sol](../contracts/plugins/assets/AppreciatingFiatCollateral.sol) can be extended, pretty easily, to support a new collateral type. This allows the collateral developer to limit their attention to the overriding of three functions: `tryPrice()`, `refPerTok()`, `targetPerRef()`. diff --git a/docs/deployed-addresses/1-ETH+.md b/docs/deployed-addresses/1-ETH+.md index 0445b0a03b..1d22c25ab1 100644 --- a/docs/deployed-addresses/1-ETH+.md +++ b/docs/deployed-addresses/1-ETH+.md @@ -2,23 +2,23 @@ ## Component Addresses | Contract | Address | Implementation | Version | | --- | --- | --- | --- | -| RToken | [0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8](https://etherscan.io/address/0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8) |[0xb6f01aa21defa4a4de33bed16bcc06cfd23b6a6f](https://etherscan.io/address/0xb6f01aa21defa4a4de33bed16bcc06cfd23b6a6f#code) | 3.0.0 | -| Main | [0xb6A7d481719E97e142114e905E86a39a2Fa0dfD2](https://etherscan.io/address/0xb6A7d481719E97e142114e905E86a39a2Fa0dfD2) |[0xf5366f67ff66a3cefcb18809a762d5b5931febf8](https://etherscan.io/address/0xf5366f67ff66a3cefcb18809a762d5b5931febf8#code) | 3.0.0 | -| AssetRegistry | [0xf526f058858E4cD060cFDD775077999562b31bE0](https://etherscan.io/address/0xf526f058858E4cD060cFDD775077999562b31bE0) |[0x773cf50adcf1730964d4a9b664baed4b9ffc2450](https://etherscan.io/address/0x773cf50adcf1730964d4a9b664baed4b9ffc2450#code) | 3.0.0 | -| BackingManager | [0x608e1e01EF072c15E5Da7235ce793f4d24eCa67B](https://etherscan.io/address/0x608e1e01EF072c15E5Da7235ce793f4d24eCa67B) |[0xbbc532a80dd141449330c1232c953da6801aed01](https://etherscan.io/address/0xbbc532a80dd141449330c1232c953da6801aed01#code) | 3.0.1 | -| BasketHandler | [0x56f40A33e3a3fE2F1614bf82CBeb35987ac10194](https://etherscan.io/address/0x56f40A33e3a3fE2F1614bf82CBeb35987ac10194) |[0x5ccca36cbb66a4e4033b08b4f6d7bac96ba55cdc](https://etherscan.io/address/0x5ccca36cbb66a4e4033b08b4f6d7bac96ba55cdc#code) | 3.0.0 | -| Broker | [0x6ca42ce37e5ece334066C504ba37144b4f14D50a](https://etherscan.io/address/0x6ca42ce37e5ece334066C504ba37144b4f14D50a) |[0x9a5f8a9bb91a868b7501139eedb20dc129d28f04](https://etherscan.io/address/0x9a5f8a9bb91a868b7501139eedb20dc129d28f04#code) | 3.0.0 | -| RSRTrader | [0x6E20823cA50aA026b99789c8D468a01f8aA3581C](https://etherscan.io/address/0x6E20823cA50aA026b99789c8D468a01f8aA3581C) |[0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af](https://etherscan.io/address/0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af#code) | 3.0.1 | -| RTokenTrader | [0x977cb0e300a58978f597fc65ED5a2D2784D2DCF9](https://etherscan.io/address/0x977cb0e300a58978f597fc65ED5a2D2784D2DCF9) |[0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af](https://etherscan.io/address/0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af#code) | 3.0.1 | -| Distributor | [0x954B4770462e8894BcD2451543482F11DC160e1e](https://etherscan.io/address/0x954B4770462e8894BcD2451543482F11DC160e1e) |[0x0e8439a17ba5cbb2d9823c03a02566b9dd5d96ac](https://etherscan.io/address/0x0e8439a17ba5cbb2d9823c03a02566b9dd5d96ac#code) | 3.0.0 | -| Furnace | [0x9862efAB36F81524B24F787e07C97e2F5A6c206e](https://etherscan.io/address/0x9862efAB36F81524B24F787e07C97e2F5A6c206e) |[0x99580fc649c02347ebc7750524caae5cacf9d34c](https://etherscan.io/address/0x99580fc649c02347ebc7750524caae5cacf9d34c#code) | 3.0.0 | -| StRSR | [0xffa151Ad0A0e2e40F39f9e5E9F87cF9E45e819dd](https://etherscan.io/address/0xffa151Ad0A0e2e40F39f9e5E9F87cF9E45e819dd) |[0xc98eafc9f249d90e3e35e729e3679dd75a899c10](https://etherscan.io/address/0xc98eafc9f249d90e3e35e729e3679dd75a899c10#code) | 3.0.0 | +| RToken | [0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8](https://etherscan.io/address/0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8) |[0x784955641292b0014bc9ef82321300f0b6c7e36d](https://etherscan.io/address/0x784955641292b0014bc9ef82321300f0b6c7e36d#code) | 3.4.0 | +| Main | [0xb6A7d481719E97e142114e905E86a39a2Fa0dfD2](https://etherscan.io/address/0xb6A7d481719E97e142114e905E86a39a2Fa0dfD2) |[0x24a4b37f9c40fb0e80ec436df2e9989fbafa8bb7](https://etherscan.io/address/0x24a4b37f9c40fb0e80ec436df2e9989fbafa8bb7#code) | 3.4.0 | +| AssetRegistry | [0xf526f058858E4cD060cFDD775077999562b31bE0](https://etherscan.io/address/0xf526f058858E4cD060cFDD775077999562b31bE0) |[0xbf1c0206de440b2cf76ea4405e1dbf2fc227a463](https://etherscan.io/address/0xbf1c0206de440b2cf76ea4405e1dbf2fc227a463#code) | 3.4.0 | +| BackingManager | [0x608e1e01EF072c15E5Da7235ce793f4d24eCa67B](https://etherscan.io/address/0x608e1e01EF072c15E5Da7235ce793f4d24eCa67B) |[0x20c801869e578e71f2298649870765aa81f7dc69](https://etherscan.io/address/0x20c801869e578e71f2298649870765aa81f7dc69#code) | 3.4.0 | +| BasketHandler | [0x56f40A33e3a3fE2F1614bf82CBeb35987ac10194](https://etherscan.io/address/0x56f40A33e3a3fE2F1614bf82CBeb35987ac10194) |[0xee7fc703f84ae2ce30475333c57e56d3a7d3adbc](https://etherscan.io/address/0xee7fc703f84ae2ce30475333c57e56d3a7d3adbc#code) | 3.4.0 | +| Broker | [0x6ca42ce37e5ece334066C504ba37144b4f14D50a](https://etherscan.io/address/0x6ca42ce37e5ece334066C504ba37144b4f14D50a) |[0x62bd44b05542bff1e59a01bf7151f533e1c9c12c](https://etherscan.io/address/0x62bd44b05542bff1e59a01bf7151f533e1c9c12c#code) | 3.4.0 | +| RSRTrader | [0x6E20823cA50aA026b99789c8D468a01f8aA3581C](https://etherscan.io/address/0x6E20823cA50aA026b99789c8D468a01f8aA3581C) |[0xc60a7cd6fce24d0c3637a1dcbc8b0f9a9bff6a7c](https://etherscan.io/address/0xc60a7cd6fce24d0c3637a1dcbc8b0f9a9bff6a7c#code) | 3.4.0 | +| RTokenTrader | [0x977cb0e300a58978f597fc65ED5a2D2784D2DCF9](https://etherscan.io/address/0x977cb0e300a58978f597fc65ED5a2D2784D2DCF9) |[0xc60a7cd6fce24d0c3637a1dcbc8b0f9a9bff6a7c](https://etherscan.io/address/0xc60a7cd6fce24d0c3637a1dcbc8b0f9a9bff6a7c#code) | 3.4.0 | +| Distributor | [0x954B4770462e8894BcD2451543482F11DC160e1e](https://etherscan.io/address/0x954B4770462e8894BcD2451543482F11DC160e1e) |[0x44a42a0f14128e81a21c5fc4322a9f91ff83b4ee](https://etherscan.io/address/0x44a42a0f14128e81a21c5fc4322a9f91ff83b4ee#code) | 3.4.0 | +| Furnace | [0x9862efAB36F81524B24F787e07C97e2F5A6c206e](https://etherscan.io/address/0x9862efAB36F81524B24F787e07C97e2F5A6c206e) |[0x845b8b0a1c6db8318414d708da25fa28d4a0dc81](https://etherscan.io/address/0x845b8b0a1c6db8318414d708da25fa28d4a0dc81#code) | 3.4.0 | +| StRSR | [0xffa151Ad0A0e2e40F39f9e5E9F87cF9E45e819dd](https://etherscan.io/address/0xffa151Ad0A0e2e40F39f9e5E9F87cF9E45e819dd) |[0xe433673648c94fec0706e5ac95d4f4097f58b5fb](https://etherscan.io/address/0xe433673648c94fec0706e5ac95d4f4097f58b5fb#code) | 3.4.0 | ## Governance Addresses | Contract | Address | | --- | --- | -| Governor Alexios | [0x239cDcBE174B4728c870A24F77540dAB3dC5F981](https://etherscan.io/address/0x239cDcBE174B4728c870A24F77540dAB3dC5F981) | -| Timelock | [0x5f4A10aE2fF68bE3cdA7d7FB432b10C6BFA6457B](https://etherscan.io/address/0x5f4A10aE2fF68bE3cdA7d7FB432b10C6BFA6457B) | +| Governor | [0x868Fe81C276d730A1995Dc84b642E795dFb8F753](https://etherscan.io/address/0x868Fe81C276d730A1995Dc84b642E795dFb8F753) | +| Timelock | [0x5d8A7DC9405F08F14541BA918c1Bf7eb2dACE556](https://etherscan.io/address/0x5d8A7DC9405F08F14541BA918c1Bf7eb2dACE556) | \ No newline at end of file diff --git a/docs/deployed-addresses/1-USD3.md b/docs/deployed-addresses/1-USD3.md new file mode 100644 index 0000000000..ab245fea85 --- /dev/null +++ b/docs/deployed-addresses/1-USD3.md @@ -0,0 +1,24 @@ +# [USD3 (Web 3 Dollar) - Mainnet](https://etherscan.io/address/0x0d86883FAf4FfD7aEb116390af37746F45b6f378) +## Component Addresses +| Contract | Address | Implementation | Version | +| --- | --- | --- | --- | +| RToken | [0x0d86883FAf4FfD7aEb116390af37746F45b6f378](https://etherscan.io/address/0x0d86883FAf4FfD7aEb116390af37746F45b6f378) |[0x784955641292b0014bc9ef82321300f0b6c7e36d](https://etherscan.io/address/0x784955641292b0014bc9ef82321300f0b6c7e36d#code) | 3.4.0 | +| Main | [0x81117e3e98910C3dCF956b5Fc97a7212E047AcF4](https://etherscan.io/address/0x81117e3e98910C3dCF956b5Fc97a7212E047AcF4) |[0x24a4b37f9c40fb0e80ec436df2e9989fbafa8bb7](https://etherscan.io/address/0x24a4b37f9c40fb0e80ec436df2e9989fbafa8bb7#code) | 3.4.0 | +| AssetRegistry | [0xd75C9768c8EC003B792AFAC35d0bBacB44B5e500](https://etherscan.io/address/0xd75C9768c8EC003B792AFAC35d0bBacB44B5e500) |[0xbf1c0206de440b2cf76ea4405e1dbf2fc227a463](https://etherscan.io/address/0xbf1c0206de440b2cf76ea4405e1dbf2fc227a463#code) | 3.4.0 | +| BackingManager | [0x4B0b3d40b0623f3a9eac09d2E01F592710ee59F0](https://etherscan.io/address/0x4B0b3d40b0623f3a9eac09d2E01F592710ee59F0) |[0x20c801869e578e71f2298649870765aa81f7dc69](https://etherscan.io/address/0x20c801869e578e71f2298649870765aa81f7dc69#code) | 3.4.0 | +| BasketHandler | [0x19835E5817A6fDC944100E86da2FCe86327457B8](https://etherscan.io/address/0x19835E5817A6fDC944100E86da2FCe86327457B8) |[0xee7fc703f84ae2ce30475333c57e56d3a7d3adbc](https://etherscan.io/address/0xee7fc703f84ae2ce30475333c57e56d3a7d3adbc#code) | 3.4.0 | +| Broker | [0x06d6d1d71761Eb101448574728ECB30deFf4B95d](https://etherscan.io/address/0x06d6d1d71761Eb101448574728ECB30deFf4B95d) | +| RSRTrader | [0x1Ae6D94992e87056A92bA2e9Eb54cf3ad3EF55D6](https://etherscan.io/address/0x1Ae6D94992e87056A92bA2e9Eb54cf3ad3EF55D6) | +| RTokenTrader | [0x52D307A4ee4E42a799B3354ad54564A5b4CfC260](https://etherscan.io/address/0x52D307A4ee4E42a799B3354ad54564A5b4CfC260) | +| Distributor | [0xcE08baB702CC1B041eEb151281Ac01Da3F8698c8](https://etherscan.io/address/0xcE08baB702CC1B041eEb151281Ac01Da3F8698c8) | +| Furnace | [0xfE52b7a53AcA0e2D155434E2c613F766D140fea8](https://etherscan.io/address/0xfE52b7a53AcA0e2D155434E2c613F766D140fea8) |[0x845b8b0a1c6db8318414d708da25fa28d4a0dc81](https://etherscan.io/address/0x845b8b0a1c6db8318414d708da25fa28d4a0dc81#code) | 3.4.0 | +| StRSR | [0x55230B05d4c1F4a0B4f804dc52919F06355DfBF8](https://etherscan.io/address/0x55230B05d4c1F4a0B4f804dc52919F06355DfBF8) |[0xe433673648c94fec0706e5ac95d4f4097f58b5fb](https://etherscan.io/address/0xe433673648c94fec0706e5ac95d4f4097f58b5fb#code) | 3.4.0 | + + +## Governance Addresses +| Contract | Address | +| --- | --- | +| Governor | [0x441808e20E625e0094b01B40F84af89436229279](https://etherscan.io/address/0x441808e20E625e0094b01B40F84af89436229279) | +| Timelock | [0x12e4F043c6464984A45173E0444105058b6C3c7B](https://etherscan.io/address/0x12e4F043c6464984A45173E0444105058b6C3c7B) | + + \ No newline at end of file diff --git a/docs/deployed-addresses/1-USDC+.md b/docs/deployed-addresses/1-USDC+.md index ace1aba0ab..b2a1e67802 100644 --- a/docs/deployed-addresses/1-USDC+.md +++ b/docs/deployed-addresses/1-USDC+.md @@ -18,7 +18,7 @@ ## Governance Addresses | Contract | Address | | --- | --- | -| Governor Alexios | [0xc837C557071D604bCb1058c8c4891ddBe8FDD630](https://etherscan.io/address/0xc837C557071D604bCb1058c8c4891ddBe8FDD630) | +| Governor | [0xc837C557071D604bCb1058c8c4891ddBe8FDD630](https://etherscan.io/address/0xc837C557071D604bCb1058c8c4891ddBe8FDD630) | | Timelock | [0x6C957417cB6DF6e821eec8555DEE8b116C291999](https://etherscan.io/address/0x6C957417cB6DF6e821eec8555DEE8b116C291999) | \ No newline at end of file diff --git a/docs/deployed-addresses/1-assets-3.3.0.md b/docs/deployed-addresses/1-assets-3.3.0.md new file mode 100644 index 0000000000..207b0b7be9 --- /dev/null +++ b/docs/deployed-addresses/1-assets-3.3.0.md @@ -0,0 +1,23 @@ +# Assets (Mainnet 3.3.0) +## Assets +| Contract | Address | +| --- | --- | +| COMP | [0x29dc6F79750020d77c6391629101BDC0F0D16ECB](https://etherscan.io/address/0x29dc6F79750020d77c6391629101BDC0F0D16ECB) | +| CRV | [0x9257a1307a72603B7916d0c97fCABC6351C3482E](https://etherscan.io/address/0x9257a1307a72603B7916d0c97fCABC6351C3482E) | +| CVX | [0x4dA79d89482737381E90d2A7005b21cd11eAeE5C](https://etherscan.io/address/0x4dA79d89482737381E90d2A7005b21cd11eAeE5C) | + +## Collaterals +| Contract | Address | +| --- | --- | +| aUSDC | [0x6E14943224d6E4F7607943512ba17DbBA9524B8e](https://etherscan.io/address/0x6E14943224d6E4F7607943512ba17DbBA9524B8e) | +| aUSDT | [0x8AD3055286f4E59B399616Bd6BEfE24F64573928](https://etherscan.io/address/0x8AD3055286f4E59B399616Bd6BEfE24F64573928) | +| wstETH | [0x3519918E2918b59f3b29bed16dC77174DEC6707b](https://etherscan.io/address/0x3519918E2918b59f3b29bed16dC77174DEC6707b) | +| rETH | [0xEdd8d4Cc0d0358a12f232fd72821d25d4EbE7704](https://etherscan.io/address/0xEdd8d4Cc0d0358a12f232fd72821d25d4EbE7704) | +| cUSDCv3 | [0xf0Fb23485057Fd88C80B9CEc8b433FdA47e0a07A](https://etherscan.io/address/0xf0Fb23485057Fd88C80B9CEc8b433FdA47e0a07A) | +| cvxeUSDFRAXBP | [0x5cD176b58a6FdBAa1aEFD0921935a730C62f03Ac](https://etherscan.io/address/0x5cD176b58a6FdBAa1aEFD0921935a730C62f03Ac) | +| sDAI | [0x29EDbbbE7415cb8637e0F62D5d19dcB3A5bC3229](https://etherscan.io/address/0x29EDbbbE7415cb8637e0F62D5d19dcB3A5bC3229) | +| saEthUSDC | [0x05beee046A5C28844804E679aD5587046dBffbc0](https://etherscan.io/address/0x05beee046A5C28844804E679aD5587046dBffbc0) | +| cUSDT | [0x1269BFa56EcaE9D6d5003810D4a35bf8479376b8](https://etherscan.io/address/0x1269BFa56EcaE9D6d5003810D4a35bf8479376b8) | +| saEthPyUSD | [0xe176A5ebFB873D5b3cf1909d0EdaE4FE095F5bc7](https://etherscan.io/address/0xe176A5ebFB873D5b3cf1909d0EdaE4FE095F5bc7) | +| cvxPayPool | [0x426Ad39C7ccF2f3872aBB16c0291Eb40c0F44D23](https://etherscan.io/address/0x426Ad39C7ccF2f3872aBB16c0291Eb40c0F44D23) | + \ No newline at end of file diff --git a/docs/deployed-addresses/1-assets-3.4.0.md b/docs/deployed-addresses/1-assets-3.4.0.md new file mode 100644 index 0000000000..551975aa52 --- /dev/null +++ b/docs/deployed-addresses/1-assets-3.4.0.md @@ -0,0 +1,66 @@ +# Assets (Mainnet 3.4.0) +## Assets +| Contract | Address | +| --- | --- | +| stkAAVE | [0xF4493581D52671a9E04d693a68ccc61853bceEaE](https://etherscan.io/address/0xF4493581D52671a9E04d693a68ccc61853bceEaE) | +| COMP | [0x63eDdF26Bc65eDa1D1c0147ce8E23c09BE963596](https://etherscan.io/address/0x63eDdF26Bc65eDa1D1c0147ce8E23c09BE963596) | +| CRV | [0xc18bF46F178F7e90b9CD8b7A8b00Af026D5ce3D3](https://etherscan.io/address/0xc18bF46F178F7e90b9CD8b7A8b00Af026D5ce3D3) | +| CVX | [0x7ef93b20C10E6662931b32Dd9D4b85861eB2E4b8](https://etherscan.io/address/0x7ef93b20C10E6662931b32Dd9D4b85861eB2E4b8) | + +## Collaterals +| Contract | Address | +| --- | --- | +| DAI | [0xEc375F2984D21D5ddb0D82767FD8a9C4CE8Eec2F](https://etherscan.io/address/0xEc375F2984D21D5ddb0D82767FD8a9C4CE8Eec2F) | +| USDC | [0x442f8fc98e3cc6B3d49a66f9858Ac9B6e70Dad3e](https://etherscan.io/address/0x442f8fc98e3cc6B3d49a66f9858Ac9B6e70Dad3e) | +| USDT | [0xe7Dcd101A027Ec34860ECb634a2797d0D2dc4d8b](https://etherscan.io/address/0xe7Dcd101A027Ec34860ECb634a2797d0D2dc4d8b) | +| USDP | [0x4C0B21Acb267f1fAE4aeFA977A26c4a63C9B35e6](https://etherscan.io/address/0x4C0B21Acb267f1fAE4aeFA977A26c4a63C9B35e6) | +| BUSD | [0x97bb4a995b98b1BfF99046b3c518276f78fA5250](https://etherscan.io/address/0x97bb4a995b98b1BfF99046b3c518276f78fA5250) | +| aDAI | [0x9ca9A9cdcE9E943608c945E7001dC89EB163991E](https://etherscan.io/address/0x9ca9A9cdcE9E943608c945E7001dC89EB163991E) | +| aUSDC | [0xc4240D22FFa144E2712aACF3E2cC302af0339ED0](https://etherscan.io/address/0xc4240D22FFa144E2712aACF3E2cC302af0339ED0) | +| aUSDT | [0x8d753659D4E4e4b4601c7F01Dc1c920cA538E333](https://etherscan.io/address/0x8d753659D4E4e4b4601c7F01Dc1c920cA538E333) | +| aBUSD | [0x01F9A6bf339cff820cA503A56FD3705AE35c27F7](https://etherscan.io/address/0x01F9A6bf339cff820cA503A56FD3705AE35c27F7) | +| aUSDP | [0xda5cc207CCefD116fF167a8ABEBBd52bD67C958E](https://etherscan.io/address/0xda5cc207CCefD116fF167a8ABEBBd52bD67C958E) | +| cDAI | [0x337E418b880bDA5860e05D632CF039B7751B907B](https://etherscan.io/address/0x337E418b880bDA5860e05D632CF039B7751B907B) | +| cUSDC | [0x043be931D9C4422e1cFeA528e19818dcDfdE9Ebc](https://etherscan.io/address/0x043be931D9C4422e1cFeA528e19818dcDfdE9Ebc) | +| cUSDT | [0x5ceadb6606C5D82FcCd3f9b312C018fE1f8aa6dA](https://etherscan.io/address/0x5ceadb6606C5D82FcCd3f9b312C018fE1f8aa6dA) | +| cUSDP | [0xa0c02De8FfBb9759b9beBA5e29C82112688A0Ff4](https://etherscan.io/address/0xa0c02De8FfBb9759b9beBA5e29C82112688A0Ff4) | +| cWBTC | [0xC0f89AFcb6F1c4E943aA61FFcdFc41fDcB7D84DD](https://etherscan.io/address/0xC0f89AFcb6F1c4E943aA61FFcdFc41fDcB7D84DD) | +| cETH | [0x4d3A8507a8eb9036895efdD1a462210CE58DE4ad](https://etherscan.io/address/0x4d3A8507a8eb9036895efdD1a462210CE58DE4ad) | +| WBTC | [0x832D65735E541c0404a58B741bEF5652c2B7D0Db](https://etherscan.io/address/0x832D65735E541c0404a58B741bEF5652c2B7D0Db) | +| WETH | [0xADDca344c92Be84A053C5CBE8e067460767FB816](https://etherscan.io/address/0xADDca344c92Be84A053C5CBE8e067460767FB816) | +| wstETH | [0xb7049ee9F533D32C9434101f0645E6Ea5DFe2cdb](https://etherscan.io/address/0xb7049ee9F533D32C9434101f0645E6Ea5DFe2cdb) | +| rETH | [0x987f5e0f845D46262893e680b652D8aAF1B5bCc0](https://etherscan.io/address/0x987f5e0f845D46262893e680b652D8aAF1B5bCc0) | +| fUSDC | [0xB58D95003Af73CF76Ce349103726a51D4Ec8af17](https://etherscan.io/address/0xB58D95003Af73CF76Ce349103726a51D4Ec8af17) | +| fUSDT | [0xD5254b740FbEF6AAcD674936ea7Fb9f4053781aF](https://etherscan.io/address/0xD5254b740FbEF6AAcD674936ea7Fb9f4053781aF) | +| fDAI | [0xA0a620B94446a7DC8952ECf252FcC495eeC65873](https://etherscan.io/address/0xA0a620B94446a7DC8952ECf252FcC495eeC65873) | +| fFRAX | [0xFd9c32198D3cf3ad3b165918FD78De3654cb22eA](https://etherscan.io/address/0xFd9c32198D3cf3ad3b165918FD78De3654cb22eA) | +| cUSDCv3 | [0x33Ba1BC07b0fafb4BBC1520B330081b91ca6bdf0](https://etherscan.io/address/0x33Ba1BC07b0fafb4BBC1520B330081b91ca6bdf0) | +| cvx3Pool | [0x8E5ADdC553962DAcdF48106B6218AC93DA9617b2](https://etherscan.io/address/0x8E5ADdC553962DAcdF48106B6218AC93DA9617b2) | +| cvxPayPool | [0x5315Fbe0CEB299F53aE375f65fd9376767C8224c](https://etherscan.io/address/0x5315Fbe0CEB299F53aE375f65fd9376767C8224c) | +| cvxeUSDFRAXBP | [0x994455cE66Fd984e2A0A0aca453e637810a8f032](https://etherscan.io/address/0x994455cE66Fd984e2A0A0aca453e637810a8f032) | +| cvxMIM3Pool | [0x3d21f841C0Fb125176C1DBDF0DE196b071323A75](https://etherscan.io/address/0x3d21f841C0Fb125176C1DBDF0DE196b071323A75) | +| cvxETHPlusETH | [0x05F164E71C46a8f8FB2ba71550a00eeC9FCd85cd](https://etherscan.io/address/0x05F164E71C46a8f8FB2ba71550a00eeC9FCd85cd) | +| crveUSDFRAXBP | [0xCDC5f5E041b49Cad373E94930E2b3bE30be70535](https://etherscan.io/address/0xCDC5f5E041b49Cad373E94930E2b3bE30be70535) | +| crvMIM3Pool | [0x692cf8CE08d03eF1f8C3dCa82F67935fa9417B62](https://etherscan.io/address/0x692cf8CE08d03eF1f8C3dCa82F67935fa9417B62) | +| crv3Pool | [0xf59a7987EDd5380cbAb30c37D1c808686f9b67B9](https://etherscan.io/address/0xf59a7987EDd5380cbAb30c37D1c808686f9b67B9) | +| sDAI | [0x62a9DDC6FF6077E823690118eCc935d16A8de47e](https://etherscan.io/address/0x62a9DDC6FF6077E823690118eCc935d16A8de47e) | +| cbETH | [0xC8b80813cad9139D0eeFe38C711a11b20147aA54](https://etherscan.io/address/0xC8b80813cad9139D0eeFe38C711a11b20147aA54) | +| maUSDT | [0x2F8F8Ac64ECbAC38f212b05115836120784a29F7](https://etherscan.io/address/0x2F8F8Ac64ECbAC38f212b05115836120784a29F7) | +| maUSDC | [0xC5d03FB7A38E6025D9A32C7444cfbBfa18B7D656](https://etherscan.io/address/0xC5d03FB7A38E6025D9A32C7444cfbBfa18B7D656) | +| maDAI | [0x7be70371e7ECd9af5A5b49015EC8F8C336B52D81](https://etherscan.io/address/0x7be70371e7ECd9af5A5b49015EC8F8C336B52D81) | +| maWBTC | [0x75B6921925e8BD632380706e722035752ffF175d](https://etherscan.io/address/0x75B6921925e8BD632380706e722035752ffF175d) | +| maWETH | [0xA402078f0A2e077Ea2b1Fb3b6ab74F0cBA10E508](https://etherscan.io/address/0xA402078f0A2e077Ea2b1Fb3b6ab74F0cBA10E508) | +| maStETH | [0x4a139215D9E696c0e7618a441eD3CFd12bbD8CD6](https://etherscan.io/address/0x4a139215D9E696c0e7618a441eD3CFd12bbD8CD6) | +| yvCurveUSDCcrvUSD | [0x1573416df7095F698e37A954D9e951868E526650](https://etherscan.io/address/0x1573416df7095F698e37A954D9e951868E526650) | +| yvCurveUSDPcrvUSD | [0xb3A3552Cc52411dFF6D520C6F725E6F9e11001EF](https://etherscan.io/address/0xb3A3552Cc52411dFF6D520C6F725E6F9e11001EF) | +| sFRAX | [0x0b7DcCBceA6f985301506D575E2661bf858CdEcC](https://etherscan.io/address/0x0b7DcCBceA6f985301506D575E2661bf858CdEcC) | +| saEthUSDC | [0x00F820794Bda3fb01E5f159ee1fF7c8409fca5AB](https://etherscan.io/address/0x00F820794Bda3fb01E5f159ee1fF7c8409fca5AB) | +| saEthPyUSD | [0x58a41c87f8C65cf21f961b570540b176e408Cf2E](https://etherscan.io/address/0x58a41c87f8C65cf21f961b570540b176e408Cf2E) | +| bbUSDT | [0x01355C7439982c57cF89CA9785d211806f866224](https://etherscan.io/address/0x01355C7439982c57cF89CA9785d211806f866224) | +| steakUSDC | [0x565CBc99EE04667581c7f3459561fCaf1CF68602](https://etherscan.io/address/0x565CBc99EE04667581c7f3459561fCaf1CF68602) | +| steakPYUSD | [0x23f06D5Fe858B18CD064A5D95054e8ae8536094a](https://etherscan.io/address/0x23f06D5Fe858B18CD064A5D95054e8ae8536094a) | +| Re7WETH | [0xa0a6C06e45437d4Ae1D778AaeB4605AC2B62A870](https://etherscan.io/address/0xa0a6C06e45437d4Ae1D778AaeB4605AC2B62A870) | +| cvxCrvUSDUSDC | [0x9Fc0F31e2D26C437461a9eEBfe858d17e2611Ea5](https://etherscan.io/address/0x9Fc0F31e2D26C437461a9eEBfe858d17e2611Ea5) | +| cvxCrvUSDUSDT | [0x69c6597690B8Df61D15F201519C03725bdec40c1](https://etherscan.io/address/0x69c6597690B8Df61D15F201519C03725bdec40c1) | +| sfrxETH | [0x4c891fCa6319d492866672E3D2AfdAAA5bDcfF67](https://etherscan.io/address/0x4c891fCa6319d492866672E3D2AfdAAA5bDcfF67) | + \ No newline at end of file diff --git a/docs/deployed-addresses/1-components-3.3.0.md b/docs/deployed-addresses/1-components-3.3.0.md new file mode 100644 index 0000000000..8c63281216 --- /dev/null +++ b/docs/deployed-addresses/1-components-3.3.0.md @@ -0,0 +1,14 @@ +# Component Implementations (Mainnet 3.3.0) +## Component Addresses +| Contract | Address | Version | +| --- | --- | --- | +| ActFacet | [0xCAB3D3d0d5544145A6BCB47e58F61368BCcAe2dB](https://etherscan.io/address/0xCAB3D3d0d5544145A6BCB47e58F61368BCcAe2dB) | N/A | +| DutchTrade | [0x4eDEb80Ce684A890Dd58Ae0d9762C38731b11b99](https://etherscan.io/address/0x4eDEb80Ce684A890Dd58Ae0d9762C38731b11b99) | 3.3.0 | +| Facade | [0x2C7ca56342177343A2954C250702Fd464f4d0613](https://etherscan.io/address/0x2C7ca56342177343A2954C250702Fd464f4d0613) | N/A | +| GNOSIS_EASY_AUCTION | [0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101](https://etherscan.io/address/0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101) | N/A | +| GnosisTrade | [0x803a52c5DAB69B78419bb160051071eF2F9Fd227](https://etherscan.io/address/0x803a52c5DAB69B78419bb160051071eF2F9Fd227) | 3.3.0 | +| MaxIssuableFacet | [0x5771d976696AA180Fed276FB6571fE2f41D0b849](https://etherscan.io/address/0x5771d976696AA180Fed276FB6571fE2f41D0b849) | N/A | +| ReadFacet | [0x823110a13eB26cB09c4Bb118DBfE4ff5f96D5526](https://etherscan.io/address/0x823110a13eB26cB09c4Bb118DBfE4ff5f96D5526) | N/A | +| RSR | [0x320623b8e4ff03373931769a31fc52a4e78b5d70](https://etherscan.io/address/0x320623b8e4ff03373931769a31fc52a4e78b5d70) | N/A | +| RSR_FEED | [0x759bBC1be8F90eE6457C44abc7d443842a976d02](https://etherscan.io/address/0x759bBC1be8F90eE6457C44abc7d443842a976d02) | N/A | + \ No newline at end of file diff --git a/docs/deployed-addresses/1-components-3.4.0.md b/docs/deployed-addresses/1-components-3.4.0.md new file mode 100644 index 0000000000..3be2335209 --- /dev/null +++ b/docs/deployed-addresses/1-components-3.4.0.md @@ -0,0 +1,31 @@ +# Component Implementations (Mainnet 3.4.0) +## Component Addresses +| Contract | Address | Version | +| --- | --- | --- | +| ActFacet | [0xCAB3D3d0d5544145A6BCB47e58F61368BCcAe2dB](https://etherscan.io/address/0xCAB3D3d0d5544145A6BCB47e58F61368BCcAe2dB) | N/A | +| AssetRegistry | [0xbF1C0206de440b2cF76Ea4405e1DbF2fC227a463](https://etherscan.io/address/0xbF1C0206de440b2cF76Ea4405e1DbF2fC227a463) | 3.4.0 | +| BackingManager | [0x20C801869e578E71F2298649870765Aa81f7DC69](https://etherscan.io/address/0x20C801869e578E71F2298649870765Aa81f7DC69) | 3.4.0 | +| BasketHandler | [0xeE7FC703f84AE2CE30475333c57E56d3A7D3AdBC](https://etherscan.io/address/0xeE7FC703f84AE2CE30475333c57E56d3A7D3AdBC) | 3.4.0 | +| BasketLib | [0xf383dC60D29A5B9ba461F40A0606870d80d1EA88](https://etherscan.io/address/0xf383dC60D29A5B9ba461F40A0606870d80d1EA88) | N/A | +| Broker | [0x62BD44b05542bfF1E59A01Bf7151F533e1c9C12c](https://etherscan.io/address/0x62BD44b05542bfF1E59A01Bf7151F533e1c9C12c) | 3.4.0 | +| Deployer | [0x2204EC97D31E2C9eE62eaD9e6E2d5F7712D3f1bF](https://etherscan.io/address/0x2204EC97D31E2C9eE62eaD9e6E2d5F7712D3f1bF) | 3.4.0 | +| Distributor | [0x44a42A0F14128E81a21c5fc4322a9f91fF83b4Ee](https://etherscan.io/address/0x44a42A0F14128E81a21c5fc4322a9f91fF83b4Ee) | 3.4.0 | +| DutchTrade | [0x971c890ACb9EeB084f292996Be667bB9A2889AE9](https://etherscan.io/address/0x971c890ACb9EeB084f292996Be667bB9A2889AE9) | 3.4.0 | +| Facade | [0x2C7ca56342177343A2954C250702Fd464f4d0613](https://etherscan.io/address/0x2C7ca56342177343A2954C250702Fd464f4d0613) | N/A | +| FacadeWrite | [0x1D94290F82D0B417B088d9F5dB316B11C9cf220C](https://etherscan.io/address/0x1D94290F82D0B417B088d9F5dB316B11C9cf220C) | N/A | +| FacadeWriteLib | [0xDf73Cd789422040182b0C24a8b2C97bbCbba3263](https://etherscan.io/address/0xDf73Cd789422040182b0C24a8b2C97bbCbba3263) | N/A | +| Furnace | [0x845B8b0a1c6DB8318414d708Da25fA28d4a0dc81](https://etherscan.io/address/0x845B8b0a1c6DB8318414d708Da25fA28d4a0dc81) | 3.4.0 | +| GNOSIS_EASY_AUCTION | [0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101](https://etherscan.io/address/0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101) | N/A | +| GnosisTrade | [0x030c9B66Ac089cB01aA2058FC8f7d9baddC9ae75](https://etherscan.io/address/0x030c9B66Ac089cB01aA2058FC8f7d9baddC9ae75) | 3.4.0 | +| Main | [0x24a4B37F9c40fB0E80ec436Df2e9989FBAFa8bB7](https://etherscan.io/address/0x24a4B37F9c40fB0E80ec436Df2e9989FBAFa8bB7) | 3.4.0 | +| MaxIssuableFacet | [0x5771d976696AA180Fed276FB6571fE2f41D0b849](https://etherscan.io/address/0x5771d976696AA180Fed276FB6571fE2f41D0b849) | N/A | +| ReadFacet | [0x823110a13eB26cB09c4Bb118DBfE4ff5f96D5526](https://etherscan.io/address/0x823110a13eB26cB09c4Bb118DBfE4ff5f96D5526) | N/A | +| RSR | [0x320623b8E4fF03373931769A31Fc52A4E78B5d70](https://etherscan.io/address/0x320623b8E4fF03373931769A31Fc52A4E78B5d70) | N/A | +| RSR_FEED | [0x759bBC1be8F90eE6457C44abc7d443842a976d02](https://etherscan.io/address/0x759bBC1be8F90eE6457C44abc7d443842a976d02) | N/A | +| RsrAsset | [0x591529f039Ba48C3bEAc5090e30ceDDcb41D0EaA](https://etherscan.io/address/0x591529f039Ba48C3bEAc5090e30ceDDcb41D0EaA) | 3.4.0 | +| RsrTrader | [0xc60a7Cd6fce24d0c3637A1dCBC8B0f9A9BFF6a7c](https://etherscan.io/address/0xc60a7Cd6fce24d0c3637A1dCBC8B0f9A9BFF6a7c) | 3.4.0 | +| RToken | [0x784955641292b0014BC9eF82321300f0b6C7E36d](https://etherscan.io/address/0x784955641292b0014BC9eF82321300f0b6C7E36d) | 3.4.0 | +| RTokenTrader | [0xc60a7Cd6fce24d0c3637A1dCBC8B0f9A9BFF6a7c](https://etherscan.io/address/0xc60a7Cd6fce24d0c3637A1dCBC8B0f9A9BFF6a7c) | 3.4.0 | +| StRSR | [0xE433673648c94FEC0706E5AC95d4f4097f58B5fb](https://etherscan.io/address/0xE433673648c94FEC0706E5AC95d4f4097f58B5fb) | 3.4.0 | +| TradingLib | [0xa54544C6C36C0d776cc4F04EBB847e0BB3A11ea2](https://etherscan.io/address/0xa54544C6C36C0d776cc4F04EBB847e0BB3A11ea2) | N/A | + \ No newline at end of file diff --git a/docs/deployed-addresses/1-eUSD.md b/docs/deployed-addresses/1-eUSD.md index c55496ec44..d9c2444d41 100644 --- a/docs/deployed-addresses/1-eUSD.md +++ b/docs/deployed-addresses/1-eUSD.md @@ -2,23 +2,23 @@ ## Component Addresses | Contract | Address | Implementation | Version | | --- | --- | --- | --- | -| RToken | [0xA0d69E286B938e21CBf7E51D71F6A4c8918f482F](https://etherscan.io/address/0xA0d69E286B938e21CBf7E51D71F6A4c8918f482F) |[0xb6f01aa21defa4a4de33bed16bcc06cfd23b6a6f](https://etherscan.io/address/0xb6f01aa21defa4a4de33bed16bcc06cfd23b6a6f#code) | 3.0.0 | -| Main | [0x7697aE4dEf3C3Cd52493Ba3a6F57fc6d8c59108a](https://etherscan.io/address/0x7697aE4dEf3C3Cd52493Ba3a6F57fc6d8c59108a) |[0xf5366f67ff66a3cefcb18809a762d5b5931febf8](https://etherscan.io/address/0xf5366f67ff66a3cefcb18809a762d5b5931febf8#code) | 3.0.0 | -| AssetRegistry | [0x9B85aC04A09c8C813c37de9B3d563C2D3F936162](https://etherscan.io/address/0x9B85aC04A09c8C813c37de9B3d563C2D3F936162) |[0x773cf50adcf1730964d4a9b664baed4b9ffc2450](https://etherscan.io/address/0x773cf50adcf1730964d4a9b664baed4b9ffc2450#code) | 3.0.0 | -| BackingManager | [0xF014FEF41cCB703975827C8569a3f0940cFD80A4](https://etherscan.io/address/0xF014FEF41cCB703975827C8569a3f0940cFD80A4) |[0xbbc532a80dd141449330c1232c953da6801aed01](https://etherscan.io/address/0xbbc532a80dd141449330c1232c953da6801aed01#code) | 3.0.1 | -| BasketHandler | [0x6d309297ddDFeA104A6E89a132e2f05ce3828e07](https://etherscan.io/address/0x6d309297ddDFeA104A6E89a132e2f05ce3828e07) |[0x5ccca36cbb66a4e4033b08b4f6d7bac96ba55cdc](https://etherscan.io/address/0x5ccca36cbb66a4e4033b08b4f6d7bac96ba55cdc#code) | 3.0.0 | -| Broker | [0x90EB22A31b69C29C34162E0E9278cc0617aA2B50](https://etherscan.io/address/0x90EB22A31b69C29C34162E0E9278cc0617aA2B50) |[0x9a5f8a9bb91a868b7501139eedb20dc129d28f04](https://etherscan.io/address/0x9a5f8a9bb91a868b7501139eedb20dc129d28f04#code) | 3.0.0 | -| RSRTrader | [0xE04C26F68E0657d402FA95377aa7a2838D6cBA6f](https://etherscan.io/address/0xE04C26F68E0657d402FA95377aa7a2838D6cBA6f) |[0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af](https://etherscan.io/address/0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af#code) | 3.0.1 | -| RTokenTrader | [0x3d5EbB5399243412c7e895a7AA468c7cD4b1014A](https://etherscan.io/address/0x3d5EbB5399243412c7e895a7AA468c7cD4b1014A) |[0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af](https://etherscan.io/address/0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af#code) | 3.0.1 | -| Distributor | [0x8a77980f82A1d537600891D782BCd8bd41B85472](https://etherscan.io/address/0x8a77980f82A1d537600891D782BCd8bd41B85472) |[0x0e8439a17ba5cbb2d9823c03a02566b9dd5d96ac](https://etherscan.io/address/0x0e8439a17ba5cbb2d9823c03a02566b9dd5d96ac#code) | 3.0.0 | -| Furnace | [0x57084b3a6317bea01bA8f7c582eD033d9345c2B2](https://etherscan.io/address/0x57084b3a6317bea01bA8f7c582eD033d9345c2B2) |[0x99580fc649c02347ebc7750524caae5cacf9d34c](https://etherscan.io/address/0x99580fc649c02347ebc7750524caae5cacf9d34c#code) | 3.0.0 | -| StRSR | [0x18ba6e33ceb80f077DEb9260c9111e62f21aE7B8](https://etherscan.io/address/0x18ba6e33ceb80f077DEb9260c9111e62f21aE7B8) |[0xc98eafc9f249d90e3e35e729e3679dd75a899c10](https://etherscan.io/address/0xc98eafc9f249d90e3e35e729e3679dd75a899c10#code) | 3.0.0 | +| RToken | [0xA0d69E286B938e21CBf7E51D71F6A4c8918f482F](https://etherscan.io/address/0xA0d69E286B938e21CBf7E51D71F6A4c8918f482F) |[0x784955641292b0014bc9ef82321300f0b6c7e36d](https://etherscan.io/address/0x784955641292b0014bc9ef82321300f0b6c7e36d#code) | 3.4.0 | +| Main | [0x7697aE4dEf3C3Cd52493Ba3a6F57fc6d8c59108a](https://etherscan.io/address/0x7697aE4dEf3C3Cd52493Ba3a6F57fc6d8c59108a) |[0x24a4b37f9c40fb0e80ec436df2e9989fbafa8bb7](https://etherscan.io/address/0x24a4b37f9c40fb0e80ec436df2e9989fbafa8bb7#code) | 3.4.0 | +| AssetRegistry | [0x9B85aC04A09c8C813c37de9B3d563C2D3F936162](https://etherscan.io/address/0x9B85aC04A09c8C813c37de9B3d563C2D3F936162) |[0xbf1c0206de440b2cf76ea4405e1dbf2fc227a463](https://etherscan.io/address/0xbf1c0206de440b2cf76ea4405e1dbf2fc227a463#code) | 3.4.0 | +| BackingManager | [0xF014FEF41cCB703975827C8569a3f0940cFD80A4](https://etherscan.io/address/0xF014FEF41cCB703975827C8569a3f0940cFD80A4) |[0x20c801869e578e71f2298649870765aa81f7dc69](https://etherscan.io/address/0x20c801869e578e71f2298649870765aa81f7dc69#code) | 3.4.0 | +| BasketHandler | [0x6d309297ddDFeA104A6E89a132e2f05ce3828e07](https://etherscan.io/address/0x6d309297ddDFeA104A6E89a132e2f05ce3828e07) |[0xee7fc703f84ae2ce30475333c57e56d3a7d3adbc](https://etherscan.io/address/0xee7fc703f84ae2ce30475333c57e56d3a7d3adbc#code) | 3.4.0 | +| Broker | [0x90EB22A31b69C29C34162E0E9278cc0617aA2B50](https://etherscan.io/address/0x90EB22A31b69C29C34162E0E9278cc0617aA2B50) |[0x62bd44b05542bff1e59a01bf7151f533e1c9c12c](https://etherscan.io/address/0x62bd44b05542bff1e59a01bf7151f533e1c9c12c#code) | 3.4.0 | +| RSRTrader | [0xE04C26F68E0657d402FA95377aa7a2838D6cBA6f](https://etherscan.io/address/0xE04C26F68E0657d402FA95377aa7a2838D6cBA6f) |[0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af](https://etherscan.io/address/0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af#code) | 3.4.0 | +| RTokenTrader | [0x3d5EbB5399243412c7e895a7AA468c7cD4b1014A](https://etherscan.io/address/0x3d5EbB5399243412c7e895a7AA468c7cD4b1014A) |[0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af](https://etherscan.io/address/0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af#code) | 3.4.0 | +| Distributor | [0x8a77980f82A1d537600891D782BCd8bd41B85472](https://etherscan.io/address/0x8a77980f82A1d537600891D782BCd8bd41B85472) |[0x44a42a0f14128e81a21c5fc4322a9f91ff83b4ee](https://etherscan.io/address/0x44a42a0f14128e81a21c5fc4322a9f91ff83b4ee#code) | 3.4.0 | +| Furnace | [0x57084b3a6317bea01bA8f7c582eD033d9345c2B2](https://etherscan.io/address/0x57084b3a6317bea01bA8f7c582eD033d9345c2B2) |[0x99580fc649c02347ebc7750524caae5cacf9d34c](https://etherscan.io/address/0x99580fc649c02347ebc7750524caae5cacf9d34c#code) | 3.4.0 | +| StRSR | [0x18ba6e33ceb80f077DEb9260c9111e62f21aE7B8](https://etherscan.io/address/0x18ba6e33ceb80f077DEb9260c9111e62f21aE7B8) |[0xe433673648c94fec0706e5ac95d4f4097f58b5fb](https://etherscan.io/address/0xe433673648c94fec0706e5ac95d4f4097f58b5fb#code) | 3.4.0 | ## Governance Addresses | Contract | Address | | --- | --- | -| Governor Alexios | [0x7e880d8bD9c9612D6A9759F96aCD23df4A4650E6](https://etherscan.io/address/0x7e880d8bD9c9612D6A9759F96aCD23df4A4650E6) | -| Timelock | [0xc8Ee187A5e5c9dC9b42414Ddf861FFc615446a2c](https://etherscan.io/address/0xc8Ee187A5e5c9dC9b42414Ddf861FFc615446a2c) | +| Governor | [0xf4A9288D5dEb0EaE987e5926795094BF6f4662F8](https://etherscan.io/address/0xf4A9288D5dEb0EaE987e5926795094BF6f4662F8) | +| Timelock | [0x7BEa807798313fE8F557780dBD6b829c1E3aD560](https://etherscan.io/address/0x7BEa807798313fE8F557780dBD6b829c1E3aD560) | \ No newline at end of file diff --git a/docs/deployed-addresses/1-hyUSD.md b/docs/deployed-addresses/1-hyUSD.md index b3323a833a..44959a599e 100644 --- a/docs/deployed-addresses/1-hyUSD.md +++ b/docs/deployed-addresses/1-hyUSD.md @@ -2,23 +2,23 @@ ## Component Addresses | Contract | Address | Implementation | Version | | --- | --- | --- | --- | -| RToken | [0xaCdf0DBA4B9839b96221a8487e9ca660a48212be](https://etherscan.io/address/0xaCdf0DBA4B9839b96221a8487e9ca660a48212be) |[0xb6f01aa21defa4a4de33bed16bcc06cfd23b6a6f](https://etherscan.io/address/0xb6f01aa21defa4a4de33bed16bcc06cfd23b6a6f#code) | 3.0.0 | -| Main | [0x2cabaa8010b3fbbDEeBe4a2D0fEffC2ed155bf37](https://etherscan.io/address/0x2cabaa8010b3fbbDEeBe4a2D0fEffC2ed155bf37) |[0xf5366f67ff66a3cefcb18809a762d5b5931febf8](https://etherscan.io/address/0xf5366f67ff66a3cefcb18809a762d5b5931febf8#code) | 3.0.0 | -| AssetRegistry | [0xaCacddeE9b900b7535B13Cd8662df130265b8c78](https://etherscan.io/address/0xaCacddeE9b900b7535B13Cd8662df130265b8c78) |[0x773cf50adcf1730964d4a9b664baed4b9ffc2450](https://etherscan.io/address/0x773cf50adcf1730964d4a9b664baed4b9ffc2450#code) | 3.0.0 | -| BackingManager | [0x61691c4181F876Dd7e19D6742B367B48AA280ed3](https://etherscan.io/address/0x61691c4181F876Dd7e19D6742B367B48AA280ed3) |[0xbbc532a80dd141449330c1232c953da6801aed01](https://etherscan.io/address/0xbbc532a80dd141449330c1232c953da6801aed01#code) | 3.0.1 | -| BasketHandler | [0x9119DB28432bd97aBF4c3D81B929849e0490c7A6](https://etherscan.io/address/0x9119DB28432bd97aBF4c3D81B929849e0490c7A6) |[0x5ccca36cbb66a4e4033b08b4f6d7bac96ba55cdc](https://etherscan.io/address/0x5ccca36cbb66a4e4033b08b4f6d7bac96ba55cdc#code) | 3.0.0 | -| Broker | [0x44344ca9014BE4bB622037224d107493586f35ed](https://etherscan.io/address/0x44344ca9014BE4bB622037224d107493586f35ed) |[0x9a5f8a9bb91a868b7501139eedb20dc129d28f04](https://etherscan.io/address/0x9a5f8a9bb91a868b7501139eedb20dc129d28f04#code) | 3.0.0 | -| RSRTrader | [0x0771301d56Eb734a5F61d275Da1b6c2459a00dc7](https://etherscan.io/address/0x0771301d56Eb734a5F61d275Da1b6c2459a00dc7) |[0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af](https://etherscan.io/address/0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af#code) | 3.0.1 | -| RTokenTrader | [0x4886f5549d3b25adCFaC68E40062c735faf81378](https://etherscan.io/address/0x4886f5549d3b25adCFaC68E40062c735faf81378) |[0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af](https://etherscan.io/address/0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af#code) | 3.0.1 | -| Distributor | [0x0297941cCB71f5595072C4fA34CE443b6C5b47A0](https://etherscan.io/address/0x0297941cCB71f5595072C4fA34CE443b6C5b47A0) |[0x0e8439a17ba5cbb2d9823c03a02566b9dd5d96ac](https://etherscan.io/address/0x0e8439a17ba5cbb2d9823c03a02566b9dd5d96ac#code) | 3.0.0 | -| Furnace | [0x43D806BB6cDfA1dde1D1754c5F2Ea28adC3bc0E8](https://etherscan.io/address/0x43D806BB6cDfA1dde1D1754c5F2Ea28adC3bc0E8) |[0x99580fc649c02347ebc7750524caae5cacf9d34c](https://etherscan.io/address/0x99580fc649c02347ebc7750524caae5cacf9d34c#code) | 3.0.0 | -| StRSR | [0x7Db3C57001c80644208fb8AA81bA1200C7B0731d](https://etherscan.io/address/0x7Db3C57001c80644208fb8AA81bA1200C7B0731d) |[0xc98eafc9f249d90e3e35e729e3679dd75a899c10](https://etherscan.io/address/0xc98eafc9f249d90e3e35e729e3679dd75a899c10#code) | 3.0.0 | +| RToken | [0xaCdf0DBA4B9839b96221a8487e9ca660a48212be](https://etherscan.io/address/0xaCdf0DBA4B9839b96221a8487e9ca660a48212be) |[0x784955641292b0014bc9ef82321300f0b6c7e36d](https://etherscan.io/address/0x784955641292b0014bc9ef82321300f0b6c7e36d#code) | 3.4.0 | +| Main | [0x2cabaa8010b3fbbDEeBe4a2D0fEffC2ed155bf37](https://etherscan.io/address/0x2cabaa8010b3fbbDEeBe4a2D0fEffC2ed155bf37) |[0x24a4b37f9c40fb0e80ec436df2e9989fbafa8bb7](https://etherscan.io/address/0x24a4b37f9c40fb0e80ec436df2e9989fbafa8bb7#code) | 3.4.0 | +| AssetRegistry | [0xaCacddeE9b900b7535B13Cd8662df130265b8c78](https://etherscan.io/address/0xaCacddeE9b900b7535B13Cd8662df130265b8c78) |[0xbf1c0206de440b2cf76ea4405e1dbf2fc227a463](https://etherscan.io/address/0xbf1c0206de440b2cf76ea4405e1dbf2fc227a463#code) | 3.4.0 | +| BackingManager | [0x61691c4181F876Dd7e19D6742B367B48AA280ed3](https://etherscan.io/address/0x61691c4181F876Dd7e19D6742B367B48AA280ed3) |[0x20c801869e578e71f2298649870765aa81f7dc69](https://etherscan.io/address/0x20c801869e578e71f2298649870765aa81f7dc69#code) | 3.4.0 | +| BasketHandler | [0x9119DB28432bd97aBF4c3D81B929849e0490c7A6](https://etherscan.io/address/0x9119DB28432bd97aBF4c3D81B929849e0490c7A6) |[0x5ccca36cbb66a4e4033b08b4f6d7bac96ba55cdc](https://etherscan.io/address/0x5ccca36cbb66a4e4033b08b4f6d7bac96ba55cdc#code) | 3.4.0 | +| Broker | [0x44344ca9014BE4bB622037224d107493586f35ed](https://etherscan.io/address/0x44344ca9014BE4bB622037224d107493586f35ed) |[0x9a5f8a9bb91a868b7501139eedb20dc129d28f04](https://etherscan.io/address/0x9a5f8a9bb91a868b7501139eedb20dc129d28f04#code) | 3.4.0 | +| RSRTrader | [0x0771301d56Eb734a5F61d275Da1b6c2459a00dc7](https://etherscan.io/address/0x0771301d56Eb734a5F61d275Da1b6c2459a00dc7) |[0xc60a7cd6fce24d0c3637a1dcbc8b0f9a9bff6a7c](https://etherscan.io/address/0xc60a7cd6fce24d0c3637a1dcbc8b0f9a9bff6a7c#code) | 3.4.0 | +| RTokenTrader | [0x4886f5549d3b25adCFaC68E40062c735faf81378](https://etherscan.io/address/0x4886f5549d3b25adCFaC68E40062c735faf81378) |[0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af](https://etherscan.io/address/0x5e3e13d3d2a0adfe16f8ef5e7a2992a88e9e65af#code) | 3.4.0 | +| Distributor | [0x0297941cCB71f5595072C4fA34CE443b6C5b47A0](https://etherscan.io/address/0x0297941cCB71f5595072C4fA34CE443b6C5b47A0) |[0x44a42a0f14128e81a21c5fc4322a9f91ff83b4ee](https://etherscan.io/address/0x44a42a0f14128e81a21c5fc4322a9f91ff83b4ee#code) | 3.4.0 | +| Furnace | [0x43D806BB6cDfA1dde1D1754c5F2Ea28adC3bc0E8](https://etherscan.io/address/0x43D806BB6cDfA1dde1D1754c5F2Ea28adC3bc0E8) |[0x845b8b0a1c6db8318414d708da25fa28d4a0dc81](https://etherscan.io/address/0x845b8b0a1c6db8318414d708da25fa28d4a0dc81#code) | 3.4.0 | +| StRSR | [0x7Db3C57001c80644208fb8AA81bA1200C7B0731d](https://etherscan.io/address/0x7Db3C57001c80644208fb8AA81bA1200C7B0731d) |[0xe433673648c94fec0706e5ac95d4f4097f58b5fb](https://etherscan.io/address/0xe433673648c94fec0706e5ac95d4f4097f58b5fb#code) | 3.4.0 | ## Governance Addresses | Contract | Address | | --- | --- | -| Governor Alexios | [0x22d7937438b4bBf02f6cA55E3831ABB94Bd0b6f1](https://etherscan.io/address/0x22d7937438b4bBf02f6cA55E3831ABB94Bd0b6f1) | -| Timelock | [0x624f9f076ED42ba3B37C3011dC5a1761C2209E1C](https://etherscan.io/address/0x624f9f076ED42ba3B37C3011dC5a1761C2209E1C) | +| Governor | [0x3F26EF1460D21A99425569Ef3148Ca6059a7eEAe](https://etherscan.io/address/0x3F26EF1460D21A99425569Ef3148Ca6059a7eEAe) | +| Timelock | [0x788Fd297B4d497e44e4BF25d642fbecA3018B5d2](https://etherscan.io/address/0x788Fd297B4d497e44e4BF25d642fbecA3018B5d2) | \ No newline at end of file diff --git a/docs/deployed-addresses/8453-Vaya.md b/docs/deployed-addresses/8453-Vaya.md index 04ae8fc6f6..1e72f836be 100644 --- a/docs/deployed-addresses/8453-Vaya.md +++ b/docs/deployed-addresses/8453-Vaya.md @@ -18,7 +18,7 @@ ## Governance Addresses | Contract | Address | | --- | --- | -| Governor Alexios | [0xEb583EA06501f92E994C353aD2741A35582987aA](https://basescan.org/address/0xEb583EA06501f92E994C353aD2741A35582987aA) | +| Governor | [0xEb583EA06501f92E994C353aD2741A35582987aA](https://basescan.org/address/0xEb583EA06501f92E994C353aD2741A35582987aA) | | Timelock | [0xeE3eC997A37e661a42673D7A489Fbf0E5ed0C223](https://basescan.org/address/0xeE3eC997A37e661a42673D7A489Fbf0E5ed0C223) | \ No newline at end of file diff --git a/docs/deployed-addresses/8453-assets-3.3.0.md b/docs/deployed-addresses/8453-assets-3.3.0.md new file mode 100644 index 0000000000..009c00521c --- /dev/null +++ b/docs/deployed-addresses/8453-assets-3.3.0.md @@ -0,0 +1,13 @@ +# Assets (Base 3.3.0) +## Assets +| Contract | Address | +| --- | --- | + + +## Collaterals +| Contract | Address | +| --- | --- | +| saBasUSDC | [0xAe9795115c7E5Bee7d2017b92c41DECa66d81dcf](https://basescan.org/address/0xAe9795115c7E5Bee7d2017b92c41DECa66d81dcf) | +| cUSDCv3 | [0x36A43E13f0c8d8612AE3978A8E7A58BB58000923](https://basescan.org/address/0x36A43E13f0c8d8612AE3978A8E7A58BB58000923) | +| USDC | [0x8b906361048D277452506d3f791020A1cA798aF3](https://basescan.org/address/0x8b906361048D277452506d3f791020A1cA798aF3) | + \ No newline at end of file diff --git a/docs/deployed-addresses/8453-assets-3.4.0.md b/docs/deployed-addresses/8453-assets-3.4.0.md new file mode 100644 index 0000000000..9ac5e09d97 --- /dev/null +++ b/docs/deployed-addresses/8453-assets-3.4.0.md @@ -0,0 +1,19 @@ +# Assets (Base 3.4.0) +## Assets +| Contract | Address | +| --- | --- | +| COMP | [0xB8794Fb1CCd62bFe631293163F4A3fC2d22e37e0](https://basescan.org/address/0xB8794Fb1CCd62bFe631293163F4A3fC2d22e37e0) | +| STG | [0xEE527CC63122732532d0f1ad33Ec035D30f3050f](https://basescan.org/address/0xEE527CC63122732532d0f1ad33Ec035D30f3050f) | + +## Collaterals +| Contract | Address | +| --- | --- | +| DAI | [0x3E40840d0282C9F9cC7d17094b5239f87fcf18e5](https://basescan.org/address/0x3E40840d0282C9F9cC7d17094b5239f87fcf18e5) | +| USDC | [0xaa85216187F92a781D8F9Bcb40825E356ee2635a](https://basescan.org/address/0xaa85216187F92a781D8F9Bcb40825E356ee2635a) | +| USDbC | [0xD126741474B0348D9B0F4911573d8f543c01C2c4](https://basescan.org/address/0xD126741474B0348D9B0F4911573d8f543c01C2c4) | +| WETH | [0x073BD162BBD05Cd2CF631B90D44239B8a367276e](https://basescan.org/address/0x073BD162BBD05Cd2CF631B90D44239B8a367276e) | +| cbETH | [0x851B461a9744f4c9E996C03072cAB6f44Fa04d0D](https://basescan.org/address/0x851B461a9744f4c9E996C03072cAB6f44Fa04d0D) | +| saBasUSDC | [0xC19f5d60e2Aca1174f3D5Fe189f0A69afaB76f50](https://basescan.org/address/0xC19f5d60e2Aca1174f3D5Fe189f0A69afaB76f50) | +| cUSDCv3 | [0xf7a9D27c3B60c78c6F6e2c2d6ED6E8B94b352461](https://basescan.org/address/0xf7a9D27c3B60c78c6F6e2c2d6ED6E8B94b352461) | +| wstETH | [0x8b4374005291B8FCD14C4E947604b2FB3C660A73](https://basescan.org/address/0x8b4374005291B8FCD14C4E947604b2FB3C660A73) | + \ No newline at end of file diff --git a/docs/deployed-addresses/8453-bsdETH.md b/docs/deployed-addresses/8453-bsdETH.md new file mode 100644 index 0000000000..9aa3ad000b --- /dev/null +++ b/docs/deployed-addresses/8453-bsdETH.md @@ -0,0 +1,24 @@ +# [bsdETH (Based ETH) - Base](https://basescan.org/address/0xCb327b99fF831bF8223cCEd12B1338FF3aA322Ff) +## Component Addresses +| Contract | Address | Implementation | Version | +| --- | --- | --- | --- | +| RToken | [0xCb327b99fF831bF8223cCEd12B1338FF3aA322Ff](https://basescan.org/address/0xCb327b99fF831bF8223cCEd12B1338FF3aA322Ff) | +| Main | [0x5f835eA13721B11A7543C8f9C94aA840c1f2Da52](https://basescan.org/address/0x5f835eA13721B11A7543C8f9C94aA840c1f2Da52) | +| AssetRegistry | [0xAd7272CCd23108922c34B0BeCdcA228671208EfE](https://basescan.org/address/0xAd7272CCd23108922c34B0BeCdcA228671208EfE) | +| BackingManager | [0x6969343c4938b4ca79B1237C94f825df23A9905d](https://basescan.org/address/0x6969343c4938b4ca79B1237C94f825df23A9905d) | +| BasketHandler | [0x2b79Ebb020E4F374a44c5eeAD609e2703Fb3DDc4](https://basescan.org/address/0x2b79Ebb020E4F374a44c5eeAD609e2703Fb3DDc4) | +| Broker | [0x564A533004a8356BC6571a76fE83935F38C23b1E](https://basescan.org/address/0x564A533004a8356BC6571a76fE83935F38C23b1E) | +| RSRTrader | [0x07F58c529170645f3dF1C277BEDC176ecEB97A0E](https://basescan.org/address/0x07F58c529170645f3dF1C277BEDC176ecEB97A0E) | +| RTokenTrader | [0x8C46348552BdAb72dD163928fd8460257089F8F9](https://basescan.org/address/0x8C46348552BdAb72dD163928fd8460257089F8F9) | +| Distributor | [0x9CFB9c16358296F4a3CF8Adc6E4B5b9937c9359f](https://basescan.org/address/0x9CFB9c16358296F4a3CF8Adc6E4B5b9937c9359f) | +| Furnace | [0x8e987a41e7C4e8D0290FEAF7102EB301Db1540A8](https://basescan.org/address/0x8e987a41e7C4e8D0290FEAF7102EB301Db1540A8) | +| StRSR | [0x3D190D968a8985673285B3B9cD5f5BDC12c9b368](https://basescan.org/address/0x3D190D968a8985673285B3B9cD5f5BDC12c9b368) | + + +## Governance Addresses +| Contract | Address | +| --- | --- | +| Governor | [0x21fBa52dA03e1F964fa521532f8B8951fC212055](https://basescan.org/address/0x21fBa52dA03e1F964fa521532f8B8951fC212055) | +| Timelock | [0xe664d294824C2A8C952A10c4034e1105d2907F46](https://basescan.org/address/0xe664d294824C2A8C952A10c4034e1105d2907F46) | + + \ No newline at end of file diff --git a/docs/deployed-addresses/8453-components-3.3.0.md b/docs/deployed-addresses/8453-components-3.3.0.md new file mode 100644 index 0000000000..c0c8fb341f --- /dev/null +++ b/docs/deployed-addresses/8453-components-3.3.0.md @@ -0,0 +1,9 @@ +# Component Implementations (Base 3.3.0) +## Component Addresses +| Contract | Address | Version | +| --- | --- | --- | +| DutchTrade | [0xd0Ff3aa130A34Eac0C448950CA8fe662330cB065](https://basescan.org/address/0xd0Ff3aa130A34Eac0C448950CA8fe662330cB065) | 3.3.0 | +| GNOSIS_EASY_AUCTION | [0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02](https://basescan.org/address/0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02) | N/A | +| RSR | [0xaB36452DbAC151bE02b16Ca17d8919826072f64a](https://basescan.org/address/0xaB36452DbAC151bE02b16Ca17d8919826072f64a) | 1.0.3 | +| RSR_FEED | [0xAa98aE504658766Dfe11F31c5D95a0bdcABDe0b1](https://basescan.org/address/0xAa98aE504658766Dfe11F31c5D95a0bdcABDe0b1) | N/A | + \ No newline at end of file diff --git a/docs/deployed-addresses/8453-components-3.4.0.md b/docs/deployed-addresses/8453-components-3.4.0.md new file mode 100644 index 0000000000..806909e9e5 --- /dev/null +++ b/docs/deployed-addresses/8453-components-3.4.0.md @@ -0,0 +1,31 @@ +# Component Implementations (Base 3.4.0) +## Component Addresses +| Contract | Address | Version | +| --- | --- | --- | +| ActFacet | [0x0eac15B9Fe585432E48Cf175571D75D111861F43](https://basescan.org/address/0x0eac15B9Fe585432E48Cf175571D75D111861F43) | N/A | +| AssetRegistry | [0x3DDe17cfd36e740CB7452cb2F59FC925eACb91aB](https://basescan.org/address/0x3DDe17cfd36e740CB7452cb2F59FC925eACb91aB) | 3.4.0 | +| BackingManager | [0xb5bDFF1FB47635383ABf13b78a79C8a21aA1b23E](https://basescan.org/address/0xb5bDFF1FB47635383ABf13b78a79C8a21aA1b23E) | 3.4.0 | +| BasketHandler | [0xA4f1Fc88eFF9a72bCc278a2D3B79cafCc1551fb5](https://basescan.org/address/0xA4f1Fc88eFF9a72bCc278a2D3B79cafCc1551fb5) | 3.4.0 | +| BasketLib | [0x182e86ad4a6139ced4f9fa4ed3f1cd9e4f7449e7](https://basescan.org/address/0x182e86ad4a6139ced4f9fa4ed3f1cd9e4f7449e7) | N/A | +| Broker | [0x1cddc45cb390C3b4a739861155E8ee95b7321eD6](https://basescan.org/address/0x1cddc45cb390C3b4a739861155E8ee95b7321eD6) | 3.4.0 | +| Deployer | [0xFD18bA9B2f9241Ce40CDE14079c1cDA1502A8D0A](https://basescan.org/address/0xFD18bA9B2f9241Ce40CDE14079c1cDA1502A8D0A) | 3.4.0 | +| Distributor | [0xba748FAF1a94B5C8De5C8Ca8D87A0906C5B0300c](https://basescan.org/address/0xba748FAF1a94B5C8De5C8Ca8D87A0906C5B0300c) | 3.4.0 | +| DutchTrade | [0x270284ecb6aF0dc521D2c8f9D77b03EEd2aace90](https://basescan.org/address/0x270284ecb6aF0dc521D2c8f9D77b03EEd2aace90) | 3.4.0 | +| Facade | [0xEb2071e9B542555E90E6e4E1F83fa17423583991](https://basescan.org/address/0xEb2071e9B542555E90E6e4E1F83fa17423583991) | N/A | +| FacadeWrite | [0x43E205A805c4be5A62C71d49de68dF60200548A0](https://basescan.org/address/0x43E205A805c4be5A62C71d49de68dF60200548A0) | N/A | +| FacadeWriteLib | [0x186d05580E6B7195323b5dC8c3ee9179Ad086d4C](https://basescan.org/address/0x186d05580E6B7195323b5dC8c3ee9179Ad086d4C) | N/A | +| Furnace | [0xE0B810bD674132b553770064Fc90440c5A5f518d](https://basescan.org/address/0xE0B810bD674132b553770064Fc90440c5A5f518d) | 3.4.0 | +| GNOSIS_EASY_AUCTION | [0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02](https://basescan.org/address/0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02) | N/A | +| GnosisTrade | [0x93de153Ba104D15785c8d8af01AE9425960de49e](https://basescan.org/address/0x93de153Ba104D15785c8d8af01AE9425960de49e) | 3.4.0 | +| Main | [0x2a2A842Dda2Da2170a531dfF4bD4A821321e4485](https://basescan.org/address/0x2a2A842Dda2Da2170a531dfF4bD4A821321e4485) | 3.4.0 | +| MaxIssuableFacet | [0x63FDcB1E8Ee5C4B64A5c4ce0FB97597917920cb6](https://basescan.org/address/0x63FDcB1E8Ee5C4B64A5c4ce0FB97597917920cb6) | N/A | +| ReadFacet | [0x5Af543D6F95a98200Dd770f39A902Fe793BAeB27](https://basescan.org/address/0x5Af543D6F95a98200Dd770f39A902Fe793BAeB27) | N/A | +| RSR | [0xaB36452DbAC151bE02b16Ca17d8919826072f64a](https://basescan.org/address/0xaB36452DbAC151bE02b16Ca17d8919826072f64a) | 1.0.3 | +| RSR_FEED | [0xAa98aE504658766Dfe11F31c5D95a0bdcABDe0b1](https://basescan.org/address/0xAa98aE504658766Dfe11F31c5D95a0bdcABDe0b1) | N/A | +| RsrAsset | [0x02062c16c28A169D1f2F5EfA7eEDc42c3311ec23](https://basescan.org/address/0x02062c16c28A169D1f2F5EfA7eEDc42c3311ec23) | 3.4.0 | +| RsrTrader | [0x3c2460ACa70bedf096f71Cf91fFBc0789F08503f](https://basescan.org/address/0x3c2460ACa70bedf096f71Cf91fFBc0789F08503f) | 3.4.0 | +| RToken | [0x02Ab5B6dF2c17d060EE3e95D08225Ff3A42504a5](https://basescan.org/address/0x02Ab5B6dF2c17d060EE3e95D08225Ff3A42504a5) | 3.4.0 | +| RTokenTrader | [0x3c2460ACa70bedf096f71Cf91fFBc0789F08503f](https://basescan.org/address/0x3c2460ACa70bedf096f71Cf91fFBc0789F08503f) | 3.4.0 | +| StRSR | [0x4Cf200D7fA568611DD8B4BD85053ba9419982C7D](https://basescan.org/address/0x4Cf200D7fA568611DD8B4BD85053ba9419982C7D) | 3.4.0 | +| TradingLib | [0x6419fe6cf428150e2d8ed38a3316b1bb468f79a7](https://basescan.org/address/0x6419fe6cf428150e2d8ed38a3316b1bb468f79a7) | N/A | + \ No newline at end of file diff --git a/docs/deployed-addresses/8453-hyUSD.md b/docs/deployed-addresses/8453-hyUSD.md index 881629127d..08f1a85804 100644 --- a/docs/deployed-addresses/8453-hyUSD.md +++ b/docs/deployed-addresses/8453-hyUSD.md @@ -2,23 +2,23 @@ ## Component Addresses | Contract | Address | Implementation | Version | | --- | --- | --- | --- | -| RToken | [0xCc7FF230365bD730eE4B352cC2492CEdAC49383e](https://basescan.org/address/0xCc7FF230365bD730eE4B352cC2492CEdAC49383e) |[0xa42850a760151bb3acf17e7f8643eb4d864bf7a6](https://basescan.org/address/0xa42850a760151bb3acf17e7f8643eb4d864bf7a6#code) | 3.0.0 | -| Main | [0xA582985c68ED30a052Ff0b07D74931140bd5a00F](https://basescan.org/address/0xA582985c68ED30a052Ff0b07D74931140bd5a00F) |[0x1d6d0b74e7a701ae5c2e11967b242e9861275143](https://basescan.org/address/0x1d6d0b74e7a701ae5c2e11967b242e9861275143#code) | 3.0.0 | -| AssetRegistry | [0xe8209777E3bE69E0f379AE5b2204D301c4FFC9B3](https://basescan.org/address/0xe8209777E3bE69E0f379AE5b2204D301c4FFC9B3) |[0x9c387fc258061bd3e02c851f36ae227db03a396c](https://basescan.org/address/0x9c387fc258061bd3e02c851f36ae227db03a396c#code) | 3.0.0 | -| BackingManager | [0xA1E1A94977ec3159DB546bf01d7a8d17DD3EbBeD](https://basescan.org/address/0xA1E1A94977ec3159DB546bf01d7a8d17DD3EbBeD) |[0x8569d60df34354cdd1115b90de832845b31c28d2](https://basescan.org/address/0x8569d60df34354cdd1115b90de832845b31c28d2#code) | 3.0.1 | -| BasketHandler | [0x9306587db04E35981e57013f6E1D867eCa89e2ec](https://basescan.org/address/0x9306587db04E35981e57013f6E1D867eCa89e2ec) |[0x25e92785c1ac01b397224e0534f3d626868a1cbf](https://basescan.org/address/0x25e92785c1ac01b397224e0534f3d626868a1cbf#code) | 3.0.0 | -| Broker | [0x0E05139662e0C8752a100DB08DA0C7E435B8aC94](https://basescan.org/address/0x0E05139662e0C8752a100DB08DA0C7E435B8aC94) |[0x12c3bb1b0da85fdae0137ae8fde901f7d0e106ba](https://basescan.org/address/0x12c3bb1b0da85fdae0137ae8fde901f7d0e106ba#code) | 3.0.0 | -| RSRTrader | [0xef34C651F1AE9593cfb2CDf02da800A4AAd612bd](https://basescan.org/address/0xef34C651F1AE9593cfb2CDf02da800A4AAd612bd) |[0xf4c5d33dabb9d4681ed9b83618d629ba1006ae16](https://basescan.org/address/0xf4c5d33dabb9d4681ed9b83618d629ba1006ae16#code) | 3.0.1 | -| RTokenTrader | [0xcc03e97F6e2e4eFb50ab95c89BB4b27911105736](https://basescan.org/address/0xcc03e97F6e2e4eFb50ab95c89BB4b27911105736) |[0xf4c5d33dabb9d4681ed9b83618d629ba1006ae16](https://basescan.org/address/0xf4c5d33dabb9d4681ed9b83618d629ba1006ae16#code) | 3.0.1 | -| Distributor | [0xf0a83bC73E9bAeb69b2fBB1e48bCdabf9C1012ca](https://basescan.org/address/0xf0a83bC73E9bAeb69b2fBB1e48bCdabf9C1012ca) |[0xd31de64957b79435bfc702044590ac417e02c19b](https://basescan.org/address/0xd31de64957b79435bfc702044590ac417e02c19b#code) | 3.0.0 | -| Furnace | [0x8532B667150F53f209F71FdF4Ca2173805D16680](https://basescan.org/address/0x8532B667150F53f209F71FdF4Ca2173805D16680) |[0x45d7dfe976cdf80962d863a66918346a457b87bd](https://basescan.org/address/0x45d7dfe976cdf80962d863a66918346a457b87bd#code) | 3.0.0 | -| StRSR | [0x796d2367AF69deB3319B8E10712b8B65957371c3](https://basescan.org/address/0x796d2367AF69deB3319B8E10712b8B65957371c3) |[0x53321f03a7cce52413515dfd0527e0163ec69a46](https://basescan.org/address/0x53321f03a7cce52413515dfd0527e0163ec69a46#code) | 3.0.0 | +| RToken | [0xCc7FF230365bD730eE4B352cC2492CEdAC49383e](https://basescan.org/address/0xCc7FF230365bD730eE4B352cC2492CEdAC49383e) | +| Main | [0xA582985c68ED30a052Ff0b07D74931140bd5a00F](https://basescan.org/address/0xA582985c68ED30a052Ff0b07D74931140bd5a00F) | +| AssetRegistry | [0xe8209777E3bE69E0f379AE5b2204D301c4FFC9B3](https://basescan.org/address/0xe8209777E3bE69E0f379AE5b2204D301c4FFC9B3) | +| BackingManager | [0xA1E1A94977ec3159DB546bf01d7a8d17DD3EbBeD](https://basescan.org/address/0xA1E1A94977ec3159DB546bf01d7a8d17DD3EbBeD) | +| BasketHandler | [0x9306587db04E35981e57013f6E1D867eCa89e2ec](https://basescan.org/address/0x9306587db04E35981e57013f6E1D867eCa89e2ec) | +| Broker | [0x0E05139662e0C8752a100DB08DA0C7E435B8aC94](https://basescan.org/address/0x0E05139662e0C8752a100DB08DA0C7E435B8aC94) | +| RSRTrader | [0xef34C651F1AE9593cfb2CDf02da800A4AAd612bd](https://basescan.org/address/0xef34C651F1AE9593cfb2CDf02da800A4AAd612bd) | +| RTokenTrader | [0xcc03e97F6e2e4eFb50ab95c89BB4b27911105736](https://basescan.org/address/0xcc03e97F6e2e4eFb50ab95c89BB4b27911105736) | +| Distributor | [0xf0a83bC73E9bAeb69b2fBB1e48bCdabf9C1012ca](https://basescan.org/address/0xf0a83bC73E9bAeb69b2fBB1e48bCdabf9C1012ca) | +| Furnace | [0x8532B667150F53f209F71FdF4Ca2173805D16680](https://basescan.org/address/0x8532B667150F53f209F71FdF4Ca2173805D16680) | +| StRSR | [0x796d2367AF69deB3319B8E10712b8B65957371c3](https://basescan.org/address/0x796d2367AF69deB3319B8E10712b8B65957371c3) | ## Governance Addresses | Contract | Address | | --- | --- | -| Governor Alexios | [0xc8e63d3501A246fa1ddBAbe4ad0B50e9d32aA8bb](https://basescan.org/address/0xc8e63d3501A246fa1ddBAbe4ad0B50e9d32aA8bb) | -| Timelock | [0xf093d7f00f3dCe6d415Be564f41Cb4bc032fb367](https://basescan.org/address/0xf093d7f00f3dCe6d415Be564f41Cb4bc032fb367) | +| Governor | [0xffef97179f58a582dEf73e6d2e4BcD2BDC8ca128](https://basescan.org/address/0xffef97179f58a582dEf73e6d2e4BcD2BDC8ca128) | +| Timelock | [0x4284D76a03F9B398FF7aEc58C9dEc94b289070CF](https://basescan.org/address/0x4284D76a03F9B398FF7aEc58C9dEc94b289070CF) | \ No newline at end of file diff --git a/docs/deployed-addresses/index.json b/docs/deployed-addresses/index.json index 41ee236cd2..ce55f2667d 100644 --- a/docs/deployed-addresses/index.json +++ b/docs/deployed-addresses/index.json @@ -4,33 +4,43 @@ "components-2.0.0", "components-2.1.0", "components-3.0.0", - "components-3.0.1" + "components-3.0.1", + "components-3.3.0", + "components-3.4.0" ], "assets": [ "assets-2.0.0", "assets-2.1.0", "assets-3.0.0", - "assets-3.0.1" + "assets-3.0.1", + "assets-3.3.0", + "assets-3.4.0" ], "rtokens": [ "eUSD", "ETH+", "hyUSD", - "USDC+" + "USDC+", + "USD3" ] }, "base": { "components": [ "components-3.0.0", - "components-3.0.1" + "components-3.0.1", + "components-3.3.0", + "components-3.4.0" ], "assets": [ "assets-3.0.0", - "assets-3.0.1" + "assets-3.0.1", + "assets-3.3.0", + "assets-3.4.0" ], "rtokens": [ "hyUSD", - "Vaya" + "Vaya", + "bsdETH" ] } } \ No newline at end of file diff --git a/docs/deployment-variables.md b/docs/deployment-variables.md index eeef1d6c15..e42d7d9049 100644 --- a/docs/deployment-variables.md +++ b/docs/deployment-variables.md @@ -78,6 +78,24 @@ The warmup period is how many seconds should pass after the basket regained the Default value: `900` = 15 minutes Reasonable range: 0 to 604800 +### `reweightable` + +Dimension: `{bool}` + +A reweightable RToken can have its target amounts changed by governance via `forceSetPrimeBasket()`. In general RTokens should be non-reweightable, unless their usecase requires it. The work required to change the target amounts of a reweightable RToken responsibly is non-trivial, and should be done by a spell. + +Default value: `false` + +### `enableIssuancePremium` + +Dimension: `{bool}` + +An RToken with issuance premium enabled will increase issuance costs to compensate for any (partial) de-pegs observed in the underlying collateral. This protects RToken holders and RSR stakers from toxic issuance that may occur on the way to a default, or simply when a collateral reliably trades below its peg, such as frxETH. + +This also creates an additional revenue stream for the RToken that scales with issuance velocity. + +Default value: `true` + ### `batchAuctionLength` Dimension: `{seconds}` @@ -102,10 +120,16 @@ Reasonable range: 100 to 3600 Dimension: `{1}` -The backing buffer is a percentage value that describes how much overcollateralization to hold in the form of RToken. This buffer allows collateral tokens to be converted into RToken, which is a more efficient form of revenue production than trading each individual collateral for the desired RToken, and also adds a small buffer that can prevent RSR from being seized when there are small losses due to slippage during rebalancing. +The backing buffer is a percentage value that describes how much extra collateral to hold in the BackingManager. This can be important for preventing RSR seizure during normal rebalancing as a result of trading slippage. + +However, too large a backing buffer (as a function of the blended collateral yield) can cause RToken and RSR staker yields to become too sensitive to supply changes; new issuance creates a hole that must be filled in before revenue handout can resume, while new redemptions cause the excess capital to be immediately realized as revenue. + +If the backing buffer is set too low, it's possible to get into a situation where RSR stakers begin individually unstaking before rebalancing proposals in order to avoid being slashed. The backing buffer should be high enough to prevent this outcome for expected rebalances. + +It is not important to consider the backing buffer for _default_ scenarios, only governance-led rebalances. Default value: `1e15` = 0.1% -Reasonable range: 1e12 to 1e18 +Reasonable range: 1e13 to 1e17 ### `maxTradeSlippage` diff --git a/docs/dev-env.md b/docs/dev-env.md index 2eebe97f45..1bf6489fbb 100644 --- a/docs/dev-env.md +++ b/docs/dev-env.md @@ -66,6 +66,9 @@ pip3 install solc-select slither-analyzer==0.9.3 # Include slitherin detectors within slither pip3 install slitherin +# Include slitherin detectors within slither +pip3 install slitherin + # Install and use solc version 0.8.19 solc-select install 0.8.19 solc-select use 0.8.19 diff --git a/docs/mev.md b/docs/mev.md index aabad1bc92..5ec0f578a6 100644 --- a/docs/mev.md +++ b/docs/mev.md @@ -11,7 +11,47 @@ Like any protocol, the Reserve Protocol causes some amount of MEV. While the ful ### 1. Issuance/Redemption -MEV searchers can arb an RToken's issuance/redemption price against the broader market, whether that be AMM pools or CEX prices. This is a fairly standard MEV opportunity and it works the way an MEV searcher would expect. All that one needs to be able to do to participate is execute `issue()` or `redeem()` on the `RToken.sol` contract. The issuance requires approvals in advance, while the `redeem()` does not. You can find more documentation elsewhere in the repo about the properties of our `issue()`/`redeem()`/`redeemCustom()` functions. In short, they are atomic and work the way a searcher would expect, with the caveat that `redeem()` will revert during rebalancing (`redeemCustom()` does not). +MEV searchers can arb an RToken's issuance/redemption price against the broader market, whether that be AMM pools or CEX prices. This is a fairly standard MEV opportunity and it works the way an MEV searcher would expect. All that one needs to be able to do to participate is execute `issue()` or `redeem()` on the `RToken.sol` contract. The issuance requires approvals in advance, while the `redeem()` does not. The challenge is in knowing the precise quantities of tokens that will be required in advance. + +A challenge that anyone building on top of the protocol will face is that underlying rates move between the time the tx is constructed and when it is executed on-chain. To get the tightest possible quote, you can execute a static call against the Facade address for your chain using the below interface: + +- Mainnet: https://etherscan.io/address/0x2C7ca56342177343A2954C250702Fd464f4d0613 +- Base: https://basescan.org/address/0xEb2071e9B542555E90E6e4E1F83fa17423583991 +- Arbitrum: https://arbiscan.io/address/0x387A0C36681A22F728ab54426356F4CAa6bB48a9 + +```solidity +function issue(address rToken, uint256 amount) + external + returns ( + address[] memory tokens, + uint256[] memory deposits, + uint192[] memory depositsUoA + ); + +function redeem(address rToken, uint256 amount) + external + returns ( + address[] memory tokens, + uint256[] memory withdrawals, + uint256[] memory available + ); + +function redeemCustom( + IRToken rToken, + uint256 amount, + uint48[] memory basketNonces, + uint192[] memory portions +) external returns (address[] memory tokens, uint256[] memory withdrawals); + +``` + +For issuance, the rates will move in favor of the issuer such that fewer collateral are required than first initially quoted. + +For redemption, the rates will move against the redeemer such that they receive less collateral than first initially quoted. + +These calls do not need to be made from an account with any prerequisite token balances or approvals. It will simulate refreshing all the underlying collateral in the current block and return a quote. + +We do not suggest executing these functions live on-chain, though it is possible. The gas cost is quite high. ### 2. Auctions diff --git a/docs/solidity-style.md b/docs/solidity-style.md index 4bec58a300..35bbf87698 100644 --- a/docs/solidity-style.md +++ b/docs/solidity-style.md @@ -135,6 +135,28 @@ That is, we expect timestamps to be any uint48 value. This should work without change for around 9M years, which is more than enough. +### Collateral decimals + +`{decimals}`: [6, 21] + +The protocol only supports collateral tokens up to 21 decimals, and they must be sufficiently valuable. + +At 21 decimals one whole collateral token must be worth `>= $1` when _at-peg_. This range enables support for `$1` tokens that have been deposited into 3 decimal offset vaults. Note that the protocol does not rely on this property for the _sale_ of collateral, only the _purchase_; therefore it is acceptable for a backing collateral to lose its peg and be worth less than `$1`, as long as its collateral plugin puts it into an IFFY state and begins the default process. + +minimum whole token value requirement (at common decimals): + +- 21 decimals: `>= $1` +- 18 decimals: `>= $0.001` +- 6 decimals: `>= $0.000000000000001` + +### Minimum RToken price + +Whole RTokens should be worth `>= $0.001` at-peg, since they must be purchasable in revenue auctions. + +### Minimum RSR price + +The protocol functions best when whole RSR is worth `>= $0.001`. This constraint is less strong than in the case of backing collateral tokens, however. The core functionality of the protocol functions properly even even below this boundary. + ## Function annotations All core functions that can be called from outside our system are classified into one of the following 3 categories: diff --git a/docs/system-design.md b/docs/system-design.md index 22128c989f..127907e211 100644 --- a/docs/system-design.md +++ b/docs/system-design.md @@ -170,6 +170,12 @@ The `dutchAuctionLength` can be configured to be any value. The suggested defaul The "best plausible price" is equal to the exchange rate at the high price of the sell token and the low price of the buy token. The "worst-case price" is equal to the exchange rate at the low price of the sell token and the high price of the sell token, plus an additional discount equal to `maxTradeSlippage`. +### Collateral decimals restriction + +The protocol only supports collateral tokens with up to 21 decimals, and for these cases only supports balances up to `~8e28`. Exceeding this could end up overflowing the `uint96` restrictions in GnosisTrade / EasyAuction. We expect `~70e6` whole tokens (for 21 decimals) to always be worth more than the `minTradeVolume`. Note that even when this assumption breaks, the protocol behaves gracefully and downsizes the GnosisTrade to be within the limits. + +In terms of rounding, with a 21 decimals token, we lose 3 decimal places when rounding down to our 18 decimal fixed point numbers (up to 999 wei). Even if one whole token is worth 1 billion USD, `1e3` wei will only be worth `1e-9` USD in that case. This is an acceptable loss. + #### Trade violation fallback Dutch auctions become disabled for an asset being traded if a trade clears in the geometric phase. The rationale is that a trade that clears in this range (multiples above the plausible price) only does so because either 1) the auctioned asset's price was manipulated downwards, or 2) the bidding asset was manipulated upwards, such that the protocol accepts an unfavorable trade. All subsequent trades for that particular trading pair will be forced to use the batch auctions as a result. Dutch auctions for disabled assets must be manually re-enabled by governance. diff --git a/docs/writing-collateral-plugins.md b/docs/writing-collateral-plugins.md index 1d2fd57f7e..3e40dda8b2 100644 --- a/docs/writing-collateral-plugins.md +++ b/docs/writing-collateral-plugins.md @@ -8,26 +8,27 @@ For details of what collateral plugins are and how they function, see [collatera Here are some basic questions to answer before beginning to write a new collateral plugin. Think of the answers like an outline for an essay: they will help gauge how much work is required to write the plugin, they will guide the final implementation, and they will contain all of the human-readable details that can then be directly translated into code. +1. **Is this collateral sufficiently valuable? At-peg it must be worth at least $0.001 if it has 18 decimals, and $1 if it has 21 decimals.** 1. **How will this plugin define the different units?** - {tok}: - {ref}: - {target}: - {UoA}: -2. **Does the target collateral require a wrapper?** (eg. aTokens require the StaticAToken wrapper, to stabilize their rebasing nature) -3. **How will the 3 internal prices be defined?** (eg. chainlink feeds, exchange rate view functions, calculations involving multiple sources) For [chainlink feeds](https://data.chain.link/ethereum/mainnet), include the address, error (deviation threshold), and timeout (heartbeat). For on-chain exchange rates, include the function calls and/or github links to examples. +1. **Does the target collateral require a wrapper?** (eg. aTokens require the StaticAToken wrapper, to stabilize their rebasing nature) +1. **How will the 3 internal prices be defined?** (eg. chainlink feeds, exchange rate view functions, calculations involving multiple sources) For [chainlink feeds](https://data.chain.link/ethereum/mainnet), include the address, error (deviation threshold), and timeout (heartbeat). For on-chain exchange rates, include the function calls and/or github links to examples. - {ref/tok}: - {target/ref}: - {UoA/target}: -4. **For each of these prices, what are the critical trust assumptions? Can any of these be manipulated within the course of a transaction?** +1. **For each of these prices, what are the critical trust assumptions? Can any of these be manipulated within the course of a transaction?** - eg. chainlink feeds require trusting the chainlink protocol and the individual oracles for that price feed - - eg. the frxETH/ETH exchange rate requires trusting the FRAX multisig to correctly push timely updates - eg. yearn vaults can have their `pricePerShare` increased via direct vault donations -5. **Are there any protocol-specific metrics that should be monitored to signal a default in the underlying collateral?** -6. **If this plugin requires unique unit & price abstractions, what do they look like?** -7. **What amount of revenue should this plugin hide? (a minimum of `1e-6`% is recommended, but some collateral may require higher thresholds, and, in rare cases, `0` can be used)** -8. **Are there rewards that can be claimed by holding this collateral? If so, how are they claimed?** Include a github link to the callable function or an example of how to claim. -9. **Does the collateral need to be "refreshed" in order to update its internal state before refreshing the plugin?** Include a github link to the callable function. -10. **Can the `price()` range be kept <5%? What is the largest possible % difference (while priced) between `price().high` and `price().low`?** See [RTokenAsset.tryPrice()](../contracts/plugins/assets/RTokenAsset.sol) and [docs/collateral.md](./collateral.md#price) for additional context. + - eg. is pricing manipuable or subject to MEV? oracle sources _must_ be manipulation resistant such as Chainlink or AMM EMA +1. **Are there any protocol-specific metrics that should be monitored to signal a default in the underlying collateral?** +1. **If this plugin requires unique unit & price abstractions, what do they look like?** +1. **What amount of revenue should this plugin hide? (a minimum of `1e-6`% is recommended, but some collateral may require higher thresholds, and, in rare cases, `0` can be used)** +1. **Are there rewards that can be claimed by holding this collateral? If so, how are they claimed?** Include a github link to the callable function or an example of how to claim. +1. **Does the collateral need to be "refreshed" in order to update its internal state before refreshing the plugin?** Include a github link to the callable function. +1. **Can the `price()` range be kept <5%? What is the largest possible % difference (while priced) between `price().high` and `price().low`?** See [RTokenAsset.tryPrice()](../contracts/plugins/assets/RTokenAsset.sol) and [docs/collateral.md](./collateral.md#price) for additional context. ## Implementation diff --git a/package.json b/package.json index 881c4fe247..79e766c6ff 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "test:fast": "bash tools/fast-test.sh", "test:p0": "PROTO_IMPL=0 hardhat test test/*.test.ts", "test:p1": "PROTO_IMPL=1 hardhat test test/*.test.ts ", + "test:registries": "PROTO_IMPL=1 hardhat test test/registries/*.test.ts", "test:plugins": "hardhat test test/{libraries,plugins}/*.test.ts", "test:plugins:integration": "PROTO_IMPL=1 FORK=1 hardhat test test/plugins/individual-collateral/**/*.test.ts", "test:integration": "PROTO_IMPL=1 FORK=1 hardhat test test/integration/**/*.test.ts", diff --git a/scripts/addresses/42161-tmp-assets-collateral.json b/scripts/addresses/42161-tmp-assets-collateral.json new file mode 100644 index 0000000000..2c8f59431c --- /dev/null +++ b/scripts/addresses/42161-tmp-assets-collateral.json @@ -0,0 +1,30 @@ +{ + "assets": { + "COMP": "0x6882560A919714A742afd2A2a0af6b4D8d20cF22", + "ARB": "0x21fBa52dA03e1F964fa521532f8B8951fC212055" + }, + "collateral": { + "DAI": "0x6FE56A3EEa3fEc93601a94D26bEa1876bD48192F", + "USDC": "0xa96aE05dFa869F4FCC4142E8D4E4F2706FEe2B57", + "USDT": "0x3Ac8F000D75a2EA4a9a36c6844410926bc0c32f7", + "saArbUSDCn": "0x7be9Bc50734820516693A376238Cc6Bf029BA682", + "saArbUSDT": "0x529D7e23Ce63efdcE41dA2a41296Fd7399157F5b", + "cUSDCv3": "0x8a5DfEa5cdA35AB374ac558951A3dF1437A6FcA6", + "cvxCrvUSDUSDT": "0xf729b03AcbD60c8aF9B449d51444445815a56d0e", + "cvxCrvUSDUSDC": "0x57547D29cf0D5B4d31c6c71Ec73b3A8c8416ade6", + "wUSDM": "0xA185a9fd314b61f33F740760a59cc46b31297e30" + }, + "erc20s": { + "COMP": "0x354A6dA3fcde098F8389cad84b0182725c6C91dE", + "DAI": "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + "USDC": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", + "USDT": "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9", + "saArbUSDCn": "0x030cDeCBDcA6A34e8De3f49d1798d5f70E3a3414", + "saArbUSDT": "0xffef97179f58a582dEf73e6d2e4BcD2BDC8ca128", + "cUSDCv3": "0xd54804250E9C561AEa9Dee34e9cf2342f767ACC5", + "ARB": "0x912ce59144191c1204e64559fe8253a0e49e6548", + "cvxCrvUSDUSDT": "0xf74d4C9b0F49fb70D8Ff6706ddF39e3a16D61E67", + "cvxCrvUSDUSDC": "0xBFEE9F3E015adC754066424AEd535313dc764116", + "wUSDM": "0x57f5e098cad7a3d1eed53991d4d66c45c9af7812" + } +} \ No newline at end of file diff --git a/scripts/addresses/42161-tmp-deployments.json b/scripts/addresses/42161-tmp-deployments.json new file mode 100644 index 0000000000..7c70ad0ba6 --- /dev/null +++ b/scripts/addresses/42161-tmp-deployments.json @@ -0,0 +1,38 @@ +{ + "prerequisites": { + "RSR": "0xCa5Ca9083702c56b481D1eec86F1776FDbd2e594", + "RSR_FEED": "0xcfF9349ec6d027f20fC9360117fef4a1Ad38B488", + "GNOSIS_EASY_AUCTION": "0xcD033976a011F41D2AB6ef47984041568F818E73" + }, + "tradingLib": "0x348644F24FA34c40a8E3C4Cf9aF14f8a96aD63fC", + "cvxMiningLib": "", + "facade": "0x387A0C36681A22F728ab54426356F4CAa6bB48a9", + "facets": { + "actFacet": "0xE774CCF1431c3DEe7Fa4c20f67534b61289CAa45", + "readFacet": "0x15175d35F3d88548B49600B4ee8067253A2e4e66" + }, + "facadeWriteLib": "0x042D85e9eb1F4372ffA362240E0630229CaA1904", + "basketLib": "0x53f1Df4E5591Ae35Bf738742981669c3767241FA", + "facadeWrite": "0xe2B652E538543d02f985A5E422645A704633956d", + "deployer": "0xfd7eb6B208E1fa7B14E26A1fb10fFC17Cf695d68", + "rsrAsset": "0x7182e3A6E29936C8B14c4fa6f63a62D0b1D0f767", + "implementations": { + "main": "0xf7a9D27c3B60c78c6F6e2c2d6ED6E8B94b352461", + "trading": { + "gnosisTrade": "0xD42620d04fCe65B1F5E8Facc894a2e34D764FEc9", + "dutchTrade": "0x8b4374005291B8FCD14C4E947604b2FB3C660A73" + }, + "components": { + "assetRegistry": "0xA9df960Af018178C0138CD5780c768A0a0A7e61f", + "backingManager": "0xD85Fac03804a3e44D29c494f3761D11A2262cBBe", + "basketHandler": "0x157b0C032192F5714BD68bf33dF96C122EA5e1d6", + "broker": "0xa24E0D3E77Ec4849A288C72F9d9bC4dF84B26558", + "distributor": "0x5Ef74A083Ac932b5f050bf41cDe1F67c659b4b88", + "furnace": "0x8A11D590B32186E1236B5E75F2d8D72c280dc880", + "rsrTrader": "0xaeCa35F0cB9d12D68adC4d734D4383593F109654", + "rTokenTrader": "0xaeCa35F0cB9d12D68adC4d734D4383593F109654", + "rToken": "0xC8f487B34251Eb76761168B70Dc10fA38B0Bd90b", + "stRSR": "0x437b525F96A2Da0A4b165efe27c61bea5c8d3CD4" + } + } +} diff --git a/scripts/addresses/arbitrum-3.4.0/42161-tmp-assets-collateral.json b/scripts/addresses/arbitrum-3.4.0/42161-tmp-assets-collateral.json index dd71b7f889..ca0800c2de 100644 --- a/scripts/addresses/arbitrum-3.4.0/42161-tmp-assets-collateral.json +++ b/scripts/addresses/arbitrum-3.4.0/42161-tmp-assets-collateral.json @@ -11,7 +11,8 @@ "saArbUSDT": "0x529D7e23Ce63efdcE41dA2a41296Fd7399157F5b", "cUSDCv3": "0x8a5DfEa5cdA35AB374ac558951A3dF1437A6FcA6", "cvxCrvUSDUSDT": "0xf729b03AcbD60c8aF9B449d51444445815a56d0e", - "cvxCrvUSDUSDC": "0x57547D29cf0D5B4d31c6c71Ec73b3A8c8416ade6" + "cvxCrvUSDUSDC": "0x57547D29cf0D5B4d31c6c71Ec73b3A8c8416ade6", + "wUSDM": "0xA185a9fd314b61f33F740760a59cc46b31297e30" }, "erc20s": { "COMP": "0x354A6dA3fcde098F8389cad84b0182725c6C91dE", @@ -23,6 +24,7 @@ "cUSDCv3": "0xd54804250E9C561AEa9Dee34e9cf2342f767ACC5", "ARB": "0x912ce59144191c1204e64559fe8253a0e49e6548", "cvxCrvUSDUSDT": "0xf74d4C9b0F49fb70D8Ff6706ddF39e3a16D61E67", - "cvxCrvUSDUSDC": "0xBFEE9F3E015adC754066424AEd535313dc764116" + "cvxCrvUSDUSDC": "0xBFEE9F3E015adC754066424AEd535313dc764116", + "wUSDM": "0x57f5e098cad7a3d1eed53991d4d66c45c9af7812" } } diff --git a/scripts/addresses/arbitrum-sepolia-3.3.0/421614-tmp-deployments.json b/scripts/addresses/arbitrum-sepolia-3.3.0/421614-tmp-deployments.json index 1335c4a9a2..67c8b564c5 100644 --- a/scripts/addresses/arbitrum-sepolia-3.3.0/421614-tmp-deployments.json +++ b/scripts/addresses/arbitrum-sepolia-3.3.0/421614-tmp-deployments.json @@ -35,4 +35,4 @@ "stRSR": "0xfd529fa21FBd569Bcf7c7f49694568fD66e8d1e9" } } -} \ No newline at end of file +} diff --git a/scripts/addresses/base-3.0.1/8453-tmp-assets-collateral.json b/scripts/addresses/base-3.0.1/8453-tmp-assets-collateral.json index d4cab5f1df..efbd43d18e 100644 --- a/scripts/addresses/base-3.0.1/8453-tmp-assets-collateral.json +++ b/scripts/addresses/base-3.0.1/8453-tmp-assets-collateral.json @@ -23,4 +23,4 @@ "wsgUSDbC": "0x073F98792ef4c00bB5f11B1F64f13cB25Cde0d8D", "STG": "0xE3B53AF74a4BF62Ae5511055290838050bf764Df" } -} \ No newline at end of file +} diff --git a/scripts/addresses/base-3.0.1/8453-tmp-deployments.json b/scripts/addresses/base-3.0.1/8453-tmp-deployments.json index 51e21b3e9f..ef16c68668 100644 --- a/scripts/addresses/base-3.0.1/8453-tmp-deployments.json +++ b/scripts/addresses/base-3.0.1/8453-tmp-deployments.json @@ -32,4 +32,4 @@ "stRSR": "0x53321f03A7cce52413515DFD0527e0163ec69A46" } } -} \ No newline at end of file +} diff --git a/scripts/addresses/base-3.3.0/8453-tmp-assets-collateral.json b/scripts/addresses/base-3.3.0/8453-tmp-assets-collateral.json index c03a5cfea0..67a885b897 100644 --- a/scripts/addresses/base-3.3.0/8453-tmp-assets-collateral.json +++ b/scripts/addresses/base-3.3.0/8453-tmp-assets-collateral.json @@ -10,4 +10,4 @@ "cUSDCv3": "0xA694f7177C6c839C951C74C797283B35D0A486c8", "USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" } -} \ No newline at end of file +} diff --git a/scripts/addresses/mainnet-3.0.0/1-tmp-assets-collateral.json b/scripts/addresses/mainnet-3.0.0/1-tmp-assets-collateral.json index d03e354462..c0cceb721d 100644 --- a/scripts/addresses/mainnet-3.0.0/1-tmp-assets-collateral.json +++ b/scripts/addresses/mainnet-3.0.0/1-tmp-assets-collateral.json @@ -97,4 +97,4 @@ "maStETH": "0x97F9d5ed17A0C99B279887caD5254d15fb1B619B", "aEthUSDC": "0x63e12c3b2DBCaeF1835Bb99Ac1Fdb0Ebe1bE69bE" } -} \ No newline at end of file +} diff --git a/scripts/addresses/mainnet-3.0.1/1-tmp-assets-collateral.json b/scripts/addresses/mainnet-3.0.1/1-tmp-assets-collateral.json index bb2d77e4f5..077e05d768 100644 --- a/scripts/addresses/mainnet-3.0.1/1-tmp-assets-collateral.json +++ b/scripts/addresses/mainnet-3.0.1/1-tmp-assets-collateral.json @@ -99,4 +99,4 @@ "aEthUSDC": "0x63e12c3b2DBCaeF1835Bb99Ac1Fdb0Ebe1bE69bE", "sFRAX": "0xA663B02CF0a4b149d2aD41910CB81e23e1c41c32" } -} \ No newline at end of file +} diff --git a/scripts/addresses/mainnet-3.0.1/1-tmp-deployments.json b/scripts/addresses/mainnet-3.0.1/1-tmp-deployments.json index 33f9664624..710105331f 100644 --- a/scripts/addresses/mainnet-3.0.1/1-tmp-deployments.json +++ b/scripts/addresses/mainnet-3.0.1/1-tmp-deployments.json @@ -32,4 +32,4 @@ "stRSR": "0xC98eaFc9F249D90e3E35E729e3679DD75A899c10" } } -} \ No newline at end of file +} diff --git a/scripts/addresses/mainnet-3.3.0/1-tmp-assets-collateral.json b/scripts/addresses/mainnet-3.3.0/1-tmp-assets-collateral.json index 99aaaa7cc6..c030bf320c 100644 --- a/scripts/addresses/mainnet-3.3.0/1-tmp-assets-collateral.json +++ b/scripts/addresses/mainnet-3.3.0/1-tmp-assets-collateral.json @@ -33,4 +33,4 @@ "CRV": "0xD533a949740bb3306d119CC777fa900bA034cd52", "CVX": "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B" } -} \ No newline at end of file +} diff --git a/scripts/compile-addresses.sh b/scripts/compile-addresses.sh index 98e3d69343..3c274d6565 100755 --- a/scripts/compile-addresses.sh +++ b/scripts/compile-addresses.sh @@ -4,26 +4,31 @@ # *** Ethereum Mainnet *** # eUSD -npx hardhat get-addys --rtoken 0xA0d69E286B938e21CBf7E51D71F6A4c8918f482F --gov 0x7e880d8bD9c9612D6A9759F96aCD23df4A4650E6 --network mainnet +npx hardhat get-addys --rtoken 0xA0d69E286B938e21CBf7E51D71F6A4c8918f482F --gov 0xf4A9288D5dEb0EaE987e5926795094BF6f4662F8 --network mainnet # ETH+ -npx hardhat get-addys --rtoken 0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8 --gov 0x239cDcBE174B4728c870A24F77540dAB3dC5F981 --network mainnet +npx hardhat get-addys --rtoken 0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8 --gov 0x868Fe81C276d730A1995Dc84b642E795dFb8F753 --network mainnet # hyUSD -npx hardhat get-addys --rtoken 0xaCdf0DBA4B9839b96221a8487e9ca660a48212be --gov 0x22d7937438b4bBf02f6cA55E3831ABB94Bd0b6f1 --network mainnet +npx hardhat get-addys --rtoken 0xaCdf0DBA4B9839b96221a8487e9ca660a48212be --gov 0x3F26EF1460D21A99425569Ef3148Ca6059a7eEAe --network mainnet # USDC+ npx hardhat get-addys --rtoken 0xFc0B1EEf20e4c68B3DCF36c4537Cfa7Ce46CA70b --gov 0xc837C557071D604bCb1058c8c4891ddBe8FDD630 --network mainnet +# USD3 +npx hardhat get-addys --rtoken 0x0d86883FAf4FfD7aEb116390af37746F45b6f378 --gov 0x441808e20E625e0094b01B40F84af89436229279 --network mainnet # *** Base L2 *** # hyUSD -npx hardhat get-addys --rtoken 0xCc7FF230365bD730eE4B352cC2492CEdAC49383e --gov 0xc8e63d3501A246fa1ddBAbe4ad0B50e9d32aA8bb --network base +npx hardhat get-addys --rtoken 0xCc7FF230365bD730eE4B352cC2492CEdAC49383e --gov 0xffef97179f58a582dEf73e6d2e4BcD2BDC8ca128 --network base # VAYA npx hardhat get-addys --rtoken 0xC9a3e2B3064c1c0546D3D0edc0A748E9f93Cf18d --gov 0xEb583EA06501f92E994C353aD2741A35582987aA --network base +# bsdETH +npx hardhat get-addys --rtoken 0xCb327b99fF831bF8223cCEd12B1338FF3aA322Ff --gov 0x21fBa52dA03e1F964fa521532f8B8951fC212055 --network base + # Components # *** Ethereum Mainnet *** @@ -31,7 +36,11 @@ npx hardhat get-addys --ver "2.0.0" --network mainnet npx hardhat get-addys --ver "2.1.0" --network mainnet npx hardhat get-addys --ver "3.0.0" --network mainnet npx hardhat get-addys --ver "3.0.1" --network mainnet +npx hardhat get-addys --ver "3.3.0" --network mainnet +npx hardhat get-addys --ver "3.4.0" --network mainnet # *** Base L2 *** npx hardhat get-addys --ver "3.0.0" --network base -npx hardhat get-addys --ver "3.0.1" --network base \ No newline at end of file +npx hardhat get-addys --ver "3.0.1" --network base +npx hardhat get-addys --ver "3.3.0" --network base +npx hardhat get-addys --ver "3.4.0" --network base diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 07e0b496f2..65f82bd7c0 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -110,6 +110,7 @@ async function main() { 'phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts', 'phase2-assets/collaterals/deploy_convex_crvusd_usdc_collateral.ts', 'phase2-assets/collaterals/deploy_convex_crvusd_usdt_collateral.ts', + 'phase2-assets/collaterals/deploy_usdm.ts', 'phase2-assets/assets/deploy_arb.ts' ) } 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 4ac4e4c6c8..72239c83c7 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_stargate_usdt_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_stargate_usdt_collateral.ts @@ -50,7 +50,9 @@ async function main() { /******** Deploy Stargate USDT Wrapper **************************/ - const WrapperFactory: ContractFactory = await hre.ethers.getContractFactory('StargateRewardableWrapper') + const WrapperFactory: ContractFactory = await hre.ethers.getContractFactory( + 'StargateRewardableWrapper' + ) const erc20 = await WrapperFactory.deploy( 'Wrapped Stargate USDT', diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_usdm.ts b/scripts/deployment/phase2-assets/collaterals/deploy_usdm.ts new file mode 100644 index 0000000000..a485083117 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_usdm.ts @@ -0,0 +1,106 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { arbitrumL2Chains, networkConfig } from '../../../../common/configuration' +import { fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { USDMCollateral } from '../../../../typechain' +import { ContractFactory } from 'ethers' +import { + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + PRICE_TIMEOUT, + ORACLE_TIMEOUT, + ORACLE_ERROR, +} from '../../../../test/plugins/individual-collateral/mountain/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 USDM Collateral - wUSDM **************************/ + let collateral: USDMCollateral + + // Only on Arbitrum for now + if (arbitrumL2Chains.includes(hre.network.name)) { + const USDMCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'USDMCollateral' + ) + + collateral = await USDMCollateralFactory.connect(deployer).deploy( + { + priceTimeout: PRICE_TIMEOUT.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.wUSDM, + oracleError: ORACLE_ERROR.toString(), // 1% + erc20: networkConfig[chainId].tokens.wUSDM, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: ORACLE_TIMEOUT.toString(), // 24 hr + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: DEFAULT_THRESHOLD.toString(), + delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), // 24h + }, + fp('1e-6') + ) + } else { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + await collateral.deployed() + + console.log( + `Deployed USDM (wUSDM) Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + // await (await collateral.refresh()).wait() + // expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + console.log( + '🚨 The wUSDM collateral requires chronicle to whitelist the collateral plugin after deployment 🚨' + ) + + console.log( + '🚨 After that, we need to return to this plugin and refresh() it and confirm it is SOUND 🚨' + ) + + assetCollDeployments.collateral.wUSDM = collateral.address + assetCollDeployments.erc20s.wUSDM = networkConfig[chainId].tokens.wUSDM + 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/refresh-whales.ts b/scripts/refresh-whales.ts index 5f170ae066..e55c1ba9e1 100644 --- a/scripts/refresh-whales.ts +++ b/scripts/refresh-whales.ts @@ -2,20 +2,20 @@ import hre from 'hardhat' import { getChainId } from '../common/blockchain-utils' import { ITokens, ITokensKeys, networkConfig } from '#/common/configuration' import { whileImpersonating } from '#/utils/impersonation' -import axios from "axios"; -import * as cheerio from "cheerio"; -import { NetworkWhales, RTOKENS, getWhalesFile, getWhalesFileName } from './whalesConfig'; +import axios from 'axios' +import * as cheerio from 'cheerio' +import { NetworkWhales, RTOKENS, getWhalesFile, getWhalesFileName } from './whalesConfig' import fs from 'fs' -import { useEnv } from '#/utils/env'; +import { useEnv } from '#/utils/env' // set to true to force a refresh of all whales -const FORCE_REFRESH = useEnv('FORCE_WHALE_REFRESH'); +const FORCE_REFRESH = useEnv('FORCE_WHALE_REFRESH') const BASESCAN_API_KEY = useEnv('BASESCAN_API_KEY') const FORK_NETWORK = useEnv('FORK_NETWORK') -const SCANNER_URLS: {[key: string]: string} = { - 'mainnet': 'etherscan.io', - 'base': 'basescan.org' +const SCANNER_URLS: { [key: string]: string } = { + mainnet: 'etherscan.io', + base: 'basescan.org', } const getstRSRs = async (rTokens: string[]) => { @@ -51,61 +51,65 @@ async function main() { const isGoodWhale = (whale: string) => { return !stRSRs.includes(whale) } - const getBigWhale = async (token: string) => { const ethUrl = `https://${SCANNER_URLS[FORK_NETWORK]}/token/generic-tokenholders2?m=light&a=${token}&p=1` // const response = await axios.get(ethUrl); if (FORK_NETWORK === 'mainnet') { - const response = await axios.get(ethUrl); - const selector = cheerio.load(response.data); - let found = false; - let i = 0; - let whale = ""; + const response = await axios.get(ethUrl) + const selector = cheerio.load(response.data) + let found = false + let i = 0 + let whale = '' while (!found) { - whale = selector(selector("tbody > tr")[i]).find("td > div > .link-secondary")[0].attribs['data-clipboard-text']; + whale = selector(selector('tbody > tr')[i]).find('td > div > .link-secondary')[0].attribs[ + 'data-clipboard-text' + ] if (isGoodWhale(whale)) { - found = true; - break; + found = true + break } - i++; + i++ } return whale } else if (FORK_NETWORK === 'base') { const response = await fetch(ethUrl, { - "headers": { - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "accept-language": "en-US,en;q=0.9", - "cache-control": "max-age=0", - "priority": "u=0, i", - "sec-ch-ua": "\"Chromium\";v=\"124\", \"Google Chrome\";v=\"124\", \"Not-A.Brand\";v=\"99\"", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "\"macOS\"", - "sec-fetch-dest": "document", - "sec-fetch-mode": "navigate", - "sec-fetch-site": "none", - "sec-fetch-user": "?1", - "upgrade-insecure-requests": "1" + headers: { + accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'accept-language': 'en-US,en;q=0.9', + 'cache-control': 'max-age=0', + priority: 'u=0, i', + 'sec-ch-ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'sec-fetch-dest': 'document', + 'sec-fetch-mode': 'navigate', + 'sec-fetch-site': 'none', + 'sec-fetch-user': '?1', + 'upgrade-insecure-requests': '1', }, - "referrerPolicy": "strict-origin-when-cross-origin", - "body": null, - "method": "GET", - "mode": "cors", - "credentials": "include" - }); - const selector = cheerio.load(await response.text()); - let found = false; - let i = 0; - let whale = ""; + referrerPolicy: 'strict-origin-when-cross-origin', + body: null, + method: 'GET', + mode: 'cors', + credentials: 'include', + }) + const selector = cheerio.load(await response.text()) + let found = false + let i = 0 + let whale = '' while (!found) { - whale = selector(selector("tbody > tr")[i]).find("td > span > a")[0].attribs['href'].split('?a=')[1]; + whale = selector(selector('tbody > tr')[i]) + .find('td > span > a')[0] + .attribs['href'].split('?a=')[1] if (isGoodWhale(whale)) { - found = true; - break; + found = true + break } - i++; + i++ } - return whale; + return whale } else { throw new Error('Invalid network') } @@ -117,7 +121,12 @@ async function main() { let tokenWhale = whales.tokens[tokenAddress] let lastUpdated = whales.lastUpdated[tokenAddress] // only get a big whale if the whale is not already set or if it was last updated more than 1 day ago - if (!FORCE_REFRESH && tokenWhale && lastUpdated && new Date().getTime() - new Date(lastUpdated).getTime() < 86400000) { + if ( + !FORCE_REFRESH && + tokenWhale && + lastUpdated && + new Date().getTime() - new Date(lastUpdated).getTime() < 86400000 + ) { console.log('Whale already set for', tokenAddress, 'skipping...') return } @@ -133,7 +142,6 @@ async function main() { console.error('Error getting whale for', tokenAddress, error) } } - // ERC20 Collaterals const tokens: ITokensKeys = Object.keys(networkConfig[chainId].tokens) as ITokensKeys for (let i = 0; i < tokens.length; i++) { @@ -155,4 +163,4 @@ main() .catch((error) => { console.error(error) process.exit(1) - }) \ No newline at end of file + }) diff --git a/scripts/verification/assets/verify_stg.ts b/scripts/verification/assets/verify_stg.ts index 794d417834..874254e465 100644 --- a/scripts/verification/assets/verify_stg.ts +++ b/scripts/verification/assets/verify_stg.ts @@ -2,7 +2,13 @@ import hre from 'hardhat' import { getChainId } from '../../../common/blockchain-utils' import { developmentChains, networkConfig } from '../../../common/configuration' -import { getAssetCollDeploymentFilename, getDeploymentFile, getDeploymentFilename, IAssetCollDeployments, IDeployments } from '../../deployment/common' +import { + getAssetCollDeploymentFilename, + getDeploymentFile, + getDeploymentFilename, + IAssetCollDeployments, + IDeployments, +} from '../../deployment/common' import { verifyContract } from '../../deployment/utils' import { fp } from '../../../common/numbers' diff --git a/scripts/verification/collateral-plugins/verify_stargate_usdc.ts b/scripts/verification/collateral-plugins/verify_stargate_usdc.ts index 3973a5ee55..56ab328ffa 100644 --- a/scripts/verification/collateral-plugins/verify_stargate_usdc.ts +++ b/scripts/verification/collateral-plugins/verify_stargate_usdc.ts @@ -7,7 +7,13 @@ import { getAssetCollDeploymentFilename, IAssetCollDeployments, } from '../../deployment/common' -import { priceTimeout, oracleTimeout, verifyContract, combinedError, revenueHiding } from '../../deployment/utils' +import { + priceTimeout, + oracleTimeout, + verifyContract, + combinedError, + revenueHiding, +} from '../../deployment/utils' let deployments: IAssetCollDeployments @@ -33,16 +39,16 @@ async function main() { const sUSDC = networkConfig[chainId].tokens.sUSDC await verifyContract( - chainId, - deployments.erc20s.wsgUSDC, - [ - name, - symbol, - networkConfig[chainId].tokens.STG, - networkConfig[chainId].STARGATE_STAKING_CONTRACT, - sUSDC - ], - 'contracts/plugins/assets/stargate/StargateRewardableWrapper.sol:StargateRewardableWrapper' + chainId, + deployments.erc20s.wsgUSDC, + [ + name, + symbol, + networkConfig[chainId].tokens.STG, + networkConfig[chainId].STARGATE_STAKING_CONTRACT, + sUSDC, + ], + 'contracts/plugins/assets/stargate/StargateRewardableWrapper.sol:StargateRewardableWrapper' ) const oracleError = fp('0.0025') // 0.25% @@ -62,7 +68,7 @@ async function main() { defaultThreshold: fp('0.01').add(oracleError).toString(), delayUntilDefault: bn('86400').toString(), // 24h }, - revenueHiding.toString() + revenueHiding.toString(), ], 'contracts/plugins/assets/stargate/StargatePoolFiatCollateral.sol:StargatePoolFiatCollateral' ) @@ -72,16 +78,16 @@ async function main() { const sUSDC = networkConfig[chainId].tokens.sUSDbC await verifyContract( - chainId, - deployments.erc20s.wsgUSDbC, - [ - name, - symbol, - networkConfig[chainId].tokens.STG, - networkConfig[chainId].STARGATE_STAKING_CONTRACT, - sUSDC - ], - 'contracts/plugins/assets/stargate/StargateRewardableWrapper.sol:StargateRewardableWrapper' + chainId, + deployments.erc20s.wsgUSDbC, + [ + name, + symbol, + networkConfig[chainId].tokens.STG, + networkConfig[chainId].STARGATE_STAKING_CONTRACT, + sUSDC, + ], + 'contracts/plugins/assets/stargate/StargateRewardableWrapper.sol:StargateRewardableWrapper' ) const oracleError = fp('0.003') // 0.3% @@ -91,17 +97,17 @@ async function main() { deployments.collateral.wsgUSDbC, [ { - priceTimeout: priceTimeout.toString(), - chainlinkFeed: networkConfig['8453'].chainlinkFeeds.USDC!, - oracleError: oracleError.toString(), - erc20: deployments.erc20s.wsgUSDbC!, - maxTradeVolume: fp('1e6').toString(), // $1m, - oracleTimeout: oracleTimeout('8453', '86400').toString(), // 24h hr, - targetName: hre.ethers.utils.formatBytes32String('USD'), - defaultThreshold: fp('0.01').add(oracleError).toString(), // ~2.5% - delayUntilDefault: bn('86400').toString(), // 24h + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig['8453'].chainlinkFeeds.USDC!, + oracleError: oracleError.toString(), + erc20: deployments.erc20s.wsgUSDbC!, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: oracleTimeout('8453', '86400').toString(), // 24h hr, + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01').add(oracleError).toString(), // ~2.5% + delayUntilDefault: bn('86400').toString(), // 24h }, - revenueHiding.toString() + revenueHiding.toString(), ], 'contracts/plugins/assets/stargate/StargatePoolFiatCollateral.sol:StargatePoolFiatCollateral' ) diff --git a/scripts/verification/collateral-plugins/verify_usdm.ts b/scripts/verification/collateral-plugins/verify_usdm.ts new file mode 100644 index 0000000000..f67ec64603 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_usdm.ts @@ -0,0 +1,60 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { fp } from '../../../common/numbers' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { verifyContract } from '../../deployment/utils' +import { + PRICE_TIMEOUT, + ORACLE_ERROR, + ORACLE_TIMEOUT, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, +} from '../../../test/plugins/individual-collateral/mountain/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 wUSDM COllateral **************************/ + await verifyContract( + chainId, + deployments.collateral.wUSDM, + [ + { + priceTimeout: PRICE_TIMEOUT.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.wUSDM, + oracleError: ORACLE_ERROR.toString(), + erc20: networkConfig[chainId].tokens.wUSDM, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: ORACLE_TIMEOUT.toString(), + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: DEFAULT_THRESHOLD.toString(), + delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), + }, + fp('1e-6'), + ], + 'contracts/plugins/assets/mountain/USDMCollateral.sol:USDMCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verify_etherscan.ts b/scripts/verify_etherscan.ts index a5ca75c15b..811916c60c 100644 --- a/scripts/verify_etherscan.ts +++ b/scripts/verify_etherscan.ts @@ -93,7 +93,8 @@ async function main() { 'collateral-plugins/verify_aave_v3_usdc.ts', 'collateral-plugins/verify_cusdcv3.ts', 'collateral-plugins/verify_convex_crvusd_usdc.ts', - 'collateral-plugins/verify_convex_crvusd_usdt.ts' + 'collateral-plugins/verify_convex_crvusd_usdt.ts', + 'collateral-plugins/verify_usdm.ts' ) } diff --git a/tasks/deployment/get-addresses.ts b/tasks/deployment/get-addresses.ts index f8433460d4..c36f6c8cf8 100644 --- a/tasks/deployment/get-addresses.ts +++ b/tasks/deployment/get-addresses.ts @@ -23,19 +23,19 @@ task('get-addys', 'Compile the deployed addresses of an RToken deployment') /* Helper functions */ - - // hacky api throttler, basescan has rate limits 5req/sec - const delay = async (ms: number) => { - return new Promise( resolve => setTimeout(resolve, ms) ); + + // hacky api throttler, basescan has rate limits 5req/sec + const delay = async (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)) } - + const capitalize = (s: string) => s && s[0].toUpperCase() + s.slice(1) - + const chainId = await getChainId(hre) const network: Network = hre.network.name as Network - let scannerUrl: string; - let scannerApiUrl: string; - switch(network) { + let scannerUrl: string + let scannerApiUrl: string + switch (network) { case 'mainnet': scannerUrl = 'https://etherscan.io/address/' scannerApiUrl = `https://api.etherscan.io/api` @@ -65,7 +65,9 @@ task('get-addys', 'Compile the deployed addresses of an RToken deployment') const component = await hre.ethers.getContractAt('ComponentP1', address) let row = `| ${name} | [${address}](${scannerUrl}${address}) |` if (!!implementation) { - row += `[${implementation}](${scannerUrl}${implementation}#code) | ${await getVersion(component)} |` + row += `[${implementation}](${scannerUrl}${implementation}#code) | ${await getVersion( + component + )} |` } return row } @@ -95,8 +97,8 @@ task('get-addys', 'Compile the deployed addresses of an RToken deployment') isRToken ? rows.push(await createRTokenTableRow(component.name, component.address)) : isComponent - ? rows.push(await createComponentTableRow(component.name, component.address)) - : rows.push(await createAssetTableRow(component.name, component.address)) + ? rows.push(await createComponentTableRow(component.name, component.address)) + : rows.push(await createAssetTableRow(component.name, component.address)) } return rows.join('\n') } @@ -125,10 +127,7 @@ ${govRows} ` } - const createComponentMarkdown = async ( - name: string, - rows: string - ) => { + const createComponentMarkdown = async (name: string, rows: string) => { return `# ${name} ## Component Addresses | Contract | Address | Version | @@ -221,7 +220,7 @@ ${collaterals} } const govComponents = [ - { name: 'Governor Alexios', address: params.gov }, + { name: 'Governor', address: params.gov }, { name: 'Timelock', address: timelock }, ] @@ -239,11 +238,11 @@ ${collaterals} const rTokenFileName = await getRTokenFileName(params.rtoken) fs.writeFileSync(rTokenFileName, markdown) console.log(`Wrote ${rTokenFileName}`) - - toc[network]['rtokens'].indexOf(rTokenSymbol) === -1 && toc[network]['rtokens'].push(rTokenSymbol) + + toc[network]['rtokens'].indexOf(rTokenSymbol) === -1 && + toc[network]['rtokens'].push(rTokenSymbol) fs.writeFileSync(tocFilename, JSON.stringify(toc, null, 2)) console.log(`Updated table of contents`) - } else if (params.ver) { console.log(`Collecting addresses for Version: ${params.ver} (${hre.network.name})`) // if version is provided, print implementation addresses @@ -306,7 +305,8 @@ ${collaterals} fs.writeFileSync(componentFileName, componentMarkdown) console.log(`Wrote ${componentFileName}`) - toc[network]['components'].indexOf(componentFileId) === -1 && toc[network]['components'].push(componentFileId) + toc[network]['components'].indexOf(componentFileId) === -1 && + toc[network]['components'].push(componentFileId) toc[network]['assets'].indexOf(assetFileId) === -1 && toc[network]['assets'].push(assetFileId) fs.writeFileSync(tocFilename, JSON.stringify(toc, null, 2)) console.log(`Updated table of contents`) diff --git a/tasks/testing/upgrade-checker-utils/governance.ts b/tasks/testing/upgrade-checker-utils/governance.ts deleted file mode 100644 index 37ca3c0157..0000000000 --- a/tasks/testing/upgrade-checker-utils/governance.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { ProposalState } from '#/common/constants' -import { bn } from '#/common/numbers' -import { whileImpersonating } from '#/utils/impersonation' -import { Delegate, Proposal, getDelegates, getProposalDetails } from '#/utils/subgraph' -import { advanceBlocks, advanceTime } from '#/utils/time' -import { BigNumber, PopulatedTransaction } from 'ethers' -import { HardhatRuntimeEnvironment } from 'hardhat/types' -import { pushOraclesForward } from './oracles' - -const validatePropState = async (propState: ProposalState, expectedState: ProposalState) => { - if (propState !== expectedState) { - throw new Error( - `Proposal should be ${ProposalState[expectedState]} but was ${ProposalState[propState]}` - ) - } -} - -export const moveProposalToActive = async ( - hre: HardhatRuntimeEnvironment, - rtokenAddress: string, - governorAddress: string, - proposalId: string -) => { - console.log('Activating Proposal:', proposalId) - - const governor = await hre.ethers.getContractAt('Governance', governorAddress) - const propState = await governor.state(proposalId) - - if (propState == ProposalState.Pending) { - console.log(`Proposal is PENDING, moving to ACTIVE...`) - - // Advance time to start voting - const votingDelay = await governor.votingDelay() - await advanceBlocks(hre, votingDelay.add(2)) - } else { - if (propState == ProposalState.Active) { - console.log(`Proposal is already ${ProposalState[ProposalState.Active]}... skipping step.`) - } else { - throw Error(`Proposal should be ${ProposalState[ProposalState.Pending]} at this step.`) - } - } - - await validatePropState(await governor.state(proposalId), ProposalState.Active) -} - -export const voteProposal = async ( - hre: HardhatRuntimeEnvironment, - rtokenAddress: string, - governorAddress: string, - proposalId: string, - proposal?: Proposal -) => { - console.log('Voting Proposal:', proposalId) - - const governor = await hre.ethers.getContractAt('Governance', governorAddress) - const propState = await governor.state(proposalId) - - if (propState == ProposalState.Active) { - console.log(`Proposal is ACTIVE, moving to SUCCEEDED...`) - - if (!proposal) { - // gather enough whale voters - let whales: Array = await getDelegates(rtokenAddress.toLowerCase()) - const startBlock = await governor.proposalSnapshot(proposalId) - const quorum = await governor.quorum(startBlock) - - let quorumNotReached = true - let currentVoteAmount = BigNumber.from(0) - let i = 0 - while (quorumNotReached) { - const whale = whales[i] - currentVoteAmount = currentVoteAmount.add(BigNumber.from(whale.delegatedVotesRaw)) - i += 1 - if (currentVoteAmount.gt(quorum)) { - quorumNotReached = false - } - } - - whales = whales.slice(0, i) - - // cast enough votes to pass the proposal - for (const whale of whales) { - await whileImpersonating(hre, whale.address, async (signer) => { - await governor.connect(signer).castVote(proposalId, 1) - }) - } - } else { - // Vote from testing account, on the assumption it is staked/delegated - const [tester] = await hre.ethers.getSigners() - await governor.connect(tester).castVote(proposalId, 1) - } - } - - await validatePropState(await governor.state(proposalId), ProposalState.Active) -} - -export const passProposal = async ( - hre: HardhatRuntimeEnvironment, - rtokenAddress: string, - governorAddress: string, - proposalId: string -) => { - console.log('Passing Proposal:', proposalId) - - const governor = await hre.ethers.getContractAt('Governance', governorAddress) - const propState = await governor.state(proposalId) - - if (propState == ProposalState.Active) { - // Advance time till voting is complete - const votingPeriod = await governor.votingPeriod() - await advanceBlocks(hre, votingPeriod.add(1)) - } - - await validatePropState(await governor.state(proposalId), ProposalState.Succeeded) -} - -export const executeProposal = async ( - hre: HardhatRuntimeEnvironment, - rtokenAddress: string, - governorAddress: string, - proposalId: string, - proposal?: Proposal, - extraAssets: string[] = [] -) => { - console.log('Executing Proposal:', proposalId) - const governor = await hre.ethers.getContractAt('Governance', governorAddress) - - // Check proposal state - let propState = await governor.state(proposalId) - - let descriptionHash: string - - if (propState == ProposalState.Succeeded) { - console.log(`Proposal is SUCCEEDED, moving to QUEUED...`) - - if (!proposal) { - proposal = await getProposalDetails(proposalId) - } - - descriptionHash = hre.ethers.utils.keccak256(hre.ethers.utils.toUtf8Bytes(proposal.description)) - // Queue proposal - await governor.queue(proposal.targets, proposal.values, proposal.calldatas, descriptionHash) - - // Check proposal state - propState = await governor.state(proposalId) - await validatePropState(propState, ProposalState.Queued) - } - - if (propState == ProposalState.Queued) { - console.log(`Proposal is QUEUED, moving to EXECUTED...`) - - if (!proposal) { - proposal = await getProposalDetails(`${governorAddress.toLowerCase()}-${proposalId}`) - } - - descriptionHash = hre.ethers.utils.keccak256(hre.ethers.utils.toUtf8Bytes(proposal.description)) - - const timelock = await hre.ethers.getContractAt('TimelockController', await governor.timelock()) - const minDelay = await timelock.getMinDelay() - - console.log('Preparing execution...') - // Advance time required by timelock - await advanceTime(hre, minDelay.add(1).toString()) - await advanceBlocks(hre, 1) - - /* - ** Executing proposals requires that the oracles aren't stale. - ** Make sure to specify any extra assets that may have been registered. - */ - await pushOraclesForward(hre, rtokenAddress, extraAssets) - - console.log('Executing now...') - - // Execute - await governor.execute(proposal.targets, proposal.values, proposal.calldatas, descriptionHash) - - propState = await governor.state(proposalId) - await validatePropState(propState, ProposalState.Executed) - } else { - throw new Error('Proposal should be queued') - } - - console.log(`Proposal is EXECUTED.`) -} - -export const stakeAndDelegateRsr = async ( - hre: HardhatRuntimeEnvironment, - rtokenAddress: string, - user: string -) => { - const rToken = await hre.ethers.getContractAt('RTokenP1', rtokenAddress) - const main = await hre.ethers.getContractAt('IMain', await rToken.main()) - const stRSR = await hre.ethers.getContractAt('StRSRP1Votes', await main.stRSR()) - const rsr = await hre.ethers.getContractAt('StRSRP1Votes', await main.rsr()) - - await whileImpersonating(hre, user, async (signer) => { - const bal = await rsr.balanceOf(signer.address) - await rsr.approve(stRSR.address, bal) - await stRSR.stake(bal) - await stRSR.delegate(signer.address) - }) -} - -export const buildProposal = (txs: Array, description: string): Proposal => { - const targets = txs.map((tx: PopulatedTransaction) => tx.to!) - const values = txs.map(() => bn(0)) - const calldatas = txs.map((tx: PopulatedTransaction) => tx.data!) - return { - targets, - values, - calldatas, - description, - } -} - -export type ProposalBuilder = ( - hre: HardhatRuntimeEnvironment, - rTokenAddress: string, - governorAddress: string -) => Promise - -export const proposeUpgrade = async ( - hre: HardhatRuntimeEnvironment, - rTokenAddress: string, - governorAddress: string, - proposalBuilder: ProposalBuilder -) => { - console.log(`\nGenerating and proposing proposal...`) - const [tester] = await hre.ethers.getSigners() - - await hre.run('give-rsr', { address: tester.address }) - await stakeAndDelegateRsr(hre, rTokenAddress, tester.address) - - const proposal = await proposalBuilder(hre, rTokenAddress, governorAddress) - - const governor = await hre.ethers.getContractAt('Governance', governorAddress) - - const call = await governor.populateTransaction.propose( - proposal.targets, - proposal.values, - proposal.calldatas, - proposal.description - ) - - console.log(`Proposal Transaction:\n`, call.data) - - const r = await governor.propose( - proposal.targets, - proposal.values, - proposal.calldatas, - proposal.description - ) - const resp = await r.wait() - - console.log('\nSuccessfully proposed!') - console.log(`Proposal ID: ${resp.events![0].args!.proposalId}`) - - return { - ...proposal, - proposalId: resp.events![0].args!.proposalId as string, - } -} diff --git a/tasks/testing/upgrade-checker-utils/oracles.ts b/tasks/testing/upgrade-checker-utils/oracles.ts deleted file mode 100644 index a4d1c72aac..0000000000 --- a/tasks/testing/upgrade-checker-utils/oracles.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* eslint-disable no-empty */ -import { networkConfig } from '../../../common/configuration' -import { EACAggregatorProxyMock } from '@typechain/EACAggregatorProxyMock' -import { HardhatRuntimeEnvironment } from 'hardhat/types' -import { BigNumber } from 'ethers' -import { AggregatorV3Interface } from '@typechain/index' -import { ONE_ADDRESS } from '../../../common/constants' - -export const overrideOracle = async ( - hre: HardhatRuntimeEnvironment, - oracleAddress: string -): Promise => { - const oracle = await hre.ethers.getContractAt( - 'contracts/plugins/mocks/EACAggregatorProxyMock.sol:EACAggregatorProxy', - oracleAddress - ) - const aggregator = await oracle.aggregator() - const accessController = await oracle.accessController() - const initPrice = await oracle.latestRoundData() - const mockOracleFactory = await hre.ethers.getContractFactory('EACAggregatorProxyMock') - const mockOracle = await mockOracleFactory.deploy(aggregator, accessController, initPrice.answer) - const bytecode = await hre.network.provider.send('eth_getCode', [mockOracle.address, 'latest']) - await hre.network.provider.request({ - method: 'hardhat_setCode', - params: [oracleAddress, bytecode], - }) - return hre.ethers.getContractAt('EACAggregatorProxyMock', oracleAddress) -} - -export const pushOraclesForward = async ( - hre: HardhatRuntimeEnvironment, - rTokenAddress: string, - extraAssets: string[] = [] -) => { - console.log(`🔃 Pushing Oracles forward for RToken: ${rTokenAddress}`) - const rToken = await hre.ethers.getContractAt('RTokenP1', rTokenAddress) - const main = await hre.ethers.getContractAt('IMain', await rToken.main()) - const assetRegistry = await hre.ethers.getContractAt( - 'AssetRegistryP1', - await main.assetRegistry() - ) - const registry = await assetRegistry.getRegistry() - - for (const asset of registry.assets) { - await pushOracleForward(hre, asset) - } - - for (const asset of extraAssets) { - await pushOracleForward(hre, asset) - } -} - -export const pushOracleForward = async (hre: HardhatRuntimeEnvironment, asset: string) => { - // Need to handle all oracle cases, ie targetUnitChainlinkFeed, PoolTokens, etc - const updateAnswer = async (chainlinkFeed: AggregatorV3Interface) => { - const initPrice = await chainlinkFeed.latestRoundData() - const oracle = await overrideOracle(hre, chainlinkFeed.address) - await oracle.updateAnswer(initPrice.answer) - - console.log('✅ Feed Updated:', chainlinkFeed.address) - } - - // chainlinkFeed - try { - const assetContract = await hre.ethers.getContractAt('TestIAsset', asset) - const feed = await hre.ethers.getContractAt( - 'AggregatorV3Interface', - await assetContract.chainlinkFeed() - ) - if (feed.address != ONE_ADDRESS) await updateAnswer(feed) - } catch {} - - // targetUnitChainlinkFeed - try { - const assetContractNonFiat = await hre.ethers.getContractAt('NonFiatCollateral', asset) - const feed = await hre.ethers.getContractAt( - 'AggregatorV3Interface', - await assetContractNonFiat.targetUnitChainlinkFeed() - ) - await updateAnswer(feed) - } catch {} - - // targetPerRefChainlinkFeed, uoaPerTargetChainlinkFeed, refPerTokenChainlinkFeed - try { - const assetContractLido = await hre.ethers.getContractAt('L2LidoStakedEthCollateral', asset) - let feed = await hre.ethers.getContractAt( - 'AggregatorV3Interface', - await assetContractLido.targetPerRefChainlinkFeed() - ) - await updateAnswer(feed) - feed = await hre.ethers.getContractAt( - 'AggregatorV3Interface', - await assetContractLido.uoaPerTargetChainlinkFeed() - ) - await updateAnswer(feed) - feed = await hre.ethers.getContractAt( - 'AggregatorV3Interface', - await assetContractLido.refPerTokenChainlinkFeed() - ) - await updateAnswer(feed) - } catch {} - - // targetPerTokChainlinkFeed - try { - const assetContractReth = await hre.ethers.getContractAt('RethCollateral', asset) - const feed = await hre.ethers.getContractAt( - 'AggregatorV3Interface', - await assetContractReth.targetPerTokChainlinkFeed() - ) - await updateAnswer(feed) - } catch {} - - // TODO do better - // Problem: The feeds on PoolTokens are internal immutable. Not in storage nor are there getters. - // Workaround solution: hard-code oracles for FRAX for eUSDFRAXBP; USDC is registered as backup - if (asset == '0x890FAa00C16EAD6AA76F18A1A7fe9C40838F9122') { - const feed = await hre.ethers.getContractAt( - 'AggregatorV3Interface', - networkConfig['1'].chainlinkFeeds.FRAX! - ) - await updateAnswer(feed) - } -} - -export const setOraclePrice = async ( - hre: HardhatRuntimeEnvironment, - asset: string, - value: BigNumber -) => { - const assetContract = await hre.ethers.getContractAt('TestIAsset', asset) - const realChainlinkFeed = await hre.ethers.getContractAt( - 'AggregatorV3Interface', - await assetContract.chainlinkFeed() - ) - const oracle = await overrideOracle(hre, realChainlinkFeed.address) - await oracle.updateAnswer(value) -} diff --git a/tasks/testing/upgrade-checker-utils/trades.ts b/tasks/testing/upgrade-checker-utils/trades.ts deleted file mode 100644 index 9349a8b079..0000000000 --- a/tasks/testing/upgrade-checker-utils/trades.ts +++ /dev/null @@ -1,348 +0,0 @@ -import { MAX_UINT256, QUEUE_START, TradeKind, TradeStatus } from '#/common/constants' -import { bn, fp } from '#/common/numbers' -import { whileImpersonating } from '#/utils/impersonation' -import { networkConfig } from '../../../common/configuration' -import { - advanceToTimestamp, - advanceTime, - getLatestBlockNumber, - getLatestBlockTimestamp, -} from '#/utils/time' -import { DutchTrade } from '@typechain/DutchTrade' -import { GnosisTrade } from '@typechain/GnosisTrade' -import { TestITrading } from '@typechain/TestITrading' -import { BigNumber, ContractTransaction } from 'ethers' -import { LogDescription } from 'ethers/lib/utils' -import { HardhatRuntimeEnvironment } from 'hardhat/types' -import { whales } from './constants' -import { logToken } from './logs' - -export const runBatchTrade = async ( - hre: HardhatRuntimeEnvironment, - trader: TestITrading, - tradeToken: string, - bidExact: boolean -) => { - // NOTE: - // buy & sell are from the perspective of the auction-starter - // placeSellOrders() flips it to be from the perspective of the trader - - const tradeAddr = await trader.trades(tradeToken) - const trade = await hre.ethers.getContractAt('GnosisTrade', tradeAddr) - - // Only works for Batch trades - if ((await trade.KIND()) != TradeKind.BATCH_AUCTION) { - throw new Error(`Invalid Trade Type`) - } - - const buyTokenAddress = await trade.buy() - console.log( - `Running batch trade: sell ${logToken(tradeToken)} for ${logToken(buyTokenAddress)}...` - ) - const endTime = await trade.endTime() - const worstPrice = await trade.worstCasePrice() // trade.buy() per trade.sell() - const auctionId = await trade.auctionId() - const sellAmount = await trade.initBal() - - const sellToken = await hre.ethers.getContractAt('ERC20Mock', await trade.sell()) - const sellDecimals = await sellToken.decimals() - const buytoken = await hre.ethers.getContractAt('ERC20Mock', await buyTokenAddress) - const buyDecimals = await buytoken.decimals() - let buyAmount = bidExact ? sellAmount : sellAmount.mul(worstPrice).div(fp('1')) - if (buyDecimals > sellDecimals) { - buyAmount = buyAmount.mul(bn(10 ** (buyDecimals - sellDecimals))) - } else if (sellDecimals > buyDecimals) { - buyAmount = buyAmount.div(bn(10 ** (sellDecimals - buyDecimals))) - } - buyAmount = buyAmount.add(fp('1').div(bn(10 ** (18 - buyDecimals)))) - - const gnosis = await hre.ethers.getContractAt('EasyAuction', await trade.gnosis()) - const whaleAddr = whales[buyTokenAddress.toLowerCase()] - - // For newly wrapped tokens we need to feed the whale - await getTokens(hre, buyTokenAddress, buyAmount, whaleAddr) - - await whileImpersonating(hre, whaleAddr, async (whale) => { - const sellToken = await hre.ethers.getContractAt('ERC20Mock', buyTokenAddress) - await sellToken.connect(whale).approve(gnosis.address, buyAmount) - - await gnosis - .connect(whale) - .placeSellOrders( - auctionId, - [sellAmount], - [buyAmount], - [QUEUE_START], - hre.ethers.constants.HashZero - ) - }) - - const lastTimestamp = await getLatestBlockTimestamp(hre) - await advanceTime(hre, BigNumber.from(endTime).sub(lastTimestamp).toString()) - await trader.settleTrade(tradeToken) - - console.log(`Settled trade for ${logToken(buyTokenAddress)}.`) -} - -export const runDutchTrade = async ( - hre: HardhatRuntimeEnvironment, - trader: TestITrading, - tradeToken: string -): Promise<[boolean, string]> => { - const router = await (await hre.ethers.getContractFactory('DutchTradeRouter')).deploy() - // NOTE: - // buy & sell are from the perspective of the auction-starter - // bid() flips it to be from the perspective of the trader - - let tradesRemain = false - let newSellToken = '' - - const tradeAddr = await trader.trades(tradeToken) - const trade = await hre.ethers.getContractAt('DutchTrade', tradeAddr) - - // Only works for Dutch trades - if ((await trade.KIND()) != TradeKind.DUTCH_AUCTION) { - throw new Error(`Invalid Trade Type`) - } - - const buyTokenAddress = await trade.buy() - console.log('=========') - console.log( - `Running Dutch Trade: Selling ${logToken(tradeToken)} for ${logToken(buyTokenAddress)}...` - ) - - const endTime = await trade.endTime() - const whaleAddr = whales[buyTokenAddress.toLowerCase()] - - // Bid near 1:1 point, which occurs at the 70% mark - const toAdvance = endBlock - .sub(await getLatestBlockNumber(hre)) - .mul(7) - .div(10) - await advanceBlocks(hre, toAdvance) - const buyAmount = await trade.bidAmount(await getLatestBlockNumber(hre)) - - // Ensure funds available - await getTokens(hre, buyTokenAddress, buyAmount, tester.address) - - const buyToken = await hre.ethers.getContractAt('ERC20Mock', buyTokenAddress) - await buyToken.connect(whaleAddr).approve(router.address, MAX_UINT256) - - // Bid - ;[tradesRemain, newSellToken] = await callAndGetNextTrade( - router.bid(trade.address, await router.signer.getAddress()), - trader - ) - - console.log( - 'Trade State:', - TradeStatus[await trade.status()], - await trade.canSettle(), - await trade.bidder(), - whaleAddr - ) - - if ( - (await trade.canSettle()) || - (await trade.status()) != TradeStatus.CLOSED || - (await trade.bidder()) != router.address - ) { - throw new Error(`Error settling Dutch Trade`) - } - - console.log(`Settled trade for ${logToken(buyTokenAddress)}.`) - - // Return new trade (if exists) - return [tradesRemain, newSellToken] -} - -export const callAndGetNextTrade = async ( - tx: Promise, - trader: TestITrading -): Promise<[boolean, string]> => { - let tradesRemain = false - let newSellToken = '' - - // Process transaction and get next trade - const r = await tx - const resp = await r.wait() - const iface = trader.interface - - for (const event of resp.events!) { - let parsedLog: LogDescription | undefined - try { - parsedLog = iface.parseLog(event) - // eslint-disable-next-line no-empty - } catch {} - - if (parsedLog && parsedLog.name == 'TradeStarted') { - // TODO: Improve this to include proper token details and parsing. - - console.log( - ` - ====== Trade Started: Selling ${logToken(parsedLog.args.sell)} / Buying ${logToken( - parsedLog.args.buy - )} ====== - minBuyAmount: ${parsedLog.args.minBuyAmount} - sellAmount: ${parsedLog.args.sellAmount} - `.trim() - ) - - tradesRemain = true - newSellToken = parsedLog.args.sell - } - } - - return [tradesRemain, newSellToken] -} - -// impersonate the whale to provide the required tokens to recipient -export const getTokens = async ( - hre: HardhatRuntimeEnvironment, - tokenAddress: string, - amount: BigNumber, - recipient: string -) => { - console.log('Acquiring tokens...', tokenAddress) - switch (tokenAddress) { - case '0x60C384e226b120d93f3e0F4C502957b2B9C32B15': // saUSDC - case '0x21fe646D1Ed0733336F2D4d9b2FE67790a6099D9': // saUSDT - await getStaticAToken(hre, tokenAddress, amount, recipient) - break - case '0xf579F9885f1AEa0d3F8bE0F18AfED28c92a43022': // cUSDCVault - case '0x4Be33630F92661afD646081BC29079A38b879aA0': // cUSDTVault - await getCTokenVault(hre, tokenAddress, amount, recipient) - break - default: - await getERC20Tokens(hre, tokenAddress, amount, recipient) - return - } -} - -// get a specific amount of wrapped cTokens -const getCTokenVault = async ( - hre: HardhatRuntimeEnvironment, - tokenAddress: string, - amount: BigNumber, - recipient: string -) => { - const collateral = await hre.ethers.getContractAt('CTokenWrapper', tokenAddress) - const cToken = await hre.ethers.getContractAt('ICToken', await collateral.underlying()) - - await whileImpersonating(hre, whales[cToken.address.toLowerCase()], async (whaleSigner) => { - await cToken.connect(whaleSigner).transfer(recipient, amount) - }) - - await whileImpersonating(hre, recipient, async (recipientSigner) => { - await cToken.connect(recipientSigner).approve(collateral.address, amount) - await collateral.connect(recipientSigner).deposit(amount, recipient) - }) -} - -// get a specific amount of static aTokens -const getStaticAToken = async ( - hre: HardhatRuntimeEnvironment, - tokenAddress: string, - amount: BigNumber, - recipient: string -) => { - const collateral = await hre.ethers.getContractAt('StaticATokenLM', tokenAddress) - const aTokensNeeded = await collateral.staticToDynamicAmount(amount) - const aToken = await hre.ethers.getContractAt( - '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', - await collateral.ATOKEN() - ) - - await whileImpersonating(hre, whales[aToken.address.toLowerCase()], async (whaleSigner) => { - await aToken.connect(whaleSigner).transfer(recipient, aTokensNeeded.mul(101).div(100)) // buffer to ensure enough balance - }) - - await whileImpersonating(hre, recipient, async (recipientSigner) => { - const bal = await aToken.balanceOf(recipientSigner.address) - await aToken.connect(recipientSigner).approve(collateral.address, bal) - await collateral.connect(recipientSigner).deposit(recipient, bal, 0, false) - }) -} - -// get a specific amount of erc20 plain token -const getERC20Tokens = async ( - hre: HardhatRuntimeEnvironment, - tokenAddress: string, - amount: BigNumber, - recipient: string -) => { - const token = await hre.ethers.getContractAt('ERC20Mock', tokenAddress) - - // special-cases for wrappers with 0 supply - const wcUSDCv3 = await hre.ethers.getContractAt( - 'CusdcV3Wrapper', - '0xfBD1a538f5707C0D67a16ca4e3Fc711B80BD931A' - ) - const saEthUSDC = await hre.ethers.getContractAt( - 'IStaticATokenV3LM', - networkConfig['1'].tokens.saEthUSDC! - ) - const saEthPyUSD = await hre.ethers.getContractAt( - 'IStaticATokenV3LM', - networkConfig['1'].tokens.saEthPyUSD! - ) - const stkcvxeUSDFRAXBP = await hre.ethers.getContractAt( - 'ConvexStakingWrapper', - '0x8e33D5aC344f9F2fc1f2670D45194C280d4fBcF1' - ) - - if (tokenAddress == wcUSDCv3.address) { - await whileImpersonating( - hre, - whales[networkConfig['1'].tokens.cUSDCv3!.toLowerCase()], - async (whaleSigner) => { - const cUSDCv3 = await hre.ethers.getContractAt( - 'ERC20Mock', - networkConfig['1'].tokens.cUSDCv3! - ) - await cUSDCv3.connect(whaleSigner).approve(wcUSDCv3.address, 0) - await cUSDCv3.connect(whaleSigner).approve(wcUSDCv3.address, MAX_UINT256) - await wcUSDCv3.connect(whaleSigner).deposit(amount.mul(2)) - const bal = await wcUSDCv3.balanceOf(whaleSigner.address) - await wcUSDCv3.connect(whaleSigner).transfer(recipient, bal) - } - ) - } else if (tokenAddress == saEthUSDC.address) { - await whileImpersonating( - hre, - whales[networkConfig['1'].tokens.USDC!.toLowerCase()], - async (whaleSigner) => { - const USDC = await hre.ethers.getContractAt('ERC20Mock', networkConfig['1'].tokens.USDC!) - await USDC.connect(whaleSigner).approve(saEthUSDC.address, amount.mul(2)) - await saEthUSDC.connect(whaleSigner).deposit(amount.mul(2), whaleSigner.address, 0, true) - await token.connect(whaleSigner).transfer(recipient, amount) // saEthUSDC transfer - } - ) - } else if (tokenAddress == saEthPyUSD.address) { - await whileImpersonating( - hre, - whales[networkConfig['1'].tokens.pyUSD!.toLowerCase()], - async (whaleSigner) => { - const pyUSD = await hre.ethers.getContractAt('ERC20Mock', networkConfig['1'].tokens.pyUSD!) - await pyUSD.connect(whaleSigner).approve(saEthPyUSD.address, amount.mul(2)) - await saEthPyUSD.connect(whaleSigner).deposit(amount.mul(2), whaleSigner.address, 0, true) - await token.connect(whaleSigner).transfer(recipient, amount) // saEthPyUSD transfer - } - ) - } else if (tokenAddress == stkcvxeUSDFRAXBP.address) { - const lpTokenAddr = '0xaeda92e6a3b1028edc139a4ae56ec881f3064d4f' - - await whileImpersonating(hre, whales[lpTokenAddr], async (whaleSigner) => { - const lpToken = await hre.ethers.getContractAt('ERC20Mock', lpTokenAddr) - await lpToken.connect(whaleSigner).approve(stkcvxeUSDFRAXBP.address, amount.mul(2)) - await stkcvxeUSDFRAXBP.connect(whaleSigner).deposit(amount.mul(2), whaleSigner.address) - await token.connect(whaleSigner).transfer(recipient, amount) - }) - } else { - const addr = whales[token.address.toLowerCase()] - if (!addr) throw new Error('missing whale for ' + tokenAddress) - await whileImpersonating(hre, whales[token.address.toLowerCase()], async (whaleSigner) => { - await token.connect(whaleSigner).transfer(recipient, amount) - }) - } -} diff --git a/tasks/testing/upgrade-checker.ts b/tasks/testing/upgrade-checker.ts deleted file mode 100644 index 057f4ee5f7..0000000000 --- a/tasks/testing/upgrade-checker.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { task } from 'hardhat/config' -import { networkConfig } from '../../common/configuration' -import { getChainId } from '../../common/blockchain-utils' -import { whileImpersonating } from '#/utils/impersonation' -import { useEnv } from '#/utils/env' -import { expect } from 'chai' -import { fp } from '#/common/numbers' -import { MAX_UINT256, TradeKind } from '#/common/constants' -import { formatEther, formatUnits } from 'ethers/lib/utils' -import { recollateralize, redeemRTokens } from './upgrade-checker-utils/rtokens' -import { claimRsrRewards } from './upgrade-checker-utils/rewards' -import { whales } from './upgrade-checker-utils/constants' -import { pushOraclesForward } from './upgrade-checker-utils/oracles' -import runChecks3_3_0, { - proposal_3_3_0_step_1, - proposal_3_3_0_step_2, - proposal_3_3_0_step_3, - proposal_3_3_0_step_4, -} from './upgrade-checker-utils/upgrades/3_3_0_plugins' -import { - passProposal, - executeProposal, - proposeUpgrade, - stakeAndDelegateRsr, - moveProposalToActive, - voteProposal, -} from './upgrade-checker-utils/governance' -import { advanceTime, getLatestBlockNumber } from '#/utils/time' - -// run script for eUSD (version 3.3.0) -// npx hardhat upgrade-checker --rtoken 0xA0d69E286B938e21CBf7E51D71F6A4c8918f482F --governor 0x7e880d8bD9c9612D6A9759F96aCD23df4A4650E6 - -/* - This script is currently useful for the upcoming eUSD upgrade. - In order to make this useful for future upgrades and for other rTokens, we will need the following: - - generic minting (5 pts) - - dynamically gather and approve the necessary basket tokens needed to mint - - use ZAPs - - generic reward claiming (5 pts) - - check for where revenue should be allocated - - dynamically run and complete necessary auctions to realize revenue - - generic basket switching (8 pts) - - not sure if possible if there is no backup basket - - 21-34 more points of work to make this more generic -*/ - -interface Params { - rtoken: string - governor: string - proposalId?: string -} - -task('upgrade-checker', 'Runs a proposal and confirms can fully rebalance + redeem + mint') - .addParam('rtoken', 'the address of the RToken being upgraded') - .addParam('governor', 'the address of the OWNER of the RToken being upgraded') - .addOptionalParam('proposalId', 'the ID of the governance proposal', undefined) - .setAction(async (params: Params, hre) => { - const chainId = await getChainId(hre) - - // make sure config exists - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - - // only run locally - if (hre.network.name != 'localhost' && hre.network.name != 'hardhat') { - throw new Error('Only run this on a local fork') - } - - // make sure subgraph is configured - if (params.proposalId && !useEnv('SUBGRAPH_URL')) { - throw new Error('SUBGRAPH_URL required for subgraph queries') - } - - console.log(`Network Block: ${await getLatestBlockNumber(hre)}`) - - await hre.run('propose', { - step: '1', - rtoken: params.rtoken, - governor: params.governor, - }) - - await hre.run('recollateralize', { - rtoken: params.rtoken, - governor: params.governor, - }) - - await hre.run('propose', { - step: '2', - rtoken: params.rtoken, - governor: params.governor, - }) - - await hre.run('recollateralize', { - rtoken: params.rtoken, - governor: params.governor, - }) - - await hre.run('propose', { - step: '3', - rtoken: params.rtoken, - governor: params.governor, - }) - - await hre.run('recollateralize', { - rtoken: params.rtoken, - governor: params.governor, - }) - - await hre.run('propose', { - step: '4', - rtoken: params.rtoken, - governor: params.governor, - }) - - const rToken = await hre.ethers.getContractAt('IRToken', params.rtoken) - const main = await hre.ethers.getContractAt('IMain', await rToken.main()) - const assetRegistry = await hre.ethers.getContractAt( - 'IAssetRegistry', - await main.assetRegistry() - ) - const basketHandler = await hre.ethers.getContractAt( - 'IBasketHandler', - await main.basketHandler() - ) - await assetRegistry.refresh() - if (!((await basketHandler.status()) == 0)) throw new Error('Basket is not SOUND') - if (!(await basketHandler.fullyCollateralized())) { - throw new Error('Basket is not fully collateralized') - } - console.log('Basket is SOUND and fully collateralized!') - }) - -interface ProposeParams { - step: string - rtoken: string - governor: string - proposalId?: string -} - -task('propose', 'propose a gov action') - .addParam('step', 'the step of the proposal') - .addParam('rtoken', 'the address of the RToken being upgraded') - .addParam('governor', 'the address of the OWNER of the RToken being upgraded') - .setAction(async (params: ProposeParams, hre) => { - const stepFunction = (() => { - console.log(`=========================== STEP ${params.step} ===============================`) - if (params.step === '1') { - return proposal_3_3_0_step_1 - } - if (params.step === '2') { - return proposal_3_3_0_step_2 - } - if (params.step === '3') { - return proposal_3_3_0_step_3 - } - if (params.step === '4') { - return proposal_3_3_0_step_4 - } - - throw Error('Invalid step') - })() - - const proposal = await proposeUpgrade(hre, params.rtoken, params.governor, stepFunction) - - await moveProposalToActive(hre, params.rtoken, params.governor, proposal.proposalId) - await voteProposal(hre, params.rtoken, params.governor, proposal.proposalId) - await passProposal(hre, params.rtoken, params.governor, proposal.proposalId) - await executeProposal(hre, params.rtoken, params.governor, proposal.proposalId, proposal) - }) - -task('recollateralize') - .addParam('rtoken', 'the address of the RToken being upgraded') - .addParam('governor', 'the address of the OWNER of the RToken being upgraded') - .setAction(async (params: Params, hre) => { - const [tester] = await hre.ethers.getSigners() - const rToken = await hre.ethers.getContractAt('RTokenP1', params.rtoken) - - // 2. Bring back to fully collateralized - const main = await hre.ethers.getContractAt('IMain', await rToken.main()) - const basketHandler = await hre.ethers.getContractAt( - 'BasketHandlerP1', - await main.basketHandler() - ) - const backingManager = await hre.ethers.getContractAt( - 'BackingManagerP1', - await main.backingManager() - ) - // const broker = await hre.ethers.getContractAt('BrokerP1', await main.broker()) - const stRSR = await hre.ethers.getContractAt('StRSRP1Votes', await main.stRSR()) - - /* - recollateralize - */ - await advanceTime(hre, (await backingManager.tradingDelay()) + 1) - await pushOraclesForward(hre, params.rtoken, []) - await recollateralize(hre, rToken.address, TradeKind.DUTCH_AUCTION).catch((e: Error) => { - if (e.message.includes('already collateralized')) { - console.log('Already Collateralized!') - - return - } - - throw e - }) - if (!(await basketHandler.fullyCollateralized())) throw new Error('Failed to recollateralize') - - // Give `tester` RTokens from a whale - const redeemAmt = fp('1e3') - await whileImpersonating(hre, whales[params.rtoken.toLowerCase()], async (whaleSigner) => { - await rToken.connect(whaleSigner).transfer(tester.address, redeemAmt) - }) - if (!(await rToken.balanceOf(tester.address)).gte(redeemAmt)) throw new Error('missing R') - - /* - redeem - */ - await redeemRTokens(hre, tester, params.rtoken, redeemAmt) - - // 3. Run the 3.0.0 checks - await runChecks3_3_0(hre, params.rtoken, params.governor) - - /* - mint - */ - - const issueAmt = redeemAmt.div(2) - console.log(`\nIssuing ${formatEther(issueAmt)} RTokens...`) - const [erc20s] = await basketHandler.quote(fp('1'), 0) - for (const e of erc20s) { - const erc20 = await hre.ethers.getContractAt( - '@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', - e - ) - await erc20.connect(tester).approve(rToken.address, MAX_UINT256) // max approval - } - const preBal = await rToken.balanceOf(tester.address) - await rToken.connect(tester).issue(issueAmt) - - const postIssueBal = await rToken.balanceOf(tester.address) - if (!postIssueBal.eq(preBal.add(issueAmt))) { - throw new Error( - `Did not issue the correct amount of RTokens. wanted: ${formatUnits( - preBal.add(issueAmt), - 'mwei' - )} balance: ${formatUnits(postIssueBal, 'mwei')}` - ) - } - - console.log('Successfully minted RTokens') - - /* - claim rewards - */ - await claimRsrRewards(hre, params.rtoken) - - /* - staking/unstaking - */ - - // get RSR - const stakeAmount = fp('4e6') - const rsr = await hre.ethers.getContractAt('StRSRP1Votes', await main.rsr()) - await whileImpersonating( - hre, - whales[networkConfig['1'].tokens.RSR!.toLowerCase()], - async (rsrSigner) => { - await rsr.connect(rsrSigner).transfer(tester.address, stakeAmount) - } - ) - - const balPrevRSR = await rsr.balanceOf(stRSR.address) - const balPrevStRSR = await stRSR.balanceOf(tester.address) - const testerBal = await rsr.balanceOf(tester.address) - - await stakeAndDelegateRsr(hre, rToken.address, tester.address) - - expect(await rsr.balanceOf(stRSR.address)).to.equal(balPrevRSR.add(testerBal)) - expect(await stRSR.balanceOf(tester.address)).to.be.gt(balPrevStRSR) - }) - -task('eusd-q1-2024-test', 'Test deployed eUSD Proposals').setAction(async (_, hre) => { - console.log(`Network Block: ${await getLatestBlockNumber(hre)}`) - - const RTokenAddress = '0xa0d69e286b938e21cbf7e51d71f6a4c8918f482f' - const RTokenGovernor = '0x7e880d8bD9c9612D6A9759F96aCD23df4A4650E6' - - const ProposalIdOne = - '114052081659629247617665835769035094910371266951213483500173240902265689564540' - const ProposalIdTwo = - '84013999114211651083886802889501217056607481369823717462033802424606122383108' - - // Make sure both proposals are active. - await moveProposalToActive(hre, RTokenAddress, RTokenGovernor, ProposalIdOne) - await moveProposalToActive(hre, RTokenAddress, RTokenGovernor, ProposalIdTwo) - - await voteProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdOne) - await voteProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdTwo) - - await passProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdOne) - await passProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdTwo) - - await executeProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdOne) - await hre.run('recollateralize', { - rtoken: RTokenAddress, - governor: RTokenGovernor, - }) - - await executeProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdTwo) - await hre.run('recollateralize', { - rtoken: RTokenAddress, - governor: RTokenGovernor, - }) -}) - -task('hyusd-q1-2024-test', 'Test deployed hyUSD Proposals').setAction(async (_, hre) => { - console.log(`Network Block: ${await getLatestBlockNumber(hre)}`) - - const RTokenAddress = '0xaCdf0DBA4B9839b96221a8487e9ca660a48212be' - const RTokenGovernor = '0x22d7937438b4bBf02f6cA55E3831ABB94Bd0b6f1' - - const ProposalIdOne = - '12128108731947079972460039600592322347543776217408895065380983128537007111991' - const ProposalIdTwo = - '67602359440860478788595002306085984888572052896929999758442845286427134600253' - - // Make sure both proposals are active. - await moveProposalToActive(hre, RTokenAddress, RTokenGovernor, ProposalIdOne) - await moveProposalToActive(hre, RTokenAddress, RTokenGovernor, ProposalIdTwo) - - await voteProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdOne) - await voteProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdTwo) - - await passProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdOne) - await passProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdTwo) - - await executeProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdOne) - await executeProposal(hre, RTokenAddress, RTokenGovernor, ProposalIdTwo) - - await hre.run('recollateralize', { - rtoken: RTokenAddress, - governor: RTokenGovernor, - }) -}) diff --git a/tasks/validation/proposal-validator.ts b/tasks/validation/proposal-validator.ts index 908ac9a8b0..56ec7452aa 100644 --- a/tasks/validation/proposal-validator.ts +++ b/tasks/validation/proposal-validator.ts @@ -21,7 +21,6 @@ import { } from './utils/governance' import { advanceTime, getLatestBlockNumber } from '#/utils/time' import { HardhatRuntimeEnvironment } from 'hardhat/types' -import { resetFork } from '#/utils/chain' import fs from 'fs' import { BigNumber } from 'ethers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' @@ -30,7 +29,7 @@ import { RTokenP1 } from '@typechain/RTokenP1' import { StRSRP1Votes } from '@typechain/StRSRP1Votes' import { IMain } from '@typechain/IMain' import { Whales, getWhalesFile } from '#/scripts/whalesConfig' -import { proposal_3_4_0_step_1, proposal_3_4_0_step_2 } from './proposals/3_4_0' +import { proposal_3_4_0_step_2 } from './proposals/3_4_0' import { validateSubgraphURL, Network } from '#/utils/fork' interface Params { @@ -362,19 +361,31 @@ const runCheck_mint = async ( console.log('Successfully minted RTokens') } -task('print-proposal') +task('save-proposal-pt-2') .addParam('rtoken', 'the address of the RToken being upgraded') .addParam('gov', 'the address of the OWNER of the RToken being upgraded') .addParam('time', 'the address of the timelock') .setAction(async (params, hre) => { - const proposal = await proposal_3_4_0_step_2(hre, params.rtoken, params.gov, params.time) + const chainId = await getChainId(hre) - console.log(`\nGenerating and proposing proposal...`) - const [tester] = await hre.ethers.getSigners() + // make sure config exists + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } - await hre.run('give-rsr', { address: tester.address }) - await stakeAndDelegateRsr(hre, params.rtoken, tester.address) + const spellAddr = + chainId == '1' + ? '0xb1df3a104d73ff86f9aaab60b491a5c44b090391' + : '0x1744c9933feb8e76563fce63d5c95a4e7f967c2a' + const proposal = await proposal_3_4_0_step_2( + hre, + params.rtoken, + params.gov, + params.time, + spellAddr + ) + console.log(`\nGenerating and hashing proposal...`) const governor = await hre.ethers.getContractAt('Governance', params.gov) const call = await governor.populateTransaction.propose( @@ -386,21 +397,23 @@ task('print-proposal') console.log(`Proposal Transaction:\n`, call.data) - const r = await governor.propose( + const proposalId = await governor.hashProposal( proposal.targets, proposal.values, proposal.calldatas, - proposal.description + hre.ethers.utils.keccak256(hre.ethers.utils.toUtf8Bytes(proposal.description)) ) - const resp = await r.wait() - console.log('\nSuccessfully proposed!') - console.log(`Proposal ID: ${resp.events![0].args!.proposalId}`) - - proposal.proposalId = resp.events![0].args!.proposalId.toString() + console.log(`Proposal ID: ${proposalId}`) + proposal.proposalId = proposalId.toString() + proposal.governor = params.gov + proposal.timelock = params.time + proposal.rtoken = params.rtoken fs.writeFileSync( `./tasks/validation/proposals/proposal-${proposal.proposalId}.json`, JSON.stringify(proposal, null, 2) ) + + console.log("Saved to proposals folder. Don't forget to run `proposal-validator`!") }) diff --git a/tasks/validation/utils/governance.ts b/tasks/validation/utils/governance.ts index 5b906d5206..226fa9fb92 100644 --- a/tasks/validation/utils/governance.ts +++ b/tasks/validation/utils/governance.ts @@ -35,15 +35,14 @@ export const moveProposalToActive = async ( const version = await rToken.version() if (version == '3.0.0' || version == '3.0.1') await advanceBlocks(hre, votingDelay.add(2)) else await advanceTime(hre, votingDelay.add(2).toNumber()) - } else { - if (propState == ProposalState.Active) { - console.log(`Proposal is already ${ProposalState[ProposalState.Active]}... skipping step.`) - } else { - throw Error(`Proposal should be ${ProposalState[ProposalState.Pending]} at this step.`) - } + } else if (propState == ProposalState.Active) { + console.log(`Proposal is already ${ProposalState[ProposalState.Active]}... skipping step.`) } - await validatePropState(await governor.state(proposalId), ProposalState.Active) + const state = await governor.state(proposalId) + if (![ProposalState.Active, ProposalState.Succeeded, ProposalState.Queued].includes(state)) { + throw new Error(`Proposal is in unexpected state ${ProposalState[propState]}`) + } } export const voteProposal = async ( @@ -96,7 +95,10 @@ export const voteProposal = async ( } } - await validatePropState(await governor.state(proposalId), ProposalState.Active) + const state = await governor.state(proposalId) + if (![ProposalState.Active, ProposalState.Succeeded, ProposalState.Queued].includes(state)) { + throw new Error(`Proposal is in unexpected state ${ProposalState[propState]}`) + } } export const passProposal = async ( @@ -115,7 +117,10 @@ export const passProposal = async ( await advanceBlocks(hre, votingPeriod.add(1)) } - await validatePropState(await governor.state(proposalId), ProposalState.Succeeded) + const state = await governor.state(proposalId) + if (![ProposalState.Succeeded, ProposalState.Queued].includes(state)) { + throw new Error(`Proposal is in unexpected state ${ProposalState[propState]}`) + } } export const executeProposal = async ( @@ -262,35 +267,48 @@ export const proposeUpgrade = async ( const main = await hre.ethers.getContractAt('IMain', await rToken.main()) const stRSR = await hre.ethers.getContractAt('StRSRP1Votes', await main.stRSR()) const amount = (await stRSR.getStakeRSR()).div(100) // 1% increase in staked RSR - - // Stake and delegate - await hre.run('give-rsr', { address: tester.address, amount: amount.toString() }) - await stakeAndDelegateRsr(hre, rTokenAddress, tester.address) - const governor = await hre.ethers.getContractAt('Governance', governorAddress) - const call = await governor.populateTransaction.propose( + let proposalId = await governor.hashProposal( proposal.targets, proposal.values, proposal.calldatas, - proposal.description + await hre.ethers.utils.keccak256(hre.ethers.utils.toUtf8Bytes(proposal.description)) ) - console.log(`Proposal Transaction:\n`, call.data) + // Only propose if not already proposed + if ((await governor.proposalSnapshot(proposalId)).eq(0)) { + await hre.run('give-rsr', { address: tester.address, amount: amount.toString() }) + await stakeAndDelegateRsr(hre, rTokenAddress, tester.address) - const r = await governor.propose( - proposal.targets, - proposal.values, - proposal.calldatas, - proposal.description - ) - const resp = await r.wait() + const call = await governor.populateTransaction.propose( + proposal.targets, + proposal.values, + proposal.calldatas, + proposal.description + ) + + console.log(`Proposal Transaction:\n`, call.data) + + const r = await governor.propose( + proposal.targets, + proposal.values, + proposal.calldatas, + proposal.description + ) + const resp = await r.wait() + proposalId = bn(resp.events![0].args!.proposalId) + + await validatePropState(await governor.state(proposalId), ProposalState.Pending) + console.log('\nSuccessfully proposed!') + } else { + console.log('\nAlready proposed!') + } - console.log('\nSuccessfully proposed!') - console.log(`Proposal ID: ${resp.events![0].args!.proposalId}`) + console.log(`Proposal ID: ${proposalId}`) return { ...proposal, - proposalId: resp.events![0].args!.proposalId as string, + proposalId: proposalId.toString(), } } diff --git a/tasks/validation/utils/rtokens.ts b/tasks/validation/utils/rtokens.ts index 933eb46ade..1d0417e0d1 100644 --- a/tasks/validation/utils/rtokens.ts +++ b/tasks/validation/utils/rtokens.ts @@ -53,7 +53,11 @@ export const redeemRTokens = async ( await assetRegistry.refresh() const basketsNeeded = await rToken.basketsNeeded() const totalSupply = await rToken.totalSupply() - const redeemQuote = await basketHandler.quote(redeemAmount.mul(basketsNeeded).div(totalSupply), 0) + const redeemQuote = await basketHandler.quote( + redeemAmount.mul(basketsNeeded).div(totalSupply), + false, + 0 + ) const expectedTokens = redeemQuote.erc20s const expectedBalances: Balances = {} let log = '' diff --git a/tasks/validation/utils/trades.ts b/tasks/validation/utils/trades.ts index b06d1cc0ea..c37bf03284 100644 --- a/tasks/validation/utils/trades.ts +++ b/tasks/validation/utils/trades.ts @@ -1,5 +1,5 @@ import { MAX_UINT256, QUEUE_START, TradeKind, TradeStatus } from '#/common/constants' -import { bn, fp } from '#/common/numbers' +import { bn, fp, pow10 } from '#/common/numbers' import { whileImpersonating } from '#/utils/impersonation' import { networkConfig } from '../../../common/configuration' import { advanceTime, getLatestBlockTimestamp } from '#/utils/time' @@ -38,7 +38,7 @@ export const runBatchTrade = async ( `Running batch trade: sell ${logToken(tradeToken)} for ${logToken(buyTokenAddress)}...` ) const endTime = await trade.endTime() - const worstPrice = await trade.worstCasePrice() // trade.buy() per trade.sell() + const worstPrice = await trade.worstCasePrice() // trade.buy() per trade.sell(), qTok const auctionId = await trade.auctionId() const sellAmount = await trade.initBal() @@ -46,13 +46,18 @@ export const runBatchTrade = async ( const sellDecimals = await sellToken.decimals() const buytoken = await hre.ethers.getContractAt('ERC20Mock', await buyTokenAddress) const buyDecimals = await buytoken.decimals() - let buyAmount = bidExact ? sellAmount : sellAmount.mul(worstPrice).div(fp('1')) + let buyAmount = bidExact + ? sellAmount + : sellAmount + .mul(worstPrice) + .mul(pow10(buyDecimals - sellDecimals)) + .div(fp('1')) if (buyDecimals > sellDecimals) { - buyAmount = buyAmount.mul(bn(10 ** (buyDecimals - sellDecimals))) + buyAmount = buyAmount.mul(pow10(buyDecimals - sellDecimals)) } else if (sellDecimals > buyDecimals) { - buyAmount = buyAmount.div(bn(10 ** (sellDecimals - buyDecimals))) + buyAmount = buyAmount.div(pow10(sellDecimals - buyDecimals)) } - buyAmount = buyAmount.add(fp('1').div(bn(10 ** (18 - buyDecimals)))) + buyAmount = buyAmount.add(fp('1').div(pow10(18 - buyDecimals))) const gnosis = await hre.ethers.getContractAt('EasyAuction', await trade.gnosis()) const whaleAddr = whales[buyTokenAddress.toLowerCase()] diff --git a/tasks/validation/whales/whales_1.json b/tasks/validation/whales/whales_1.json index 537bc44460..2c05e7cdd9 100644 --- a/tasks/validation/whales/whales_1.json +++ b/tasks/validation/whales/whales_1.json @@ -50,7 +50,7 @@ "0xc3d688b66703497daa19211eedff47f25384cdc3": "0x7f714b13249BeD8fdE2ef3FBDfB18Ed525544B03", "0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3": "0x677FD4Ed8aE623f2f625DEB2D64F2070E46cA1A1", "0xa663b02cf0a4b149d2ad41910cb81e23e1c41c32": "0x6A7efa964Cf6D9Ab3BC3c47eBdDB853A8853C502", - "0x83f20f44975d03b1b09e64809b757c47f942beea": "0xDdE0d6e90bfB74f1dC8ea070cFd0c0180C03Ad16", + "0x83f20f44975d03b1b09e64809b757c47f942beea": "0x4aa42145Aa6Ebf72e164C9bBC74fbD3788045016", "0xbe9895146f7af43049ca1c1ae358b0541ea49704": "0xED1F7bb04D2BA2b6EbE087026F03C96Ea2c357A8", "0xaf5191b0de278c7286d6c7cc6ab6bb8a73ba2cd6": "0x65bb797c2B9830d891D87288F029ed8dACc19705", "0xdf0770df86a8034b3efef0a1bb3c889b8332ff56": "0xB0D502E938ed5f4df2E681fE6E419ff29631d62b", @@ -129,7 +129,7 @@ "0xc3d688b66703497daa19211eedff47f25384cdc3": "2024-05-02T02:11:46.235Z", "0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3": "2024-05-02T02:11:46.579Z", "0xa663b02cf0a4b149d2ad41910cb81e23e1c41c32": "2024-05-02T02:11:46.677Z", - "0x83f20f44975d03b1b09e64809b757c47f942beea": "2024-05-02T02:11:46.802Z", + "0x83f20f44975d03b1b09e64809b757c47f942beea": "2024-07-16T02:11:46.802Z", "0xbe9895146f7af43049ca1c1ae358b0541ea49704": "2024-05-02T02:11:47.091Z", "0xaf5191b0de278c7286d6c7cc6ab6bb8a73ba2cd6": "2024-05-02T02:11:47.333Z", "0xdf0770df86a8034b3efef0a1bb3c889b8332ff56": "2024-05-02T02:11:47.488Z", diff --git a/tasks/validation/whales/whales_31337.json b/tasks/validation/whales/whales_31337.json index 808ba0a19b..65eb040129 100644 --- a/tasks/validation/whales/whales_31337.json +++ b/tasks/validation/whales/whales_31337.json @@ -137,4 +137,4 @@ "0x0d86883faf4ffd7aeb116390af37746f45b6f378": "2024-05-01T16:12:40.659Z", "0x78da5799cf427fee11e9996982f4150ece7a99a7": "2024-05-01T16:12:40.776Z" } -} \ No newline at end of file +} diff --git a/test/Broker.test.ts b/test/Broker.test.ts index 9ebcd98888..58d0ec73db 100644 --- a/test/Broker.test.ts +++ b/test/Broker.test.ts @@ -56,7 +56,7 @@ import { getLatestBlockTimestamp, setNextBlockTimestamp, } from './utils/time' -import { ITradeRequest, disableBatchTrade, disableDutchTrade } from './utils/trades' +import { ITradeRequest, disableBatchTrade, disableDutchTrade, getTrade } from './utils/trades' import { useEnv } from '#/utils/env' import { parseUnits } from 'ethers/lib/utils' @@ -1102,6 +1102,40 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { expect(await token0.balanceOf(backingManager.address)).to.equal(amount.add(newFunds)) }) + it('Should downsize trades above uint96 - sell side', async () => { + const tradeRequest: ITradeRequest = { + sell: collateral0.address, + buy: collateral1.address, + sellAmount: MAX_UINT96.add(1), + minBuyAmount: amount, + } + + // Should open trade JUST on MAX_UINT96 approval, not MAX_UINT96 + 1 + await whileImpersonating(backingManager.address, async (bmSigner) => { + await token0.mint(backingManager.address, MAX_UINT96) + await token0.connect(bmSigner).approve(broker.address, MAX_UINT96) + await broker.connect(bmSigner).openTrade(TradeKind.BATCH_AUCTION, tradeRequest, prices) + // should not revert + }) + }) + + it('Should downsize trades above uint96 - buy side', async () => { + const tradeRequest: ITradeRequest = { + sell: collateral0.address, + buy: collateral1.address, + sellAmount: amount, + minBuyAmount: MAX_UINT96.add(1), + } + + // Should open trade JUST on amount - 1 approval, not amount + await whileImpersonating(backingManager.address, async (bmSigner) => { + await token0.mint(backingManager.address, amount.sub(1)) + await token0.connect(bmSigner).approve(broker.address, amount.sub(1)) + await broker.connect(bmSigner).openTrade(TradeKind.BATCH_AUCTION, tradeRequest, prices) + // should not revert + }) + }) + // There is no test here for the reportViolation case; that is in Revenues.test.ts }) diff --git a/test/Facade.test.ts b/test/Facade.test.ts index 72b8c531c1..b85f1ed79c 100644 --- a/test/Facade.test.ts +++ b/test/Facade.test.ts @@ -284,7 +284,7 @@ describe('Facade + FacadeMonitor contracts', () => { }) it('Should return maxIssuableByAmounts correctly', async () => { - const [erc20Addrs] = await basketHandler.quote(fp('1'), 0) + const [erc20Addrs] = await basketHandler.quote(fp('1'), false, 0) const erc20s = await Promise.all(erc20Addrs.map((a) => ethers.getContractAt('ERC20Mock', a))) const addr1Amounts = await Promise.all(erc20s.map((e) => e.balanceOf(addr1.address))) const addr2Amounts = await Promise.all(erc20s.map((e) => e.balanceOf(addr2.address))) diff --git a/test/Main.test.ts b/test/Main.test.ts index 0ae0ae4cdb..9718d0b734 100644 --- a/test/Main.test.ts +++ b/test/Main.test.ts @@ -89,6 +89,11 @@ const describeP1 = IMPLEMENTATION == Implementation.P1 ? describe : describe.ski const describeGas = IMPLEMENTATION == Implementation.P1 && useEnv('REPORT_GAS') ? describe.only : describe.skip +const oldBHInterface = [ + 'function quote(uint192,uint8) view returns (address[] erc20s,uint256[] quantities)', + 'function price() view returns (uint192 low,uint192 high)', +] + describe(`MainP${IMPLEMENTATION} contract`, () => { let owner: SignerWithAddress let addr1: SignerWithAddress @@ -380,7 +385,12 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { // Attempt to reinitialize - Basket Handler await expect( - basketHandler.init(main.address, config.warmupPeriod, config.reweightable) + basketHandler.init( + main.address, + config.warmupPeriod, + config.reweightable, + config.enableIssuancePremium + ) ).to.be.revertedWith('Initializable: contract is already initialized') // Attempt to reinitialize - Distributor @@ -963,6 +973,25 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { ).to.be.revertedWith('invalid warmupPeriod') }) + it('Should allow to update enableIssuancePremium if OWNER', async () => { + // Check existing value + expect(await basketHandler.enableIssuancePremium()).to.equal(true) + + // If not owner cannot update + await expect(basketHandler.connect(other).setIssuancePremiumEnabled(false)).to.be.reverted + + // Check value did not change + expect(await basketHandler.enableIssuancePremium()).to.equal(true) + + // Update with owner + await expect(basketHandler.connect(owner).setIssuancePremiumEnabled(false)) + .to.emit(basketHandler, 'EnableIssuancePremiumSet') + .withArgs(true, false) + + // Check value was updated + expect(await basketHandler.enableIssuancePremium()).to.equal(false) + }) + it('Should allow to update tradingDelay if OWNER and perform validations', async () => { const newValue: BigNumber = bn('360') @@ -1232,7 +1261,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { await basketHandler.setPrimeBasket(erc20s, targetAmts) await basketHandler.refreshBasket() expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) - const [quoteERC20s, tokAmts] = await basketHandler.quote(fp('1'), 0) + const [quoteERC20s, tokAmts] = await basketHandler.quote(fp('1'), false, 0) expect(quoteERC20s.length).to.equal(128) expect(tokAmts.length).to.equal(128) @@ -1708,16 +1737,18 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { const newBasketHandler = async (): Promise => { if (IMPLEMENTATION == Implementation.P0) { const BasketHandlerFactory = await ethers.getContractFactory('BasketHandlerP0') - return ((await BasketHandlerFactory.deploy()) as unknown) + const bh = await BasketHandlerFactory.deploy() + return await ethers.getContractAt('TestIBasketHandler', bh.address) } else if (IMPLEMENTATION == Implementation.P1) { const basketLib = await (await ethers.getContractFactory('BasketLibP1')).deploy() const BasketHandlerFactory = await ethers.getContractFactory('BasketHandlerP1', { libraries: { BasketLibP1: basketLib.address }, }) - return await upgrades.deployProxy(BasketHandlerFactory, [], { + const bh = await upgrades.deployProxy(BasketHandlerFactory, [], { kind: 'uups', unsafeAllow: ['external-library-linking'], // BasketLibP1 }) + return await ethers.getContractAt('TestIBasketHandler', bh.address) } else { throw new Error('PROTO_IMPL must be set to either `0` or `1`') } @@ -1725,7 +1756,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { beforeEach(async () => { indexBH = await newBasketHandler() - await indexBH.init(main.address, config.warmupPeriod, true) + await indexBH.init(main.address, config.warmupPeriod, true, config.enableIssuancePremium) eurToken = await (await ethers.getContractFactory('ERC20Mock')).deploy('EURO Token', 'EUR') const FiatCollateralFactory: ContractFactory = await ethers.getContractFactory( @@ -1762,10 +1793,10 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { it('Should not allow to set prime Basket with invalid length', async () => { await expect( basketHandler.connect(owner).setPrimeBasket([token0.address], []) - ).to.be.revertedWith('len mismatch') + ).to.be.revertedWith('invalid lengths') await expect( basketHandler.connect(owner).forceSetPrimeBasket([token0.address], []) - ).to.be.revertedWith('len mismatch') + ).to.be.revertedWith('invalid lengths') }) it('Should not allow to set prime Basket with non-collateral tokens', async () => { @@ -1830,10 +1861,10 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { it('Should not allow to set prime Basket with an empty basket', async () => { await expect(basketHandler.connect(owner).setPrimeBasket([], [])).to.be.revertedWith( - 'empty basket' + 'invalid lengths' ) await expect(basketHandler.connect(owner).forceSetPrimeBasket([], [])).to.be.revertedWith( - 'empty basket' + 'invalid lengths' ) }) @@ -1847,13 +1878,13 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { // for non-reweightable baskets, also try setting a zero amount as the *original* basket const newBH = await newBasketHandler() - await newBH.init(main.address, config.warmupPeriod, false) + await newBH.init(main.address, config.warmupPeriod, false, config.enableIssuancePremium) await expect(newBH.connect(owner).setPrimeBasket([token0.address], [0])).to.be.revertedWith( - 'invalid target amount; must be nonzero' + 'invalid target amount' ) await expect( newBH.connect(owner).forceSetPrimeBasket([token0.address], [0]) - ).to.be.revertedWith('invalid target amount; must be nonzero') + ).to.be.revertedWith('invalid target amount') }) it('Should be able to set exactly same basket', async () => { @@ -2046,6 +2077,22 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { ) ).to.be.revertedWith('new target weights') }) + + it('Should retain backwards-compatible quote() -- FLOOR', async () => { + const bh = new ethers.Contract(basketHandler.address, oldBHInterface, owner) + const quote = await basketHandler.quote(fp('1'), false, RoundingMode.FLOOR) + const quote2 = await bh.quote(fp('1'), RoundingMode.FLOOR) + expectEqualArrays(quote.erc20s, quote2.erc20s) + expectEqualArrays(quote.quantities, quote2.quantities) + }) + + it('Should retain backwards-compatible quote() -- CEIL', async () => { + const bh = new ethers.Contract(basketHandler.address, oldBHInterface, owner) + const quote = await basketHandler.quote(fp('1'), true, RoundingMode.CEIL) + const quote2 = await bh.quote(fp('1'), RoundingMode.CEIL) + expectEqualArrays(quote.erc20s, quote2.erc20s) + expectEqualArrays(quote.quantities, quote2.quantities) + }) }) context('Index BasketHandler', () => { @@ -2065,10 +2112,10 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { it('Should not allow to set prime Basket with invalid length', async () => { await expect( indexBH.connect(owner).setPrimeBasket([token0.address], []) - ).to.be.revertedWith('len mismatch') + ).to.be.revertedWith('invalid lengths') await expect( indexBH.connect(owner).forceSetPrimeBasket([token0.address], []) - ).to.be.revertedWith('len mismatch') + ).to.be.revertedWith('invalid lengths') }) it('Should not allow to set prime Basket with non-collateral tokens', async () => { @@ -2115,28 +2162,28 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { // not possible on non-fresh basketHandler await expect( indexBH.connect(owner).setPrimeBasket([token0.address], [MAX_TARGET_AMT.add(1)]) - ).to.be.revertedWith('invalid target amount; too large') + ).to.be.revertedWith('invalid target amount') await expect( indexBH.connect(owner).forceSetPrimeBasket([token0.address], [MAX_TARGET_AMT.add(1)]) - ).to.be.revertedWith('invalid target amount; too large') + ).to.be.revertedWith('invalid target amount') }) it('Should not allow to set prime Basket with an empty basket', async () => { await expect(indexBH.connect(owner).setPrimeBasket([], [])).to.be.revertedWith( - 'empty basket' + 'invalid lengths' ) await expect(indexBH.connect(owner).forceSetPrimeBasket([], [])).to.be.revertedWith( - 'empty basket' + 'invalid lengths' ) }) it('Should not allow to set prime Basket with a zero amount', async () => { await expect( indexBH.connect(owner).setPrimeBasket([token0.address], [0]) - ).to.be.revertedWith('invalid target amount; must be nonzero') + ).to.be.revertedWith('invalid target amount') await expect( indexBH.connect(owner).forceSetPrimeBasket([token0.address], [0]) - ).to.be.revertedWith('invalid target amount; must be nonzero') + ).to.be.revertedWith('invalid target amount') }) it('Should be able to set exactly same basket', async () => { @@ -2210,102 +2257,20 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { ).to.be.revertedWith('invalid collateral') }) - it('Should normalize by price for index RTokens', async () => { - // Throughout this test the $ value of the RToken should remain - - // Set initial basket - await indexBH.connect(owner).setPrimeBasket([token0.address], [fp('1')]) - await indexBH.connect(owner).refreshBasket() - let [erc20s, tokAmts] = await indexBH.quote(fp('1'), 0) - expect(erc20s.length).to.equal(1) - expect(tokAmts[0]).to.equal(fp('1')) - - // Add EURO into the basket as a pure addition, changing price to $0.80 in USD and $0.20 in EURO - await indexBH - .connect(owner) - .setPrimeBasket([token0.address, eurToken.address], [fp('1'), fp('0.25')]) - await indexBH.connect(owner).refreshBasket() - ;[erc20s, tokAmts] = await indexBH.quote(fp('1'), 0) - expect(erc20s.length).to.equal(2) - expect(erc20s[0]).to.equal(token0.address) - expect(erc20s[1]).to.equal(eurToken.address) - expect(tokAmts[0]).to.equal(fp('0.8')) - expect(tokAmts[1]).to.equal(fp('0.2')) - - // Remove USD from the basket entirely, changing price to $1 in EURO - await indexBH.connect(owner).setPrimeBasket([eurToken.address], [fp('1000')]) - await indexBH.connect(owner).refreshBasket() - ;[erc20s, tokAmts] = await indexBH.quote(fp('1'), 0) - expect(erc20s.length).to.equal(1) - expect(erc20s[0]).to.equal(eurToken.address) - expect(tokAmts[0]).to.equal(fp('1')) // still $1! - - // No change by simply resizing the basket - await indexBH.connect(owner).setPrimeBasket([eurToken.address], [fp('0.000001')]) - await indexBH.connect(owner).refreshBasket() - ;[erc20s, tokAmts] = await indexBH.quote(fp('1'), 0) - expect(erc20s.length).to.equal(1) - expect(erc20s[0]).to.equal(eurToken.address) - expect(tokAmts[0]).to.equal(fp('1')) // still $1! - - // Not refreshing the basket in between should still allow a consecutive setPrimeBasket - await indexBH.connect(owner).setPrimeBasket([token0.address], [fp('1')]) - ;[erc20s, tokAmts] = await indexBH.quote(fp('1'), 0) - expect(erc20s.length).to.equal(1) - expect(erc20s[0]).to.equal(eurToken.address) // not token0 yet - expect(tokAmts[0]).to.equal(fp('1')) - await indexBH - .connect(owner) - .setPrimeBasket([token0.address, eurToken.address], [fp('0.25'), fp('0.25')]) - await indexBH.connect(owner).refreshBasket() - - // $0.50 USD / $0.50 EURO by the end - ;[erc20s, tokAmts] = await indexBH.quote(fp('1'), 0) - expect(erc20s.length).to.equal(2) - expect(erc20s[0]).to.equal(token0.address) - expect(erc20s[1]).to.equal(eurToken.address) - expect(tokAmts[0]).to.equal(fp('0.5')) - expect(tokAmts[1]).to.equal(fp('0.5')) - }) - - it('Should not normalize by price when the current basket is unpriced', async () => { - await indexBH.connect(owner).setPrimeBasket([token0.address], [fp('1')]) - await indexBH.connect(owner).refreshBasket() - - // Set Token0 to unpriced - stale oracle - await advanceTime(DECAY_DELAY.add(PRICE_TIMEOUT).toString()) - await expectUnpriced(collateral0.address) - - // Attempt to add EURO, basket is not SOUND - await expect( - indexBH - .connect(owner) - .setPrimeBasket([token0.address, eurToken.address], [fp('1'), fp('0.25')]) - ).to.be.revertedWith('unsound basket') - }) - - it('Should not normalize by price when the current basket is unsound', async () => { - await indexBH.connect(owner).setPrimeBasket([token0.address], [fp('1')]) - await indexBH.connect(owner).refreshBasket() - await setOraclePrice(collateral0.address, fp('0.5')) - await assetRegistry.refresh() - expect(await collateral0.status()).to.equal(CollateralStatus.IFFY) - expect(await collateral1.status()).to.equal(CollateralStatus.SOUND) - await expect( - indexBH.connect(owner).setPrimeBasket([token1.address], [fp('1')]) - ).to.be.revertedWith('unsound basket') + it('Should retain backwards-compatible quote() -- FLOOR', async () => { + const bh = new ethers.Contract(indexBH.address, oldBHInterface, owner) + const quote = await indexBH.quote(fp('1'), false, RoundingMode.FLOOR) + const quote2 = await bh.quote(fp('1'), RoundingMode.FLOOR) + expectEqualArrays(quote.erc20s, quote2.erc20s) + expectEqualArrays(quote.quantities, quote2.quantities) }) - it('Should not normalize by price if the new collateral is unsound', async () => { - await indexBH.connect(owner).setPrimeBasket([token0.address], [fp('1')]) - await indexBH.connect(owner).refreshBasket() - await setOraclePrice(collateral1.address, fp('0.5')) - await assetRegistry.refresh() - expect(await collateral0.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral1.status()).to.equal(CollateralStatus.IFFY) - await expect( - indexBH.connect(owner).setPrimeBasket([token1.address], [fp('1')]) - ).to.be.revertedWith('unsound new collateral') + it('Should retain backwards-compatible quote() -- CEIL', async () => { + const bh = new ethers.Contract(indexBH.address, oldBHInterface, owner) + const quote = await indexBH.quote(fp('1'), true, RoundingMode.CEIL) + const quote2 = await bh.quote(fp('1'), RoundingMode.CEIL) + expectEqualArrays(quote.erc20s, quote2.erc20s) + expectEqualArrays(quote.quantities, quote2.quantities) }) }) @@ -2370,7 +2335,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { const amount = fp('10000') await expect( indexBH.quoteCustomRedemption(basketNonces, portions, amount) - ).to.be.revertedWith('bad portions len') + ).to.be.revertedWith('invalid lengths') }) it('Should correctly quote the current basket, same as quote()', async () => { @@ -2380,7 +2345,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { const basketNonces = [1] const portions = [fp('1')] const amount = fp('10000') - const baseline = await indexBH.quote(amount, RoundingMode.FLOOR) + const baseline = await indexBH.quote(amount, false, RoundingMode.FLOOR) const quote = await indexBH.quoteCustomRedemption(basketNonces, portions, amount) expectEqualArrays(quote.erc20s, baseline.erc20s) expectEqualArrays(quote.quantities, baseline.quantities) @@ -2532,7 +2497,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { fp('0.1'), ] const amount = fp('10000') - const baseline = await indexBH.quote(amount, RoundingMode.FLOOR) + const baseline = await indexBH.quote(amount, false, RoundingMode.FLOOR) const quote = await indexBH.quoteCustomRedemption(basketNonces, portions, amount) expectEqualArrays(quote.erc20s, baseline.erc20s) expectEqualArrays(quote.quantities, baseline.quantities) @@ -2957,7 +2922,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { basketHandler .connect(owner) .setBackupConfig(ethers.utils.formatBytes32String('USD'), bn(1), erc20s) - ).to.not.be.revertedWith('erc20s too large') + ).to.not.be.revertedWith('too large') // Should fail at 65 erc20s.push(ONE_ADDRESS) @@ -2965,21 +2930,21 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { basketHandler .connect(owner) .setBackupConfig(ethers.utils.formatBytes32String('USD'), bn(1), erc20s) - ).to.be.revertedWith('erc20s too large') + ).to.be.revertedWith('too large') // Should succeed at 64 await expect( basketHandler .connect(owner) .setBackupConfig(ethers.utils.formatBytes32String('USD'), bn(64), []) - ).to.not.be.revertedWith('max too large') + ).to.not.be.revertedWith('too large') // Should fail at 65 await expect( basketHandler .connect(owner) .setBackupConfig(ethers.utils.formatBytes32String('USD'), bn(65), []) - ).to.be.revertedWith('max too large') + ).to.be.revertedWith('too large') }) it('Should allow to set backup Config if OWNER', async () => { @@ -3122,7 +3087,68 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { // expect(toks.length).to.equal(0) }) - it('Should include value of defaulted collateral when checking basket price', async () => { + it('Should include value of defaulted collateral when checking basket price -- /w premium', async () => { + // Check status and price + expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) + await expectPrice(basketHandler.address, fp('1'), ORACLE_ERROR, true) + + // Default one of the collaterals + // Set Token1 to default - 50% price reduction + await setOraclePrice(collateral1.address, bn('0.5e8')) + + // Mark default as probable + await collateral1.refresh() + + // Advance time post delayUntilDefault + await advanceTime((await collateral1.delayUntilDefault()).toString()) + + // Mark default as confirmed + await collateral1.refresh() + + // Check status and price again + expect(await basketHandler.status()).to.equal(CollateralStatus.DISABLED) + + // Check BU price -- 1/4 of the basket has lost half its value + const avgPrice = fp('0.875') + let [lowPrice, highPrice] = await basketHandler.price(true) + const expectedLow = avgPrice.sub(avgPrice.mul(ORACLE_ERROR).div(fp('1'))) + const expectedHigh = fp('1').add(fp('1').mul(ORACLE_ERROR).div(fp('1'))) // at-peg! + + const tolerance = avgPrice.div(bn('1e15')) + expect(lowPrice).to.be.closeTo(expectedLow, tolerance) + expect(lowPrice).to.be.gte(expectedLow) + expect(highPrice).to.be.closeTo(expectedHigh, tolerance) + expect(highPrice).to.be.lte(expectedHigh) + + // Set collateral1 price to [0, FIX_MAX] + await advanceTime(DECAY_DELAY.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 + ;[lowPrice, highPrice] = await basketHandler.price(true) + expect(lowPrice).to.be.closeTo(fp('0.75'), fp('0.75').div(100)) // within 1% + expect(highPrice).to.equal(MAX_UINT192) + + // Set basket config + await expect( + basketHandler + .connect(owner) + .setBackupConfig(ethers.utils.formatBytes32String('USD'), bn(1), [ + token0.address, + token2.address, + token3.address, + ]) + ).to.emit(basketHandler, 'BackupConfigSet') + + // After basket refresh, price should increase + await basketHandler.refreshBasket() + + // Check BU price + await expectPrice(basketHandler.address, fp('1'), ORACLE_ERROR, true) + }) + + it('Should include value of defaulted collateral when checking basket price -- w/o premium', async () => { // Check status and price expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) await expectPrice(basketHandler.address, fp('1'), ORACLE_ERROR, true) @@ -3152,8 +3178,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { await assetRegistry.refresh() // Check BU price -- 1/4 of the basket has lost all its value - const asset = await ethers.getContractAt('Asset', basketHandler.address) - const [lowPrice, highPrice] = await asset.price() + const [lowPrice, highPrice] = await basketHandler.price(false) expect(lowPrice).to.be.closeTo(fp('0.75'), fp('0.75').div(100)) // within 1% expect(highPrice).to.equal(MAX_UINT192) @@ -3208,7 +3233,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { await setOraclePrice(collateral1.address, bn('1e8')) // Check status and price again - const p = await basketHandler.price() + const p = await basketHandler.price(false) expect(p[0]).to.be.closeTo(fp('1').div(4), fp('1').div(4).div(100)) // within 1% expect(p[0]).to.be.lt(fp('1').div(4)) expect(p[1]).to.equal(MAX_UINT192) @@ -3275,7 +3300,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { const newPrice: BigNumber = MAX_UINT192.div(bn('1e10')) await setOraclePrice(collateral2.address, newPrice.sub(newPrice.div(100))) // oracle error - const [lowPrice, highPrice] = await indexBH.price() + const [lowPrice, highPrice] = await indexBH.price(false) expect(lowPrice).to.equal(MAX_UINT192) expect(highPrice).to.equal(MAX_UINT192) }) @@ -3288,7 +3313,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { const newPrice: BigNumber = MAX_UINT192.div(bn('1e10')) await setOraclePrice(collateral0.address, newPrice.sub(newPrice.div(100))) // oracle error - const [lowPrice, highPrice] = await indexBH.price() + const [lowPrice, highPrice] = await indexBH.price(false) expect(lowPrice).to.equal(MAX_UINT192) expect(highPrice).to.equal(MAX_UINT192) }) @@ -3611,7 +3636,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { expect(await basketHandler.status()).to.equal(CollateralStatus.IFFY) await basketHandler.connect(owner).refreshBasket() expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) - const [tokens] = await basketHandler.quote(fp('1'), 0) + const [tokens] = await basketHandler.quote(fp('1'), false, 0) expect(tokens.length).to.equal(3) expect(tokens[0]).to.not.equal(collateral1.address) expect(tokens[1]).to.not.equal(collateral1.address) diff --git a/test/RToken.test.ts b/test/RToken.test.ts index aadd8e62f1..6d5f2a54db 100644 --- a/test/RToken.test.ts +++ b/test/RToken.test.ts @@ -305,7 +305,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { }) it('Should return a price of 0 if the assets become unregistered', async () => { - const startPrice = await basketHandler.price() + const startPrice = await basketHandler.price(false) expect(startPrice[0]).to.gt(0) expect(startPrice[1]).to.gt(0) @@ -314,7 +314,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { await assetRegistry.connect(owner).unregister(basket[i].address) } - const endPrice = await basketHandler.price() + const endPrice = await basketHandler.price(false) expect(endPrice[0]).to.eq(0) expect(endPrice[1]).to.eq(0) diff --git a/test/RTokenExtremes.test.ts b/test/RTokenExtremes.test.ts index bdf29d52fb..af09e357d9 100644 --- a/test/RTokenExtremes.test.ts +++ b/test/RTokenExtremes.test.ts @@ -4,7 +4,7 @@ import { expect } from 'chai' import { BigNumber, ContractFactory } from 'ethers' import { ethers } from 'hardhat' import { BN_SCALE_FACTOR, CollateralStatus } from '../common/constants' -import { bn, fp, shortString } from '../common/numbers' +import { bn, fp, shortString, toBNDecimals } from '../common/numbers' import { ERC20MockDecimals, FiatCollateral, @@ -139,12 +139,14 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { for (let i = 0; i < N; i++) { const erc20: ERC20MockDecimals = erc20s[i] // user owner starts with enough basket assets to issue (totalSupply - toIssue) - const toMint0: BigNumber = toIssue0.mul(weights[i]).add(e18.sub(1)).div(e18) + const toIssue0Scaled: BigNumber = toBNDecimals(toIssue0, Number(collateralDecimals)) + const toMint0: BigNumber = toIssue0Scaled.mul(weights[i]).add(e18.sub(1)).div(e18) await erc20.mint(owner.address, toMint0) await erc20.connect(owner).increaseAllowance(rToken.address, toMint0) // user addr1 starts with enough basket assets to issue (toIssue) - const toMint: BigNumber = toIssue.mul(weights[i]).add(e18.sub(1)).div(e18) + const toIssueScaled: BigNumber = toBNDecimals(toIssue, Number(collateralDecimals)) + const toMint: BigNumber = toIssueScaled.mul(weights[i]).add(e18.sub(1)).div(e18) await erc20.mint(addr1.address, toMint) await erc20.connect(addr1).increaseAllowance(rToken.address, toMint) } @@ -219,12 +221,12 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { [MIN_RTOKENS, MAX_RTOKENS], // toIssue [MIN_RTOKENS, MAX_RTOKENS], // toRedeem [MAX_RTOKENS], // totalSupply - [bn(1)], // numAssets + [bn(1), bn(3)], // numAssets [MIN_WEIGHT, MAX_WEIGHT], // weightFirst [MIN_WEIGHT], // weightRest [MIN_ISSUANCE_PCT, fp(1)], // issuanceThrottle.pctRate [MIN_REDEMPTION_PCT, fp(1)], // redemptionThrottle.pctRate - [bn(6), bn(27)], // collateralDecimals + [bn(6), bn(18), bn(27)], // collateralDecimals ] paramList = cartesianProduct(...bounds) } diff --git a/test/Recollateralization.test.ts b/test/Recollateralization.test.ts index d448fd2884..d61a2f1cfb 100644 --- a/test/Recollateralization.test.ts +++ b/test/Recollateralization.test.ts @@ -2260,7 +2260,63 @@ 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 - const leftoverSellAmt = issueAmount.sub(sellAmt) + 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, + }, + ]) + + // 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 token0.balanceOf(backingManager.address)).to.equal( + issueAmount.sub(sellAmt.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 + 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 remaining amount + const leftoverSellAmt = issueAmount.sub(sellAmt.mul(2)) const leftoverMinBuyAmt = await toMinBuyAmt(leftoverSellAmt, fp('0.5'), fp('1')) await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ { @@ -2321,8 +2377,11 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { // 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) + // 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) await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ { contract: backingManager, diff --git a/test/Revenues.test.ts b/test/Revenues.test.ts index 47afb01805..aa3ea4dda0 100644 --- a/test/Revenues.test.ts +++ b/test/Revenues.test.ts @@ -474,18 +474,9 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { await setOraclePrice(collateral0.address, bn('7e7')) await collateral0.refresh() await token0.connect(addr1).transfer(rTokenTrader.address, issueAmount) - const rtokenPrice = await basketHandler.price() - const realRtokenPrice = rtokenPrice.low.add(rtokenPrice.high).div(2) - const minBuyAmt = await toMinBuyAmt(issueAmount, fp('0.7'), realRtokenPrice) await expect(rTokenTrader.manageTokens([token0.address], [TradeKind.BATCH_AUCTION])) .to.emit(rTokenTrader, 'TradeStarted') - .withArgs( - anyValue, - token0.address, - rToken.address, - issueAmount, - withinTolerance(minBuyAmt) - ) + .withArgs(anyValue, token0.address, rToken.address, issueAmount, anyValue) }) it('Should forward revenue to traders', async () => { @@ -820,111 +811,6 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(newRTokenTotal).equal(bn(0)) }) - 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 token0.connect(owner).mint(rsrTrader.address, issueAmount.add(1)) @@ -958,9 +844,6 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { // Mint RSR await rsr.connect(owner).mint(rsrTrader.address, issueAmount) - // Mint RSR - await rsr.connect(owner).mint(rsrTrader.address, issueAmount) - // Should fail for unregistered token await assetRegistry.connect(owner).unregister(collateral1.address) await expect( @@ -1124,7 +1007,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { // Trade should have extremely nonzero worst-case price const trade = await getTrade(rTokenTrader, token0.address) expect(await trade.initBal()).to.equal(issueAmount) - expect(await trade.worstCasePrice()).to.be.gte(fp('0.775')) + expect(await trade.worstCasePrice()).to.be.gte(fp('0.485').mul(bn('1e9'))) // D27 precision }) it('Should claim COMP and handle revenue auction correctly - small amount processed in single auction', async () => { @@ -1325,7 +1208,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { ).to.emit(rTokenTrader, 'TradeStarted') }) - it('Should only be able to start a dust auction BATCH_AUCTION (and not DUTCH_AUCTION) if oracle has failed', async () => { + it('Should be able to force through a dust BATCH auction but not DUTCH, if oracle has failed', async () => { const minTrade = bn('1e18') await rsrTrader.connect(owner).setMinTradeVolume(minTrade) @@ -1348,6 +1231,8 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { rsrTrader, 'TradeStarted' ) + + expect(await token0.balanceOf(rsrTrader.address)).to.equal(0) // should sell entire balance }) it('Should not launch an auction for 1 qTok', async () => { @@ -1711,7 +1596,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(99) // due to oracle error + const sellAmt: BigNumber = fp('1').mul(100).div(101) // due to high price setting trade size const minBuyAmt: BigNumber = await toMinBuyAmt(sellAmt, fp('1'), fp('1')) // Run auctions @@ -2300,10 +2185,6 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { await rsr.connect(addr1).transfer(rTokenTrader.address, distAmount) await rsr.connect(addr1).transfer(rsrTrader.address, distAmount) - // 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) @@ -2985,127 +2866,6 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { 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 - const router = await (await ethers.getContractFactory('DutchTradeRouter')).deploy() - await rsr.connect(addr1).approve(router.address, constants.MaxUint256) - await rToken.connect(addr1).approve(router.address, constants.MaxUint256) - 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 expectEvents(facadeTest.claimRewards(rToken.address), [ diff --git a/test/Upgradeability.test.ts b/test/Upgradeability.test.ts index 0e4e24bd0d..8a610f7c4a 100644 --- a/test/Upgradeability.test.ts +++ b/test/Upgradeability.test.ts @@ -257,7 +257,7 @@ describeP1(`Upgradeability - P${IMPLEMENTATION}`, () => { it('Should deploy valid implementation - BasketHandler', async () => { const newBasketHandler: BasketHandlerP1 = await upgrades.deployProxy( BasketHandlerFactory, - [main.address, config.warmupPeriod, config.reweightable], + [main.address, config.warmupPeriod, config.reweightable, config.enableIssuancePremium], { initializer: 'init', kind: 'uups', @@ -903,7 +903,9 @@ describeP1(`Upgradeability - P${IMPLEMENTATION}`, () => { beforeEach(async () => { const versionRegistryFactory = await ethers.getContractFactory('VersionRegistry') - versionRegistry = await versionRegistryFactory.deploy(owner.address) + const mockRoleRegistryFactory = await ethers.getContractFactory('MockRoleRegistry') + const mockRoleRegistry = await mockRoleRegistryFactory.deploy() + versionRegistry = await versionRegistryFactory.deploy(mockRoleRegistry.address) const assetPluginRegistryFactory = await ethers.getContractFactory('AssetPluginRegistry') assetPluginRegistry = await assetPluginRegistryFactory.deploy(versionRegistry.address) diff --git a/test/fixtures.ts b/test/fixtures.ts index cf52748cba..d916eaa2fe 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -457,6 +457,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = withdrawalLeak: fp('0'), // 0%; always refresh warmupPeriod: bn('60'), // (the delay _after_ SOUND was regained) reweightable: false, + enableIssuancePremium: true, tradingDelay: bn('0'), // (the delay _after_ default has been confirmed) batchAuctionLength: bn('900'), // 15 minutes dutchAuctionLength: bn('1800'), // 30 minutes diff --git a/test/fuzz/ChaosScenario.test.ts b/test/fuzz/ChaosScenario.test.ts index f5d938ee36..03a15f6caf 100644 --- a/test/fuzz/ChaosScenario.test.ts +++ b/test/fuzz/ChaosScenario.test.ts @@ -218,6 +218,8 @@ const scenarioSpecificTests = () => { // Uses reweightable basket expect(await comp.basketHandler.reweightable()).to.be.true + + expect(await comp.basketHandler.enableIssuancePremium()).to.equal(true) }) it('can create stable+ collateral with reward', async () => { diff --git a/test/fuzz/NormalScenario.test.ts b/test/fuzz/NormalScenario.test.ts index 45bf939284..230be87fb2 100644 --- a/test/fuzz/NormalScenario.test.ts +++ b/test/fuzz/NormalScenario.test.ts @@ -192,6 +192,8 @@ const scenarioSpecificTests = () => { expect(await comp.broker.main()).to.equal(main.address) expect(await comp.basketHandler.status()).to.equal(CollateralStatus.SOUND) + + expect(await comp.basketHandler.enableIssuancePremium()).to.equal(true) }) it('has only initially-true properties', async () => { diff --git a/test/fuzz/RebalancingScenario.test.ts b/test/fuzz/RebalancingScenario.test.ts index 613402c3dd..eb8aff05ef 100644 --- a/test/fuzz/RebalancingScenario.test.ts +++ b/test/fuzz/RebalancingScenario.test.ts @@ -222,6 +222,8 @@ const scenarioSpecificTests = () => { expect(await comp.rTokenTrader.main()).to.equal(main.address) expect(await comp.furnace.main()).to.equal(main.address) expect(await comp.broker.main()).to.equal(main.address) + + expect(await comp.basketHandler.enableIssuancePremium()).to.equal(true) }) it('can create stable+ collateral with reward', async () => { @@ -360,7 +362,7 @@ const scenarioSpecificTests = () => { it('performs validations on set prime basket if non-reweightable', async () => { // Check current basket - const [tokenAddrs] = await comp.basketHandler.quote(1n * exa, RoundingMode.CEIL) + const [tokenAddrs] = await comp.basketHandler['quote(uint192,bool,uint8)'](1n * exa, true, RoundingMode.CEIL) expect(tokenAddrs.length).to.equal(9) @@ -409,7 +411,7 @@ const scenarioSpecificTests = () => { await comp.basketHandler.savePrev() await scenario.refreshBasket() - const [newTokenAddrs, amts] = await comp.basketHandler.quote(1n * exa, RoundingMode.CEIL) + const [newTokenAddrs, amts] = await comp.basketHandler['quote(uint192,bool,uint8)'](1n * exa, true, RoundingMode.CEIL) expect(await comp.basketHandler.prevEqualsCurr()).to.be.false expect(newTokenAddrs.length).to.equal(3) diff --git a/test/fuzz/common.ts b/test/fuzz/common.ts index f3635c4442..1a52300f20 100644 --- a/test/fuzz/common.ts +++ b/test/fuzz/common.ts @@ -70,6 +70,7 @@ export const CONFIG: IConfig = { issuanceRate: fp('0.00025'), // 0.025% per block or ~0.1% per minute scalingRedemptionRate: fp('0.05'), redemptionRateFloor: fp('2e7'), + enableIssuancePremium: true } export const ZERO_COMPONENTS = { diff --git a/test/fuzz/commonAbnormalTests.ts b/test/fuzz/commonAbnormalTests.ts index 10fda1c8fc..379c970521 100644 --- a/test/fuzz/commonAbnormalTests.ts +++ b/test/fuzz/commonAbnormalTests.ts @@ -116,7 +116,7 @@ export default function fn(context: FuzzTestContext { // Check current basket - const [tokenAddrs] = await comp.basketHandler.quote(1n * exa, RoundingMode.CEIL) + const [tokenAddrs] = await comp.basketHandler['quote(uint192,bool,uint8)'](1n * exa, true, RoundingMode.CEIL) expect(tokenAddrs.length).to.equal(9) @@ -152,7 +152,7 @@ export default function fn(context: FuzzTestContext(context: FuzzTestContext { + it('EasyAuction reverts when sum of bids > type(uint96).max', async () => { + const sellAmount = fp('1') + const endTime = (await getLatestBlockTimestamp()) + Number(config.batchAuctionLength) + const minBuyAmount = MAX_UINT96.sub(1) + + // Mints tokens + await token0.connect(owner).mint(owner.address, sellAmount) + await token1.connect(owner).mint(addr1.address, MAX_UINT96) + await token1.connect(owner).mint(addr2.address, MAX_UINT96) + + // Start auction + await token0.connect(owner).approve(easyAuction.address, sellAmount) + + // Get auction Id + const auctionId = await easyAuction.callStatic.initiateAuction( + token0.address, + token1.address, + endTime, + endTime, + sellAmount, + minBuyAmount, + 1, + 0, + false, + ZERO_ADDRESS, + new Uint8Array(0) + ) + + // Initiate auction + await easyAuction.initiateAuction( + token0.address, + token1.address, + endTime, + endTime, + sellAmount, + minBuyAmount, + 1, + 0, + false, + ZERO_ADDRESS, + new Uint8Array(0) + ) + + // Perform first bid + await token1.connect(addr1).approve(easyAuction.address, minBuyAmount.sub(1)) + await easyAuction.connect(addr1).placeSellOrders( + auctionId, + [1], + [minBuyAmount.sub(1)], // falls short + [QUEUE_START], + ethers.constants.HashZero + ) + + // Perform second bid + await token1.connect(addr2).approve(easyAuction.address, minBuyAmount) + await easyAuction.connect(addr2).placeSellOrders( + auctionId, + [1], + [minBuyAmount.sub(1)], // Sum will exceed uint96.MAX + [QUEUE_START], + ethers.constants.HashZero + ) + + // Attempt to settle - should revert + await advanceTime(config.batchAuctionLength.add(100).toString()) + await expect(easyAuction.settleAuction(auctionId)).to.be.revertedWith( + "SafeCast: value doesn't fit in 96 bits" + ) + }) + }) + describeExtreme(`Extreme Values ${SLOW ? 'slow mode' : 'fast mode'}`, () => { if (!(Implementation.P1 && useEnv('EXTREME') && useEnv('FORK'))) return // prevents bunch of skipped tests diff --git a/test/integration/UpgradeToR4.test.ts b/test/integration/UpgradeToR4.test.ts index b5e6215c90..3d6d24db09 100644 --- a/test/integration/UpgradeToR4.test.ts +++ b/test/integration/UpgradeToR4.test.ts @@ -98,7 +98,9 @@ describe('Upgrade from 3.4.0 to 4.0.0 (Mainnet Fork)', () => { ) const versionRegistryFactory = await ethers.getContractFactory('VersionRegistry') - versionRegistry = await versionRegistryFactory.deploy(await owner.getAddress()) + const mockRoleRegistryFactory = await ethers.getContractFactory('MockRoleRegistry') + const mockRoleRegistry = await mockRoleRegistryFactory.deploy() + versionRegistry = await versionRegistryFactory.deploy(mockRoleRegistry.address) await versionRegistry.registerVersion(deployer.address) @@ -107,7 +109,10 @@ describe('Upgrade from 3.4.0 to 4.0.0 (Mainnet Fork)', () => { (await AssetPluginRegistryFactory.deploy()) as unknown as AssetPluginRegistry const DAOFeeRegistryFactory = await ethers.getContractFactory('DAOFeeRegistry') - daoFeeRegistry = await DAOFeeRegistryFactory.deploy(await owner.getAddress()) + daoFeeRegistry = await DAOFeeRegistryFactory.deploy( + mockRoleRegistry.address, + await owner.getAddress() + ) }) describe('The Upgrade', () => { diff --git a/test/integration/UpgradeToR4WithRegistries.test.ts b/test/integration/UpgradeToR4WithRegistries.test.ts index b0b12a7334..632a4d37ee 100644 --- a/test/integration/UpgradeToR4WithRegistries.test.ts +++ b/test/integration/UpgradeToR4WithRegistries.test.ts @@ -46,13 +46,18 @@ describe('Upgrade from 4.0.0 to New Version with all Registries Enabled', () => // Setup Registries const versionRegistryFactory = await ethers.getContractFactory('VersionRegistry') - versionRegistry = await versionRegistryFactory.deploy(await owner.getAddress()) + const mockRoleRegistryFactory = await ethers.getContractFactory('MockRoleRegistry') + const mockRoleRegistry = await mockRoleRegistryFactory.deploy() + versionRegistry = await versionRegistryFactory.deploy(mockRoleRegistry.address) const AssetPluginRegistryFactory = await ethers.getContractFactory('AssetPluginRegistry') assetPluginRegistry = await AssetPluginRegistryFactory.deploy(versionRegistry.address) const DAOFeeRegistryFactory = await ethers.getContractFactory('DAOFeeRegistry') - daoFeeRegistry = await DAOFeeRegistryFactory.deploy(await owner.getAddress()) + daoFeeRegistry = await DAOFeeRegistryFactory.deploy( + mockRoleRegistry.address, + await owner.getAddress() + ) // Setup Common Dependencies const TradingLibFactory = await ethers.getContractFactory('RecollateralizationLibP1') diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index 97630e1ba6..49ff961a8d 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -644,6 +644,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = withdrawalLeak: fp('0'), // 0%; always refresh warmupPeriod: bn('60'), // (the delay _after_ SOUND was regained) reweightable: false, + enableIssuancePremium: true, tradingDelay: bn('0'), // (the delay _after_ default has been confirmed) batchAuctionLength: bn('900'), // 15 minutes dutchAuctionLength: bn('1800'), // 30 minutes diff --git a/test/integration/fork-block-numbers.ts b/test/integration/fork-block-numbers.ts index e0adbbffc9..70b563090c 100644 --- a/test/integration/fork-block-numbers.ts +++ b/test/integration/fork-block-numbers.ts @@ -8,6 +8,8 @@ const forkBlockNumber = { 'facade-monitor': 18742016, // Ethereum 'old-curve-plugins': 16915576, // Ethereum 'new-curve-plugins': 19626711, // Ethereum + 'mainnet-3.4.0': 20328530, // Ethereum + // TODO add all the block numbers we fork from to benefit from caching default: 19742528, // Ethereum } diff --git a/test/integration/mainnet-test/IssuancePremium.test.ts b/test/integration/mainnet-test/IssuancePremium.test.ts new file mode 100644 index 0000000000..7c014d2428 --- /dev/null +++ b/test/integration/mainnet-test/IssuancePremium.test.ts @@ -0,0 +1,402 @@ +import { expect } from 'chai' +import { BigNumber } from 'ethers' +import hre, { ethers } from 'hardhat' +import { evmRevert, evmSnapshot } from '../utils' +import { bn, fp } from '../../../common/numbers' +import { IMPLEMENTATION } from '../../fixtures' +import { getChainId } from '../../../common/blockchain-utils' +import { networkConfig } from '../../../common/configuration' +import forkBlockNumber from '../fork-block-numbers' +import { whileImpersonating } from '../../utils/impersonation' +import { + AssetRegistryP1, + EmaPriceOracleStableSwapMock, + LidoStakedEthCollateral, + RTokenAsset, + SFraxEthCollateral, + TestIBasketHandler, + RethCollateral, +} from '../../../typechain' +import { useEnv } from '#/utils/env' +import { combinedError } from '#/scripts/deployment/utils' + +const describeFork = useEnv('FORK') ? describe : describe.skip + +const ASSET_REGISTRY_ADDR = '0xf526f058858E4cD060cFDD775077999562b31bE0' // ETH+ asset registry +const BASKET_HANDLER_ADDR = '0x56f40A33e3a3fE2F1614bf82CBeb35987ac10194' // ETH+ basket handler +const BASKET_LIB_ADDR = '0xf383dC60D29A5B9ba461F40A0606870d80d1EA88' // BasketLibP1 +const RTOKEN_ASSET_ADDR = '0x3f11C47E7ed54b24D7EFC222FD406d8E1F49Fb69' // ETH+ RTokenAsset +const OWNER = '0x5d8A7DC9405F08F14541BA918c1Bf7eb2dACE556' // ETH+ timelock + +// run on mainnet only + +describeFork(`ETH+ Issuance Premium - Mainnet Forking P${IMPLEMENTATION}`, function () { + let assetRegistry: AssetRegistryP1 + let basketHandler: TestIBasketHandler + let rTokenAsset: RTokenAsset + let chainId: string + + let snap: string + + let oldRTokenPrice: BigNumber[] // <4.0.0 + let newRTokenPrice: BigNumber[] // >= <4.0.0 + let oldPrice: BigNumber[] // <4.0.0 + let newPriceF: BigNumber[] // >= 4.0.0 price(false) + let newPriceT: BigNumber[] // >= 4.0.0 price(true) + let oldQs: BigNumber[] // <4.0.0 quantities + let newQs: BigNumber[] // >= 4.0.0 quantities + + let sfrxETH: SFraxEthCollateral + let sfraxEmaOracle: EmaPriceOracleStableSwapMock + + // 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, + }, + }, + ], + }) + } + + before(async () => { + await setup(forkBlockNumber['mainnet-3.4.0']) + + chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + assetRegistry = ( + await ethers.getContractAt('AssetRegistryP1', ASSET_REGISTRY_ADDR) + ) + basketHandler = ( + await ethers.getContractAt('TestIBasketHandler', BASKET_HANDLER_ADDR) + ) + rTokenAsset = await ethers.getContractAt('RTokenAsset', RTOKEN_ASSET_ADDR) + + const oldBasketHandler = await ethers.getContractAt('BasketHandlerP1', BASKET_HANDLER_ADDR) + oldRTokenPrice = await rTokenAsset.price() + oldPrice = await oldBasketHandler['price()']() + oldQs = (await oldBasketHandler['quote(uint192,uint8)'](fp('1'), 2)).quantities + + // frxETH/ETH EMA oracle + const currentEmaOracle = await ethers.getContractAt( + 'contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol:IEmaPriceOracleStableSwap', + networkConfig[chainId].CURVE_POOL_WETH_FRXETH! + ) + const EmaPriceOracleStableSwapMockFactory = await ethers.getContractFactory( + 'EmaPriceOracleStableSwapMock' + ) + sfraxEmaOracle = ( + await EmaPriceOracleStableSwapMockFactory.deploy(await currentEmaOracle.price_oracle()) + ) + + // === Upgrade to 4.0.0 (minimally)=== + + // RTokenAsset + const RTokenAssetFactory = await ethers.getContractFactory('RTokenAsset') + rTokenAsset = await RTokenAssetFactory.deploy( + await rTokenAsset.erc20(), + await rTokenAsset.maxTradeVolume() + ) + + // BasketHandler + const BasketHandlerFactory = await ethers.getContractFactory('BasketHandlerP1', { + libraries: { + BasketLibP1: BASKET_LIB_ADDR, + }, + }) + const newBasketHandlerImpl = await BasketHandlerFactory.deploy() + + // SFraxEthCollateral + const SFraxEthCollateralFactory = await hre.ethers.getContractFactory('SFraxEthCollateral') + let oracleError = combinedError(fp('0.005'), fp('0.0002')) // 0.5% & 0.02% + const newSfrxETH = await SFraxEthCollateralFactory.deploy( + { + priceTimeout: '604800', + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.ETH!, + oracleError: oracleError.toString(), + erc20: networkConfig[chainId].tokens.sfrxETH!, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '3600', // 1 hr + 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% + sfraxEmaOracle.address + ) + sfrxETH = newSfrxETH + await sfrxETH.refresh() + + // LidoStakedEthCollateral + const WSTETHCollateralFactory = await hre.ethers.getContractFactory('LidoStakedEthCollateral') + const newWstETH = await WSTETHCollateralFactory.deploy( + { + priceTimeout: '604800', + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.stETHUSD!, + oracleError: fp('0.01').toString(), // 1%: only for stETHUSD feed + erc20: networkConfig[chainId].tokens.wstETH!, + maxTradeVolume: fp('1e6').toString(), // $1m, + 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% + networkConfig[chainId].chainlinkFeeds.stETHETH!, // targetPerRefChainlinkFeed + '86400' // targetPerRefChainlinkTimeout + ) + await newWstETH.refresh() + + // RethCollateral + const RethCollateralFactory = await hre.ethers.getContractFactory('RethCollateral') + oracleError = combinedError(fp('0.005'), fp('0.02')) // 0.5% & 2% + const newRETH = await RethCollateralFactory.deploy( + { + priceTimeout: '604800', + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.ETH!, + oracleError: oracleError.toString(), // 1%: only for rETH feed + erc20: networkConfig[chainId].tokens.rETH!, + maxTradeVolume: fp('1e6').toString(), // $1m, + 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.rETH!, + '86400' // refPerTokChainlinkTimeout + ) + await newRETH.refresh() + + // Putting it all together... + await whileImpersonating(OWNER, async (timelockSigner) => { + const bh = await ethers.getContractAt('BasketHandlerP1', BASKET_HANDLER_ADDR) + await bh.connect(timelockSigner).upgradeTo(newBasketHandlerImpl.address) + await assetRegistry.connect(timelockSigner).swapRegistered(newSfrxETH.address) + await assetRegistry.connect(timelockSigner).swapRegistered(newWstETH.address) + await assetRegistry.connect(timelockSigner).swapRegistered(newRETH.address) + await assetRegistry.connect(timelockSigner).swapRegistered(rTokenAsset.address) + await basketHandler.connect(timelockSigner).setIssuancePremiumEnabled(true) + }) + await basketHandler.refreshBasket() + expect(await basketHandler.status()).to.equal(0) + expect(await basketHandler.fullyCollateralized()).to.equal(true) + + newRTokenPrice = await rTokenAsset.price() + newPriceF = await basketHandler.price(false) + newPriceT = await basketHandler.price(true) + newQs = (await basketHandler.quote(fp('1'), true, 2)).quantities + + snap = await evmSnapshot() // what are testing frameworks for if not this in all its glory + }) + + beforeEach(async () => { + await evmRevert(snap) + snap = await evmSnapshot() + }) + + after(async () => { + await evmRevert(snap) + }) + + it('from 3.4.0 to 4.0.0', async () => { + // this test case compares the state before the 4.0.0 upgrade to the state after the 4.0.0 upgrade + // USD issuance costs rise ~0.04% due to sfrxETH's ~0.12% premium, as given by basketHandler.price(true) + + // rTokenAsset.price() + const lowRTokenPriceChange = newRTokenPrice[0] + .sub(oldRTokenPrice[0]) + .mul(fp('1')) + .div(oldRTokenPrice[0]) + const highRTokenPriceChange = newRTokenPrice[1] + .sub(oldRTokenPrice[1]) + .mul(fp('1')) + .div(oldRTokenPrice[1]) + expect(lowRTokenPriceChange).to.be.closeTo(fp('0'), fp('1e-4')) // low RToken price no change + expect(highRTokenPriceChange).to.be.closeTo(fp('0.0004'), fp('1e-4')) // high RToken price +0.04% + + // basketHandler.price(false) + const lowPriceChangeF = newPriceF[0].sub(oldPrice[0]).mul(fp('1')).div(oldPrice[0]) + const highPriceChangeF = newPriceF[1].sub(oldPrice[1]).mul(fp('1')).div(oldPrice[1]) + expect(lowPriceChangeF).to.be.closeTo(fp('0'), fp('1e-4')) // low price no change + expect(highPriceChangeF).to.be.closeTo(fp('0'), fp('1e-4')) // high price no change + + // basketHandler.price(true) + const lowPriceChangeT = newPriceT[0].sub(oldPrice[0]).mul(fp('1')).div(oldPrice[0]) + const highPriceChangeT = newPriceT[1].sub(oldPrice[1]).mul(fp('1')).div(oldPrice[1]) + expect(lowPriceChangeT).to.be.closeTo(fp('0'), fp('1e-4')) // low price no change + expect(highPriceChangeT).to.be.closeTo(fp('0.0004'), fp('1e-4')) // high price +0.04% + + // basketHandler.quote() + const sfrxETHChange = newQs[0].sub(oldQs[0]).mul(fp('1')).div(oldQs[0]) + const wstETHChange = newQs[1].sub(oldQs[1]).mul(fp('1')).div(oldQs[1]) + const rETHChange = newQs[2].sub(oldQs[2]).mul(fp('1')).div(oldQs[2]) + expect(sfrxETHChange).to.be.closeTo(fp('0.0012'), fp('1e-4')) // sFraxETH +0.12% + expect(wstETHChange).to.be.closeTo(fp('0.0001'), fp('1e-4')) // wstETH +0.01% + expect(rETHChange).to.be.equal(0) // rETH no change + }) + + it('from 4.0.0 to 4.0.0 at-peg', async () => { + // this test case compares the state after the 4.0.0 upgrade to the state when frxETH is at peg + // as given by basketHandler.price(true), USD issuance costs do not change since the premium compensates completely + + await sfraxEmaOracle.setPrice(fp('1')) + + const parRTokenPrice = await rTokenAsset.price() + const parPriceF = await basketHandler.price(false) + const parPriceT = await basketHandler.price(true) + const parQs = (await basketHandler.quote(fp('1'), true, 2)).quantities + + // rTokenAsset.price() + const lowRTokenPriceChange = parRTokenPrice[0] + .sub(newRTokenPrice[0]) + .mul(fp('1')) + .div(newRTokenPrice[0]) + const highRTokenPriceChange = parRTokenPrice[1] + .sub(newRTokenPrice[1]) + .mul(fp('1')) + .div(newRTokenPrice[1]) + expect(lowRTokenPriceChange).to.be.closeTo(fp('0.0004'), fp('1e-4')) // low price +0.04% + expect(highRTokenPriceChange).to.be.closeTo(fp('0'), fp('1e-4')) // high price no change + + // basketHandler.price(false) + const lowPriceChangeF = parPriceF[0].sub(newPriceF[0]).mul(fp('1')).div(newPriceF[0]) + const highPriceChangeF = parPriceF[1].sub(newPriceF[1]).mul(fp('1')).div(newPriceF[1]) + expect(lowPriceChangeF).to.be.closeTo(fp('0.0004'), fp('1e-4')) // low price +0.04% + expect(highPriceChangeF).to.be.closeTo(fp('0.0004'), fp('1e-4')) // high price +0.04%% + + // basketHandler.price(true) + const lowPriceChangeT = parPriceT[0].sub(newPriceT[0]).mul(fp('1')).div(newPriceT[0]) + const highPriceChangeT = parPriceT[1].sub(newPriceT[1]).mul(fp('1')).div(newPriceT[1]) + expect(lowPriceChangeT).to.be.closeTo(fp('0.0004'), fp('1e-4')) // low price +0.04% + expect(highPriceChangeT).to.be.closeTo(fp('0'), fp('1e-4')) // high price no change + + // basketHandler.quote() + const sfrxETHChange = parQs[0].sub(newQs[0]).mul(fp('1')).div(newQs[0]) + const wstETHChange = parQs[1].sub(newQs[1]).mul(fp('1')).div(newQs[1]) + const rETHChange = parQs[2].sub(newQs[2]).mul(fp('1')).div(newQs[2]) + expect(sfrxETHChange).to.be.closeTo(fp('-0.0012'), fp('1e-4')) // sFraxETH -0.12%% + expect(wstETHChange).to.be.closeTo(fp('-0.0001'), fp('1e-4')) // wstETH -0.01% + expect(rETHChange).to.be.equal(0) // rETH no change + }) + + it('from 4.0.0 at-peg to 2% below peg', async () => { + // this test case compares the state from at-peg to the state after a 2% de-peg of frxETH + // which is well within the default threshold. + // as given by basketHandler.price(true), USD issuance costs do not change since the premium compensates completely + + await sfraxEmaOracle.setPrice(fp('1')) + + const parRTokenPrice = await rTokenAsset.price() + const parPriceF = await basketHandler.price(false) + const parPriceT = await basketHandler.price(true) + const parQs = (await basketHandler.quote(fp('1'), true, 2)).quantities + + // de-peg by 2% + + await sfraxEmaOracle.setPrice(fp('0.98')) + const depeggedRTokenPrice = await rTokenAsset.price() + await sfrxETH.refresh() + expect(await sfrxETH.savedPegPrice()).to.equal(fp('0.98')) + + const depeggedPriceF = await basketHandler.price(false) + const depeggedPriceT = await basketHandler.price(true) + const depeggedQs = (await basketHandler.quote(fp('1'), true, 2)).quantities + + // rTokenAsset.price() + const lowRTokenPriceChange = depeggedRTokenPrice[0] + .sub(parRTokenPrice[0]) + .mul(fp('1')) + .div(parRTokenPrice[0]) + const highRTokenPriceChange = depeggedRTokenPrice[1] + .sub(parRTokenPrice[1]) + .mul(fp('1')) + .div(parRTokenPrice[1]) + expect(lowRTokenPriceChange).to.be.closeTo(fp('-0.0067'), fp('1e-4')) // low RToken price -0.67% + expect(highRTokenPriceChange).be.closeTo(fp('-0.0065'), fp('1e-4')) // high RToken -0.66% + + // basketHandler.price(false) + const lowPriceChangeF = depeggedPriceF[0].sub(parPriceF[0]).mul(fp('1')).div(parPriceF[0]) + const highPriceChangeF = depeggedPriceF[1].sub(parPriceF[1]).mul(fp('1')).div(parPriceF[1]) + expect(lowPriceChangeF).to.be.closeTo(fp('-0.0067'), fp('1e-4')) // low price -0.67% + expect(highPriceChangeF).be.closeTo(fp('-0.0065'), fp('1e-4')) // high price -0.66% + + // basketHandler.price(true) + const lowPriceChangeT = depeggedPriceT[0].sub(parPriceT[0]).mul(fp('1')).div(parPriceT[0]) + const highPriceChangeT = depeggedPriceT[1].sub(parPriceT[1]).mul(fp('1')).div(parPriceT[1]) + expect(lowPriceChangeT).to.be.closeTo(fp('-0.0067'), fp('1e-4')) // low price -0.67% + expect(highPriceChangeT).be.closeTo(0, fp('1e-4')) // high price no change + + // basketHandler.quote() + const sfrxETHChange = depeggedQs[0].sub(parQs[0]).mul(fp('1')).div(parQs[0]) + const wstETHChange = depeggedQs[1].sub(parQs[1]).mul(fp('1')).div(parQs[1]) + const rETHChange = depeggedQs[2].sub(parQs[2]).mul(fp('1')).div(parQs[2]) + expect(sfrxETHChange).to.be.closeTo(fp('0.0204'), fp('1e-4')) // sFraxETH +2.04 + expect(wstETHChange).to.be.closeTo(0, fp('1e-4')) // wstETH no change + expect(rETHChange).to.be.equal(0) // rETH no change + }) + + it('from 4.0.0 at-peg to 50% below peg', async () => { + // this test case compares the state from at-peg to the state after a 50% de-peg of frxETH + // as given by basketHandler.price(true), USD issuance costs do not change since the premium compensates completely + + await sfraxEmaOracle.setPrice(fp('1')) + + const parRTokenPrice = await rTokenAsset.price() + const parPriceF = await basketHandler.price(false) + const parPriceT = await basketHandler.price(true) + const parQs = (await basketHandler.quote(fp('1'), true, 2)).quantities + + // de-peg by 50% + + await sfraxEmaOracle.setPrice(fp('0.5')) + const depeggedRTokenPrice = await rTokenAsset.price() + await sfrxETH.refresh() + expect(await sfrxETH.savedPegPrice()).to.equal(fp('0.5')) + + const depeggedPriceF = await basketHandler.price(false) + const depeggedPriceT = await basketHandler.price(true) + const depeggedQs = (await basketHandler.quote(fp('1'), true, 2)).quantities + + // rTokenAsset.price() + const lowRTokenPriceChange = depeggedRTokenPrice[0] + .sub(parRTokenPrice[0]) + .mul(fp('1')) + .div(parRTokenPrice[0]) + const highRTokenPriceChange = depeggedRTokenPrice[1] + .sub(parRTokenPrice[1]) + .mul(fp('1')) + .div(parRTokenPrice[1]) + expect(lowRTokenPriceChange).to.be.closeTo(fp('-0.1676'), fp('1e-4')) // low RToken price -16.76% + expect(highRTokenPriceChange).to.be.closeTo(fp('-0.1649'), fp('1e-4')) // high RToken price -16.49% + + // basketHandler.price(false) + const lowPriceChangeF = depeggedPriceF[0].sub(parPriceF[0]).mul(fp('1')).div(parPriceF[0]) + const highPriceChangeF = depeggedPriceF[1].sub(parPriceF[1]).mul(fp('1')).div(parPriceF[1]) + expect(lowPriceChangeF).to.be.closeTo(fp('-0.1676'), fp('1e-4')) // low price -16.76% + expect(highPriceChangeF).be.closeTo(fp('-0.1649'), fp('1e-4')) // high price -16.49% + + // basketHandler.price(true) + const lowPriceChangeT = depeggedPriceT[0].sub(parPriceT[0]).mul(fp('1')).div(parPriceT[0]) + const highPriceChangeT = depeggedPriceT[1].sub(parPriceT[1]).mul(fp('1')).div(parPriceT[1]) + expect(lowPriceChangeT).to.be.closeTo(fp('-0.1676'), fp('1e-4')) // low price -16.76% + expect(highPriceChangeT).be.closeTo(0, fp('1e-4')) // high price no change + + // basketHandler.quote() + const sfrxETHChange = depeggedQs[0].sub(parQs[0]).mul(fp('1')).div(parQs[0]) + const wstETHChange = depeggedQs[1].sub(parQs[1]).mul(fp('1')).div(parQs[1]) + const rETHChange = depeggedQs[2].sub(parQs[2]).mul(fp('1')).div(parQs[2]) + expect(sfrxETHChange).to.be.closeTo(fp('1'), fp('1e-4')) // sFraxETH +100% + expect(wstETHChange).to.be.closeTo(0, fp('1e-4')) // wstETH no change + expect(rETHChange).to.be.equal(0) // rETH no change + }) +}) diff --git a/test/plugins/Collateral.test.ts b/test/plugins/Collateral.test.ts index b07978fb52..833cc4e225 100644 --- a/test/plugins/Collateral.test.ts +++ b/test/plugins/Collateral.test.ts @@ -28,6 +28,7 @@ import { TestIBackingManager, TestIRToken, UnpricedAppreciatingFiatCollateralMock, + UnpricedFiatCollateralMock, USDCMock, WETH9, } from '../../typechain' diff --git a/test/plugins/OracleDeprecation.test.ts b/test/plugins/OracleDeprecation.test.ts index d76d16df0e..d3cb321335 100644 --- a/test/plugins/OracleDeprecation.test.ts +++ b/test/plugins/OracleDeprecation.test.ts @@ -44,7 +44,11 @@ describe('Chainlink Oracle', () => { await rToken.connect(wallet).issue(amt) }) - describe('Chainlink deprecates an asset', () => { + // Expected behavior on deprecation + // - Chainlink: latestRoundData() reverts and aggregator == address(0) + // - Redstone: latestRoundData() does not revert, only signal is outdated price + // - Chronicle: latestRoundData() does not revert, but price is set to 0 + describe('Chainlink/Chronicle deprecates an asset', () => { it('Refresh should mark the asset as IFFY', async () => { const MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') const [, aUSDCCollateral] = fixture.bySymbol.ausdc @@ -60,5 +64,21 @@ describe('Chainlink Oracle', () => { 'StalePrice' ) }) + + it('Price = 0 should mark the asset as IFFY (Chronicle)', async () => { + const MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') + const [, aUSDCCollateral] = fixture.bySymbol.ausdc + const chainLinkOracle = MockV3AggregatorFactory.attach(await aUSDCCollateral.chainlinkFeed()) + await aUSDCCollateral.refresh() + await aUSDCCollateral.tryPrice() + expect(await aUSDCCollateral.status()).to.equal(0) + await chainLinkOracle.updateAnswer(0) + await aUSDCCollateral.refresh() + expect(await aUSDCCollateral.status()).to.equal(1) + await expect(aUSDCCollateral.tryPrice()).to.be.revertedWithCustomError( + aUSDCCollateral, + 'InvalidPrice' + ) + }) }) }) diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 25f5eda7b0..1f2b98fbc3 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -224,18 +224,6 @@ export default function fn( } }) - it('enters IFFY state when price becomes stale', async () => { - const decayDelay = (await collateral.maxOracleTimeout()) + ORACLE_TIMEOUT_BUFFER - await advanceToTimestamp((await getLatestBlockTimestamp()) + decayDelay) - await advanceBlocks(decayDelay / 12) - await collateral.refresh() - expect(await collateral.status()).to.not.equal(CollateralStatus.SOUND) - if (!itHasOracleRefPerTok) { - // if an oracle isn't involved in refPerTok, then it should disable slowly - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - } - }) - itChecksPriceChanges('prices change as USD feed price changes', async () => { const oracleError = await collateral.oracleError() const expectedPrice = await getExpectedPrice(ctx) diff --git a/test/plugins/individual-collateral/curve/collateralTests.ts b/test/plugins/individual-collateral/curve/collateralTests.ts index 90285f5f0f..48c28790d6 100644 --- a/test/plugins/individual-collateral/curve/collateralTests.ts +++ b/test/plugins/individual-collateral/curve/collateralTests.ts @@ -68,10 +68,6 @@ const getDescribeFork = (targetNetwork = 'mainnet') => { return useEnv('FORK') && useEnv('FORK_NETWORK') === targetNetwork ? describe : describe.skip } -const getDescribeFork = (targetNetwork = 'mainnet') => { - return useEnv('FORK') && useEnv('FORK_NETWORK') === targetNetwork ? describe : describe.skip -} - export default function fn( fixtures: CurveCollateralTestSuiteFixtures ) { diff --git a/test/plugins/individual-collateral/mountain/USDMCollateral.test.ts b/test/plugins/individual-collateral/mountain/USDMCollateral.test.ts new file mode 100644 index 0000000000..570c60345f --- /dev/null +++ b/test/plugins/individual-collateral/mountain/USDMCollateral.test.ts @@ -0,0 +1,322 @@ +import collateralTests from '../collateralTests' +import { CollateralFixtureContext, CollateralOpts, MintCollateralFunc } from '../pluginTestTypes' +import { resetFork, mintWUSDM, mintUSDM } from './helpers' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { ContractFactory, BigNumberish, BigNumber } from 'ethers' +import { + ERC20Mock, + IERC20Metadata, + MockV3Aggregator, + MockV3Aggregator__factory, + TestICollateral, +} from '../../../../typechain' +import { expectUnpriced, pushOracleForward } from '../../../utils/oracles' +import { bn, fp, toBNDecimals } from '../../../../common/numbers' +import { + BN_SCALE_FACTOR, + ONE_ADDRESS, + ZERO_ADDRESS, + CollateralStatus, +} from '../../../../common/constants' +import { whileImpersonating } from '../../../utils/impersonation' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + ARB_USDM, + ARB_WUSDM, + ARB_WUSDM_USD_PRICE_FEED, + ORACLE_ERROR, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, + MAX_TRADE_VOL, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + ARB_CHRONICLE_FEED_AUTH, +} from './constants' + +/* + Define deployment functions +*/ + +export const defaultUSDMCollateralOpts: CollateralOpts = { + erc20: ARB_WUSDM, + targetName: ethers.utils.formatBytes32String('USD'), + rewardERC20: ZERO_ADDRESS, + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ARB_WUSDM_USD_PRICE_FEED, + oracleTimeout: ORACLE_TIMEOUT, + oracleError: ORACLE_ERROR, + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: fp('0'), +} + +export const deployCollateral = async (opts: CollateralOpts = {}): Promise => { + opts = { ...defaultUSDMCollateralOpts, ...opts } + + const USDMCollateralFactory: ContractFactory = await ethers.getContractFactory('USDMCollateral') + const collateral = await USDMCollateralFactory.deploy( + { + erc20: opts.erc20, + targetName: opts.targetName, + priceTimeout: opts.priceTimeout, + chainlinkFeed: opts.chainlinkFeed, + oracleError: opts.oracleError, + oracleTimeout: opts.oracleTimeout, + maxTradeVolume: opts.maxTradeVolume, + defaultThreshold: opts.defaultThreshold, + delayUntilDefault: opts.delayUntilDefault, + }, + opts.revenueHiding, + { gasLimit: 2000000000 } + ) + await collateral.deployed() + + // It might revert if using real Chronicle oracle and not whitelisted (skip refresh()) + try { + // Push forward 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()) + } catch { + expect(await collateral.chainlinkFeed()).to.equal(ARB_WUSDM_USD_PRICE_FEED) + } + + return collateral +} + +const chainlinkDefaultAnswer = bn('1.03e8') + +type Fixture = () => Promise + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultUSDMCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const chainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, chainlinkDefaultAnswer) + ) + collateralOpts.chainlinkFeed = chainlinkFeed.address + + const wusdm = (await ethers.getContractAt('IERC20Metadata', ARB_WUSDM)) as IERC20Metadata + const rewardToken = (await ethers.getContractAt('ERC20Mock', ZERO_ADDRESS)) as ERC20Mock + const collateral = await deployCollateral(collateralOpts) + const tok = await ethers.getContractAt('IERC20Metadata', await collateral.erc20()) + + return { + alice, + collateral, + chainlinkFeed, + tok, + wusdm, + rewardToken, + } + } + + return makeCollateralFixtureContext +} + +const mintCollateralTo: MintCollateralFunc = async ( + ctx: CollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintWUSDM(ctx.tok, user, amount, recipient) +} + +const reduceTargetPerRef = async (ctx: CollateralFixtureContext, pctDecrease: BigNumberish) => { + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.sub(lastRound.answer.mul(pctDecrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) +} + +const increaseTargetPerRef = async (ctx: CollateralFixtureContext, pctIncrease: BigNumberish) => { + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.add(lastRound.answer.mul(pctIncrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) +} + +// prettier-ignore +const reduceRefPerTok = async ( + ctx: CollateralFixtureContext, + pctDecrease: BigNumberish +) => { + const usdm = await ethers.getContractAt("IERC20Metadata", ARB_USDM) + const currentBal = await usdm.balanceOf(ctx.tok.address) + const removeBal = currentBal.mul(pctDecrease).div(100) + await whileImpersonating(ctx.tok.address, async (wusdmSigner) => { + await usdm.connect(wusdmSigner).transfer(ONE_ADDRESS, removeBal) + }) + + // push chainlink oracle forward so that tryPrice() still works and keeps peg + const latestRoundData = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = latestRoundData.answer.sub(latestRoundData.answer.mul(pctDecrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) +} + +// prettier-ignore +const increaseRefPerTok = async ( + ctx: CollateralFixtureContext, + pctIncrease: BigNumberish + +) => { + + const usdm = await ethers.getContractAt("IERC20Metadata", ARB_USDM) + + const currentBal = await usdm.balanceOf(ctx.tok.address) + const addBal = currentBal.mul(pctIncrease).div(100) + await mintUSDM(usdm, ctx.alice!, addBal, ctx.tok.address) + + // push chainlink oracle forward so that tryPrice() still works and keeps peg + const latestRoundData = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = latestRoundData.answer.add(latestRoundData.answer.mul(pctIncrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) +} + +const getExpectedPrice = async (ctx: CollateralFixtureContext): Promise => { + const clData = await ctx.chainlinkFeed.latestRoundData() + const clDecimals = await ctx.chainlinkFeed.decimals() + + return clData.answer.mul(bn(10).pow(18 - clDecimals)) +} + +/* + Define collateral-specific tests +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificConstructorTests = () => {} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificStatusTests = () => { + it('does revenue hiding correctly', async () => { + const [, alice] = await ethers.getSigners() + const tempCtx = await makeCollateralFixtureContext(alice, { + erc20: ARB_WUSDM, + revenueHiding: fp('0.0101'), + })() + + // Set correct price to maintain peg + const newPrice = fp('1') + .mul(await tempCtx.collateral.underlyingRefPerTok()) + .div(BN_SCALE_FACTOR) + await tempCtx.chainlinkFeed.updateAnswer(toBNDecimals(newPrice, 8)) + await tempCtx.collateral.refresh() + expect(await tempCtx.collateral.status()).to.equal(CollateralStatus.SOUND) + + // Should remain SOUND after a 1% decrease + let refPerTok = await tempCtx.collateral.refPerTok() + await reduceRefPerTok(tempCtx, 1) + await tempCtx.collateral.refresh() + expect(await tempCtx.collateral.status()).to.equal(CollateralStatus.SOUND) + + // refPerTok should be unchanged + expect(await tempCtx.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 tempCtx.collateral.refPerTok() + await reduceRefPerTok(tempCtx, bn(1)) + await tempCtx.collateral.refresh() + expect(await tempCtx.collateral.status()).to.equal(CollateralStatus.DISABLED) + + // refPerTok should have fallen 1% + refPerTok = refPerTok.sub(refPerTok.div(100)) + expect(await tempCtx.collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand + }) + + it('whitelisted Chronicle oracle works correctly', async () => { + await resetFork() // need fresh refPerTok() to maintain peg + + const collateral = await deployCollateral(defaultUSDMCollateralOpts) // using real Chronicle oracle + const chronicleFeed = await ethers.getContractAt('IChronicle', await collateral.chainlinkFeed()) + + // Oracle reverts when attempting to read price from Plugin (specific error - non-empty) + await whileImpersonating(collateral.address, async (pluginSigner) => { + await expect(chronicleFeed.connect(pluginSigner).read()).to.be.revertedWithCustomError( + chronicleFeed, + 'NotTolled' + ) + await expect( + chronicleFeed.connect(pluginSigner).latestRoundData() + ).to.be.revertedWithCustomError(chronicleFeed, 'NotTolled') + }) + + // Plugin is unpriced if not whitelisted + await expectUnpriced(collateral.address) + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + // Refresh sets collateral to IFFY if not whitelisted + await collateral.refresh() + expect(await collateral.status()).to.equal(CollateralStatus.IFFY) + + // Whitelist plugin in Chronicle oracle + await whileImpersonating(ARB_CHRONICLE_FEED_AUTH, async (authSigner) => { + await expect(chronicleFeed.connect(authSigner).kiss(collateral.address)).to.emit( + chronicleFeed, + 'TollGranted' + ) + }) + + // Plugin can now read + await whileImpersonating(collateral.address, async (pluginSigner) => { + await expect(chronicleFeed.connect(pluginSigner).read()).to.not.be.reverted + await expect(chronicleFeed.connect(pluginSigner).latestRoundData()).to.not.be.reverted + }) + + // Should have a price now + const [low, high] = await collateral.price() + expect(low).to.be.closeTo(fp('1.02'), fp('0.01')) // close to $1.03 (chainlink answer in this block) + expect(high).to.be.closeTo(fp('1.04'), fp('0.01')) + expect(high).to.be.gt(low) + + // Refresh sets it back to SOUND now that it's whitelisted + await collateral.refresh() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + }) +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const beforeEachRewardsTest = async () => {} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + beforeEachRewardsTest, + makeCollateralFixtureContext, + mintCollateralTo, + reduceTargetPerRef, + increaseTargetPerRef, + reduceRefPerTok, + increaseRefPerTok, + getExpectedPrice, + itClaimsRewards: it.skip, + itChecksTargetPerRefDefault: it, + itChecksTargetPerRefDefaultUp: it, + itChecksRefPerTokDefault: it, + itChecksPriceChanges: it, + itChecksNonZeroDefaultThreshold: it, + itHasRevenueHiding: it.skip, // implemented in this file + collateralName: 'USDM Collateral', + chainlinkDefaultAnswer, + itIsPricedByPeg: true, + resetFork, + targetNetwork: 'arbitrum', +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/mountain/constants.ts b/test/plugins/individual-collateral/mountain/constants.ts new file mode 100644 index 0000000000..240a6414f9 --- /dev/null +++ b/test/plugins/individual-collateral/mountain/constants.ts @@ -0,0 +1,18 @@ +import { bn, fp } from '../../../../common/numbers' +import { networkConfig } from '../../../../common/configuration' + +// Mainnet Addresses +export const ARB_USDM = networkConfig['42161'].tokens.USDM as string +export const ARB_WUSDM = networkConfig['42161'].tokens.wUSDM as string +export const ARB_WUSDM_USD_PRICE_FEED = networkConfig['42161'].chainlinkFeeds.wUSDM as string +export const ARB_CHRONICLE_FEED_AUTH = '0x39aBD7819E5632Fa06D2ECBba45Dca5c90687EE3' +export const ARB_WUSDM_HOLDER = '0x8c60248a6ca9b6c5620279d40c12eb81e03cd667' +export const ARB_USDM_HOLDER = '0x4bd135524897333bec344e50ddd85126554e58b4' +export const PRICE_TIMEOUT = bn('604800') // 1 week +export const ORACLE_TIMEOUT = bn(86400) // 24 hours in seconds +export const ORACLE_ERROR = fp('0.01') // 1% +export const DEFAULT_THRESHOLD = ORACLE_ERROR.add(fp('0.01')) // 1% + ORACLE_ERROR +export const DELAY_UNTIL_DEFAULT = bn(86400) +export const MAX_TRADE_VOL = bn(1000) + +export const FORK_BLOCK_ARBITRUM = 213549300 diff --git a/test/plugins/individual-collateral/mountain/helpers.ts b/test/plugins/individual-collateral/mountain/helpers.ts new file mode 100644 index 0000000000..bb08c8ae38 --- /dev/null +++ b/test/plugins/individual-collateral/mountain/helpers.ts @@ -0,0 +1,30 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { IERC20Metadata } from '../../../../typechain' +import { whileImpersonating } from '../../../utils/impersonation' +import { BigNumberish } from 'ethers' +import { FORK_BLOCK_ARBITRUM, ARB_WUSDM_HOLDER, ARB_USDM_HOLDER } from './constants' +import { getResetFork } from '../helpers' + +export const mintWUSDM = async ( + wusdm: IERC20Metadata, + account: SignerWithAddress, + amount: BigNumberish, + recipient: string +) => { + await whileImpersonating(ARB_WUSDM_HOLDER, async (whale) => { + await wusdm.connect(whale).transfer(recipient, amount) + }) +} + +export const mintUSDM = async ( + usdm: IERC20Metadata, + account: SignerWithAddress, + amount: BigNumberish, + recipient: string +) => { + await whileImpersonating(ARB_USDM_HOLDER, async (whale) => { + await usdm.connect(whale).transfer(recipient, amount) + }) +} + +export const resetFork = getResetFork(FORK_BLOCK_ARBITRUM) diff --git a/test/plugins/individual-collateral/stargate/StargateUSDCTestSuite.test.ts_DEPRECATED b/test/plugins/individual-collateral/stargate/StargateUSDCTestSuite.test.ts_DEPRECATED index c7dd81588e..009ffdf1a8 100644 --- a/test/plugins/individual-collateral/stargate/StargateUSDCTestSuite.test.ts_DEPRECATED +++ b/test/plugins/individual-collateral/stargate/StargateUSDCTestSuite.test.ts_DEPRECATED @@ -201,10 +201,7 @@ const mintCollateralTo: MintCollateralFunc = a await ctx.pool.connect(router).mint(user.address, amount) }) await ctx.pool.connect(user).approve(ctx.wpool.address, ethers.constants.MaxUint256) -<<<<<<< HEAD:test/plugins/individual-collateral/stargate/StargateUSDCTestSuite.test.ts -======= ->>>>>>> 4.0.0:test/plugins/individual-collateral/stargate/StargateUSDCTestSuite.test.ts_DEPRECATED await ctx.wpool.connect(user).deposit(amount, user.address) await ctx.wpool.connect(user).transfer(recipient, amount) if (ctx.pool.address != SUSDC) { diff --git a/test/registries/AssetPluginRegistry.test.ts b/test/registries/AssetPluginRegistry.test.ts index 0c727afb9e..2b3287c550 100644 --- a/test/registries/AssetPluginRegistry.test.ts +++ b/test/registries/AssetPluginRegistry.test.ts @@ -4,7 +4,13 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' import { ZERO_ADDRESS } from '#/common/constants' import { Collateral, Implementation, IMPLEMENTATION, defaultFixture } from '../fixtures' -import { AssetPluginRegistry, TestIDeployer, VersionRegistry, DeployerMock } from '../../typechain' +import { + AssetPluginRegistry, + RoleRegistry, + TestIDeployer, + VersionRegistry, + DeployerMock, +} from '../../typechain' const describeP1 = IMPLEMENTATION == Implementation.P1 ? describe : describe.skip @@ -25,6 +31,7 @@ describeP1('Asset Plugin Registry', () => { // Registries let versionRegistry: VersionRegistry let assetPluginRegistry: AssetPluginRegistry + let roleRegistry: RoleRegistry beforeEach(async () => { ;[owner, other] = await ethers.getSigners() @@ -32,8 +39,11 @@ describeP1('Asset Plugin Registry', () => { // Deploy fixture ;({ deployer, basket } = await loadFixture(defaultFixture)) + const RoleRegistryFactory = await ethers.getContractFactory('RoleRegistry') + roleRegistry = await RoleRegistryFactory.connect(owner).deploy() + const versionRegistryFactory = await ethers.getContractFactory('VersionRegistry') - versionRegistry = await versionRegistryFactory.deploy(await owner.getAddress()) + versionRegistry = await versionRegistryFactory.deploy(roleRegistry.address) const assetPluginRegistryFactory = await ethers.getContractFactory('AssetPluginRegistry') assetPluginRegistry = await assetPluginRegistryFactory.deploy(versionRegistry.address) @@ -50,7 +60,7 @@ describeP1('Asset Plugin Registry', () => { describe('Deployment', () => { it('should set the owner/version registry correctly', async () => { - expect(await assetPluginRegistry.owner()).to.eq(await owner.getAddress()) + expect(await assetPluginRegistry.roleRegistry()).to.eq(roleRegistry.address) expect(await assetPluginRegistry.versionRegistry()).to.eq(versionRegistry.address) }) }) @@ -130,7 +140,7 @@ describeP1('Asset Plugin Registry', () => { // If not owner cannot register asset await expect( assetPluginRegistry.connect(other).registerAsset(tokenAsset.address, [versionHash]) - ).to.be.revertedWith('Ownable: caller is not the owner') + ).to.be.revertedWithCustomError(assetPluginRegistry, 'AssetPluginRegistry__InvalidCaller') // Invalid registration with zero address is also rejected await expect( @@ -260,7 +270,7 @@ describeP1('Asset Plugin Registry', () => { assetPluginRegistry .connect(other) .updateVersionsByAsset(tokenAsset.address, [versionHash], [true]) - ).to.be.revertedWith('Ownable: caller is not the owner') + ).to.be.revertedWithCustomError(assetPluginRegistry, 'AssetPluginRegistry__InvalidCaller') // Fails if any of the versions is not registered const versionV1Hash = ethers.utils.keccak256( @@ -385,7 +395,7 @@ describeP1('Asset Plugin Registry', () => { assetPluginRegistry .connect(other) .updateAssetsByVersion(versionHash, [tokenAsset.address], [true]) - ).to.be.revertedWith('Ownable: caller is not the owner') + ).to.be.revertedWithCustomError(assetPluginRegistry, 'AssetPluginRegistry__InvalidCaller') // Fails if any of the assets is zero address await expect( diff --git a/test/registries/DAOFeeRegistry.test.ts b/test/registries/DAOFeeRegistry.test.ts index 4a75c0c77c..4c67a2e361 100644 --- a/test/registries/DAOFeeRegistry.test.ts +++ b/test/registries/DAOFeeRegistry.test.ts @@ -9,6 +9,7 @@ import { whileImpersonating } from '../utils/impersonation' import { DAOFeeRegistry, ERC20Mock, + RoleRegistry, TestIDistributor, TestIRevenueTrader, TestIMain, @@ -28,6 +29,7 @@ describeP1('DAO Fee Registry', () => { let rsrTrader: TestIRevenueTrader let feeRegistry: DAOFeeRegistry + let roleRegistry: RoleRegistry beforeEach(async () => { ;[owner, other] = await ethers.getSigners() @@ -35,15 +37,19 @@ describeP1('DAO Fee Registry', () => { // Deploy fixture ;({ distributor, main, rToken, rsr, rsrTrader } = await loadFixture(defaultFixture)) + const RoleRegistryFactory = await ethers.getContractFactory('RoleRegistry') + roleRegistry = await RoleRegistryFactory.connect(owner).deploy() + const DAOFeeRegistryFactory = await ethers.getContractFactory('DAOFeeRegistry') - feeRegistry = await DAOFeeRegistryFactory.connect(owner).deploy(await owner.getAddress()) + feeRegistry = await DAOFeeRegistryFactory.connect(owner).deploy( + roleRegistry.address, + await owner.getAddress() + ) + await main.connect(owner).setDAOFeeRegistry(feeRegistry.address) }) describe('Deployment', () => { - it('should set the owner correctly', async () => { - expect(await feeRegistry.owner()).to.eq(await owner.getAddress()) - }) it('fee should begin zero and assigned to owner', async () => { const feeDetails = await feeRegistry.getFeeDetails(rToken.address) expect(feeDetails.recipient).to.equal(owner.address) @@ -52,37 +58,20 @@ describeP1('DAO Fee Registry', () => { }) }) - describe('Ownership', () => { - it('Should be able to change owner', async () => { - expect(await feeRegistry.owner()).to.eq(await owner.getAddress()) - await feeRegistry.connect(owner).transferOwnership(other.address) - expect(await feeRegistry.owner()).to.eq(await other.getAddress()) - await expect(feeRegistry.connect(owner).setFeeRecipient(owner.address)).to.be.revertedWith( - 'Ownable: caller is not the owner' - ) - await expect(feeRegistry.connect(owner).setDefaultFeeNumerator(bn('100'))).to.be.revertedWith( - 'Ownable: caller is not the owner' - ) - await expect( - feeRegistry.connect(owner).setRTokenFeeNumerator(rToken.address, bn('100')) - ).to.be.revertedWith('Ownable: caller is not the owner') - }) - }) - describe('Negative cases', () => { it('Should not allow calling setters by anyone other than owner', async () => { - await expect(feeRegistry.connect(other).setFeeRecipient(owner.address)).to.be.revertedWith( - 'Ownable: caller is not the owner' - ) - await expect(feeRegistry.connect(other).setDefaultFeeNumerator(bn('100'))).to.be.revertedWith( - 'Ownable: caller is not the owner' - ) + await expect( + feeRegistry.connect(other).setFeeRecipient(owner.address) + ).to.be.revertedWithCustomError(feeRegistry, 'DAOFeeRegistry__InvalidCaller') + await expect( + feeRegistry.connect(other).setDefaultFeeNumerator(bn('100')) + ).to.be.revertedWithCustomError(feeRegistry, 'DAOFeeRegistry__InvalidCaller') await expect( feeRegistry.connect(other).setRTokenFeeNumerator(rToken.address, bn('100')) - ).to.be.revertedWith('Ownable: caller is not the owner') - await expect(feeRegistry.connect(other).resetRTokenFee(rToken.address)).to.be.revertedWith( - 'Ownable: caller is not the owner' - ) + ).to.be.revertedWithCustomError(feeRegistry, 'DAOFeeRegistry__InvalidCaller') + await expect( + feeRegistry.connect(other).resetRTokenFee(rToken.address) + ).to.be.revertedWithCustomError(feeRegistry, 'DAOFeeRegistry__InvalidCaller') }) it('Should not allow setting fee recipient to zero address', async () => { @@ -205,9 +194,9 @@ describeP1('DAO Fee Registry', () => { expect(await rsr.balanceOf(owner.address)).to.equal(0) await distributor.connect(signer).distribute(rsr.address, amt) - // Expected returned amount is for the fee times 5/3 to account for rev share split - const expectedAmt = amt.mul(defaultFee).div(bn('1e4')).mul(5).div(3) - expect(await rsr.balanceOf(owner.address)).to.equal(expectedAmt) + const feeShares = bn('1e4').mul(defaultFee).div(bn('1e4').sub(defaultFee)) + const expectedAmt = amt.mul(feeShares).div(feeShares.add(6000)) + expect(await rsr.balanceOf(owner.address)).to.be.closeTo(expectedAmt, 10000) }) }) } diff --git a/test/registries/VersionRegistry.test.ts b/test/registries/VersionRegistry.test.ts index 9edfc493d7..789a3a3c1b 100644 --- a/test/registries/VersionRegistry.test.ts +++ b/test/registries/VersionRegistry.test.ts @@ -4,12 +4,13 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' import { ONE_ADDRESS, ZERO_ADDRESS, ZERO_BYTES } from '#/common/constants' import { IImplementations } from '#/common/configuration' -import { DeployerMock, TestIDeployer, VersionRegistry } from '../../typechain' +import { DeployerMock, RoleRegistry, TestIDeployer, VersionRegistry } from '../../typechain' import { Implementation, IMPLEMENTATION, defaultFixture } from '../fixtures' const describeP1 = IMPLEMENTATION == Implementation.P1 ? describe : describe.skip describeP1('Version Registry', () => { + let roleRegistry: RoleRegistry let versionRegistry: VersionRegistry let deployer: TestIDeployer let deployerMockV1: DeployerMock @@ -21,8 +22,11 @@ describeP1('Version Registry', () => { ;[owner, other] = await ethers.getSigners() ;({ deployer } = await loadFixture(defaultFixture)) + const RoleRegistryFactory = await ethers.getContractFactory('RoleRegistry') + roleRegistry = await RoleRegistryFactory.connect(owner).deploy() + const versionRegistryFactory = await ethers.getContractFactory('VersionRegistry') - versionRegistry = await versionRegistryFactory.deploy(await owner.getAddress()) + versionRegistry = await versionRegistryFactory.deploy(roleRegistry.address) const DeployerMockFactoryV1 = await ethers.getContractFactory('DeployerMock') deployerMockV1 = await DeployerMockFactoryV1.deploy() @@ -32,8 +36,8 @@ describeP1('Version Registry', () => { }) describe('Deployment', () => { - it('should set the owner to the specified address', async () => { - expect(await versionRegistry.owner()).to.eq(await owner.getAddress()) + it('should set the role registry to the specified address', async () => { + expect(await versionRegistry.roleRegistry()).to.eq(roleRegistry.address) }) }) @@ -48,7 +52,9 @@ describeP1('Version Registry', () => { expect(versionData.versionHash).not.be.eq(ZERO_BYTES) expect(versionData.deprecated).be.eq(false) - const expectedVersionHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes('V1')) + const expectedVersionHash = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes(await deployerMockV1.version()) + ) expect(versionData.versionHash).to.eq(expectedVersionHash) expect(await versionRegistry.deployments(expectedVersionHash)).to.not.equal(ZERO_ADDRESS) expect(await versionRegistry.deployments(expectedVersionHash)).to.equal( @@ -60,7 +66,7 @@ describeP1('Version Registry', () => { // If not owner, should be rejected await expect( versionRegistry.connect(other).registerVersion(deployer.address) - ).to.be.revertedWith('Ownable: caller is not the owner') + ).to.be.revertedWithCustomError(versionRegistry, 'VersionRegistry__InvalidCaller') // Same version, different deployer, should be rejected. const DeployerMockFactory = await ethers.getContractFactory('DeployerMock') @@ -79,7 +85,9 @@ describeP1('Version Registry', () => { const initialVersionData = await versionRegistry.getLatestVersion() // Register new version - const expectedV2Hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes('V2')) + const expectedV2Hash = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes(await deployerMockV2.version()) + ) await expect(versionRegistry.connect(owner).registerVersion(deployerMockV2.address)) .to.emit(versionRegistry, 'VersionRegistered') .withArgs(expectedV2Hash, deployerMockV2.address) diff --git a/test/scenario/BadCollateralPlugin.test.ts b/test/scenario/BadCollateralPlugin.test.ts index ec2e04c0ee..17a043be1c 100644 --- a/test/scenario/BadCollateralPlugin.test.ts +++ b/test/scenario/BadCollateralPlugin.test.ts @@ -222,8 +222,8 @@ describe(`Bad Collateral Plugin - P${IMPLEMENTATION}`, () => { const unslippedPrice = fp('1.1') const lowSellPrice = fp('1').sub(fp('1').mul(ORACLE_ERROR).div(fp('1'))) const highBuyPrice = fp('1').add(fp('1').mul(ORACLE_ERROR).div(fp('1'))) - const worstCasePrice = divCeil(unslippedPrice.mul(lowSellPrice), highBuyPrice).sub(1) - expect(await trade.worstCasePrice()).to.equal(worstCasePrice) + const worstCasePrice = divCeil(unslippedPrice.mul(bn('1e9')).mul(lowSellPrice), highBuyPrice) // D27 + expect(await trade.worstCasePrice()).to.be.closeTo(worstCasePrice, bn('1e9')) }) }) diff --git a/test/scenario/ComplexBasket.test.ts b/test/scenario/ComplexBasket.test.ts index f3924355e7..450cdb9460 100644 --- a/test/scenario/ComplexBasket.test.ts +++ b/test/scenario/ComplexBasket.test.ts @@ -1577,8 +1577,8 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { // Running auctions will trigger recollateralization - cETH partial sale for weth // Will sell about 841K of cETH, expect to receive 8167 wETH (minimum) // We would still have about 438K to sell of cETH - let [low] = await cETHCollateral.price() - const sellAmtUnscaled = MAX_TRADE_VOLUME.mul(BN_SCALE_FACTOR).div(low) + let [, high] = await cETHCollateral.price() + const sellAmtUnscaled = MAX_TRADE_VOLUME.mul(BN_SCALE_FACTOR).div(high) const sellAmt = toBNDecimals(sellAmtUnscaled, 8) const sellAmtRemainder = (await cETH.balanceOf(backingManager.address)).sub(sellAmt) // Price for cETH = 1200 / 50 = $24 at rate 50% = $12 @@ -1723,8 +1723,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 - ;[low] = await rsrAsset.price() - const sellAmtRSR1 = MAX_TRADE_VOLUME.mul(BN_SCALE_FACTOR).div(low) + ;[, high] = await rsrAsset.price() + const sellAmtRSR1 = MAX_TRADE_VOLUME.mul(BN_SCALE_FACTOR).div(high) const buyAmtBidRSR1 = toMinBuyAmt( sellAmtRSR1, rsrPrice, diff --git a/test/scenario/LargeDecimals.test.ts b/test/scenario/LargeDecimals.test.ts new file mode 100644 index 0000000000..c3900bfeb7 --- /dev/null +++ b/test/scenario/LargeDecimals.test.ts @@ -0,0 +1,791 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +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 { IConfig } from '../../common/configuration' +import { bn, fp, pow10, toBNDecimals } from '../../common/numbers' +import { + Asset, + ERC20Mock, + FacadeTest, + GnosisMock, + IAssetRegistry, + MockV3Aggregator, + RTokenAsset, + TestIBackingManager, + TestIBasketHandler, + TestIFacade, + TestIRevenueTrader, + TestIRToken, + TestIFurnace, + TestIStRSR, + TestIDistributor, + AppreciatingMockDecimals, + AppreciatingMockDecimalsCollateral, + ERC20MockDecimals, +} from '../../typechain' +import { advanceTime, getLatestBlockTimestamp } from '../utils/time' +import { + defaultFixtureNoBasket, + IMPLEMENTATION, + ORACLE_ERROR, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, + REVENUE_HIDING, +} from '../fixtures' +import { BN_SCALE_FACTOR, CollateralStatus, FURNACE_DEST, STRSR_DEST } from '../../common/constants' +import { expectTrade, getTrade, toMinBuyAmt } from '../utils/trades' +import { expectPrice, expectRTokenPrice, setOraclePrice } from '../utils/oracles' +import { expectEvents } from '../../common/events' + +const DEFAULT_THRESHOLD = fp('0.01') // 1% +const DELAY_UNTIL_DEFAULT = bn('86400') // 24h +const MAX_TRADE_VOLUME = fp('1e7') // $10M + +const point5Pct = (value: BigNumber): BigNumber => { + return value.mul(5).div(1000) +} + +describe(`Large Decimals Basket - P${IMPLEMENTATION}`, () => { + let owner: SignerWithAddress + let addr1: SignerWithAddress + + // Non-backing assets + let rsr: ERC20Mock + let underlying: ERC20MockDecimals + let rewardToken: ERC20MockDecimals + let token: AppreciatingMockDecimals + + let erc20s: ERC20Mock[] + + let rsrAsset: Asset + let rewardAsset: Asset + let rTokenAsset: RTokenAsset + + // Trading + let gnosis: GnosisMock + let rsrTrader: TestIRevenueTrader + let rTokenTrader: TestIRevenueTrader + + // Tokens and Assets + let initialBal: BigNumber + let rewardAmount: BigNumber + + let collateral: AppreciatingMockDecimalsCollateral + let targetAmt: BigNumber + + let usdToken: ERC20Mock + + // Config values + let config: IConfig + + // Contracts to retrieve after deploy + let distributor: TestIDistributor + let furnace: TestIFurnace + let rToken: TestIRToken + let stRSR: TestIStRSR + let assetRegistry: IAssetRegistry + let basketHandler: TestIBasketHandler + let facade: TestIFacade + let facadeTest: FacadeTest + let backingManager: TestIBackingManager + + // Perform tests for each of these decimal variations (> 18) + const optDecimals = [21, 27] + optDecimals.forEach((decimals) => { + describe(`With decimals: ${decimals}`, () => { + beforeEach(async () => { + ;[owner, addr1] = await ethers.getSigners() + + // Deploy fixture + ;({ + erc20s, + config, + rToken, + assetRegistry, + backingManager, + basketHandler, + facade, + facadeTest, + rsr, + rsrAsset, + furnace, + distributor, + stRSR, + rTokenTrader, + rsrTrader, + gnosis, + rTokenAsset, + } = await loadFixture(defaultFixtureNoBasket)) + + // Mint initial balances + initialBal = bn('100000000').mul(pow10(decimals)) + + usdToken = erc20s[0] // DAI Token + + // Setup Factories + const ERC20MockDecimalsFactory: ContractFactory = await ethers.getContractFactory( + 'ERC20MockDecimals' + ) + const AppreciatingMockDecimalsFactory: ContractFactory = await ethers.getContractFactory( + 'AppreciatingMockDecimals' + ) + const AppreciatingMockDecimalsCollateralFactory: ContractFactory = + await ethers.getContractFactory('AppreciatingMockDecimalsCollateral') + const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( + 'MockV3Aggregator' + ) + + // Replace RSRAsset + const AssetFactory = await ethers.getContractFactory('Asset') + const newRSRAsset: Asset = ( + await AssetFactory.deploy( + PRICE_TIMEOUT, + await rsrAsset.chainlinkFeed(), + ORACLE_ERROR, + rsr.address, + MAX_TRADE_VOLUME, + ORACLE_TIMEOUT + ) + ) + await assetRegistry.connect(owner).swapRegistered(newRSRAsset.address) + rsrAsset = newRSRAsset + + // Setup reward asset + rewardToken = ( + await ERC20MockDecimalsFactory.deploy( + `Reward Token ${decimals}`, + `REWARD_TKN${decimals}`, + decimals + ) + ) + const rewardChainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, bn('1e8')) + ) + rewardAsset = ( + await AssetFactory.deploy( + PRICE_TIMEOUT, + rewardChainlinkFeed.address, + ORACLE_ERROR, + rewardToken.address, + MAX_TRADE_VOLUME, + ORACLE_TIMEOUT + ) + ) + await assetRegistry.connect(owner).register(rewardAsset.address) + + /***** Setup Basket, Appreciating collateral with large decimals ***********/ + underlying = ( + await ERC20MockDecimalsFactory.deploy(`Token ${decimals}`, `TKN${decimals}`, decimals) + ) + + token = ( + await AppreciatingMockDecimalsFactory.deploy( + `AppreciatingToken_${decimals}`, + `AppreciatingToken_SYM_:${decimals}`, + decimals, + underlying.address + ) + ) + await token.setExchangeRate(fp('1')) + await token.setRewardToken(rewardToken.address) + + const chainlinkFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + collateral = ( + await AppreciatingMockDecimalsCollateralFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + erc20: token.address, + maxTradeVolume: MAX_TRADE_VOLUME, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + REVENUE_HIDING + ) + ) + await assetRegistry.connect(owner).register(collateral.address) + + targetAmt = fp('1') + + // Set basket + await basketHandler.setPrimeBasket([token.address], [targetAmt]) + await basketHandler.connect(owner).refreshBasket() + + // Advance time post warmup period + await advanceTime(Number(config.warmupPeriod) + 1) + + // Mint and approve initial balances + await token.mint(addr1.address, initialBal) + await token.connect(addr1).approve(rToken.address, initialBal) + + // Mint backup token + await usdToken.mint(addr1.address, initialBal) + + // Grant allowances + await backingManager.grantRTokenAllowance(token.address) + }) + + it('Should Issue/Redeem correctly', async () => { + // Basket + expect(await basketHandler.fullyCollateralized()).to.equal(true) + + // Check other values + expect(await basketHandler.timestamp()).to.be.gt(bn(0)) + expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.equal(0) + await expectPrice(basketHandler.address, fp('1'), ORACLE_ERROR, true) + + const issueAmt = bn('10e18') + + // Get quotes + const [, quotes] = await facade.connect(addr1).callStatic.issue(rToken.address, issueAmt) + + // Issue + await rToken.connect(addr1).issue(issueAmt) + expect(await rToken.balanceOf(addr1.address)).to.equal(issueAmt) + expect(await rToken.totalSupply()).to.equal(issueAmt) + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( + issueAmt, + fp('0.5') + ) + + await expectPrice(basketHandler.address, fp('1'), ORACLE_ERROR, true) + + // Set expected quotes + const expectedTkn: BigNumber = toBNDecimals( + issueAmt.mul(targetAmt).div(await collateral.refPerTok()), + decimals + ) + + // Check balances + expect(await token.balanceOf(backingManager.address)).to.equal(expectedTkn) + expect(await token.balanceOf(addr1.address)).to.equal(initialBal.sub(expectedTkn)) + expect(expectedTkn).to.equal(quotes[0]) + + // Redeem + await rToken.connect(addr1).redeem(issueAmt) + expect(await rToken.balanceOf(addr1.address)).to.equal(0) + expect(await rToken.totalSupply()).to.equal(0) + + // Check balances - Back to initial status + expect(await token.balanceOf(backingManager.address)).to.equal(0) + expect(await token.balanceOf(addr1.address)).to.equal(initialBal) + }) + + it('Should claim rewards correctly - All RSR', async () => { + // Set RSR price + const rsrPrice = fp('0.005') // 0.005 usd + await setOraclePrice(rsrAsset.address, toBNDecimals(rsrPrice, 8)) + + // Set reward token price + const rewardPrice = fp('50') // 50 usd + await setOraclePrice(rewardAsset.address, toBNDecimals(rewardPrice, 8)) + + // Set Reward amount = approx 5 usd + rewardAmount = bn('1').mul(pow10(decimals - 1)) + + // Mint some RSR (arbitrary) + await rsr.connect(owner).mint(addr1.address, initialBal) + + // Set f=1 // All revenues to RSR + await expect( + distributor + .connect(owner) + .setDistribution(STRSR_DEST, { rTokenDist: bn(0), rsrDist: bn(10000) }) + ) + .to.emit(distributor, 'DistributionSet') + .withArgs(STRSR_DEST, bn(0), bn(10000)) + + await expect( + distributor + .connect(owner) + .setDistribution(FURNACE_DEST, { rTokenDist: bn(0), rsrDist: bn(0) }) + ) + .to.emit(distributor, 'DistributionSet') + .withArgs(FURNACE_DEST, bn(0), bn(0)) + + // Set Rewards + await token.setRewards(backingManager.address, rewardAmount) + + // Collect revenue - Called via poke + const sellAmt: BigNumber = rewardAmount // all will be sold + const minBuyAmt = toMinBuyAmt( + sellAmt.div(pow10(decimals - 18)), // scale to 18 decimals (to obtain RSR amount) + rewardPrice, + rsrPrice, + ORACLE_ERROR, + config.maxTradeSlippage + ) + + await expectEvents(backingManager.claimRewards(), [ + { + contract: backingManager, + name: 'RewardsClaimed', + args: [rewardToken.address, rewardAmount], + 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) + + // Run auctions + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: rsrTrader, + name: 'TradeStarted', + args: [anyValue, rewardToken.address, rsr.address, sellAmt, minBuyAmt], + emitted: true, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + emitted: false, + }, + ]) + + const auctionTimestamp: number = await getLatestBlockTimestamp() + + // Check auctions registered + // RewardToken -> RSR Auction + await expectTrade(rsrTrader, { + sell: rewardToken.address, + buy: rsr.address, + endTime: auctionTimestamp + Number(config.batchAuctionLength), + externalId: bn('0'), + }) + + // Check funds in Market + expect(await rewardToken.balanceOf(gnosis.address)).to.equal(rewardAmount) + + // 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 gnosis.placeBid(0, { + bidder: addr1.address, + sellAmount: sellAmt, + buyAmount: minBuyAmt, + }) + + // Close auctions + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: rsrTrader, + name: 'TradeSettled', + args: [anyValue, rewardToken.address, rsr.address, sellAmt, minBuyAmt], + emitted: true, + }, + { + contract: rsrTrader, + name: 'TradeStarted', + emitted: false, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + emitted: false, + }, + ]) + + // Check balances sent to corresponding destinations + // StRSR + expect(await rsr.balanceOf(stRSR.address)).to.be.closeTo(minBuyAmt, 10000) + // Furnace + expect(await rToken.balanceOf(furnace.address)).to.equal(0) + }) + + it('Should sell collateral as it appreciates and handle revenue auction correctly', async () => { + // Set RSR price + const rsrPrice = fp('0.005') // 0.005 usd + await setOraclePrice(rsrAsset.address, toBNDecimals(rsrPrice, 8)) + + // Mint some RSR (arbitrary) + await rsr.connect(owner).mint(addr1.address, initialBal) + + // Issue 1 RToken + const issueAmount = bn('1e18') + + // Get quotes for RToken + const [, quotes] = await facade.connect(addr1).callStatic.issue(rToken.address, issueAmount) + + // Issue 1 RToken + await rToken.connect(addr1).issue(issueAmount) + + const origAssetValue = issueAmount + await expectRTokenPrice( + rTokenAsset.address, + fp('1'), + ORACLE_ERROR, + await backingManager.maxTradeSlippage(), + config.minTradeVolume.mul((await assetRegistry.erc20s()).length) + ) + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( + origAssetValue, + fp('0.5') + ) + expect(await rToken.balanceOf(addr1.address)).to.equal(issueAmount) + expect(await rToken.totalSupply()).to.equal(issueAmount) + + // Increase redemption rate to double + await token.setExchangeRate(fp('2')) + + // Get updated quotes + const [, newQuotes] = await facade + .connect(addr1) + .callStatic.issue(rToken.address, issueAmount) + + await assetRegistry.refresh() // refresh to update refPerTok() + const expectedNewTkn: BigNumber = toBNDecimals( + issueAmount.mul(targetAmt).div(await collateral.refPerTok()), + decimals + ) + + expect(expectedNewTkn).to.equal(newQuotes[0]) + expect(newQuotes[0]).to.equal(quotes[0].div(2)) // requires half the tokens now + + // Check Price and assets value + // Excess token = 0.5 tok (50% of issued amount) + const excessQuantity: BigNumber = quotes[0].sub(newQuotes[0]) + const excessQuantity18: BigNumber = excessQuantity.div(pow10(decimals - 18)) + + const [lowPrice, highPrice] = await collateral.price() + const excessValueLow: BigNumber = excessQuantity18.mul(lowPrice).div(BN_SCALE_FACTOR) + const excessValueHigh: BigNumber = excessQuantity18.mul(highPrice).div(BN_SCALE_FACTOR) + + expect(excessQuantity).to.equal(toBNDecimals(issueAmount.div(2), decimals)) + await expectPrice(collateral.address, fp('2'), ORACLE_ERROR, true) // price doubled + expect(excessValueLow).to.be.lt(fp('1')) + expect(excessValueHigh).to.be.gt(fp('1')) + + // RToken price remains the same + await expectRTokenPrice( + rTokenAsset.address, + fp('1'), + ORACLE_ERROR, + await backingManager.maxTradeSlippage(), + config.minTradeVolume.mul((await assetRegistry.erc20s()).length) + ) + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.lt( + origAssetValue.add(excessValueHigh) + ) + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.gt( + origAssetValue.add(excessValueLow) + ) + + // 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) + + // Expected values + const currentTotalSupply: BigNumber = await rToken.totalSupply() + + // Excess Token will be minted into a full RToken + const excessInRToken = issueAmount + const expectedToTrader = excessInRToken.mul(60).div(100) // 60% of 1 RToken (0.6 RTokens) + const expectedToFurnace = excessInRToken.sub(expectedToTrader) // Remainder (0.4 RTokens) + expect(expectedToTrader).to.equal(fp('0.6')) + expect(expectedToFurnace).to.equal(fp('0.4')) + + // Set expected values for first auction + const sellAmt: BigNumber = expectedToTrader // everything is auctioned, below max auction + const minBuyAmt = toMinBuyAmt( + sellAmt, + fp('1'), + rsrPrice, + ORACLE_ERROR, + config.maxTradeSlippage + ) + + await expectRTokenPrice( + rTokenAsset.address, + fp('1'), + ORACLE_ERROR, + await backingManager.maxTradeSlippage(), + config.minTradeVolume.mul((await assetRegistry.erc20s()).length) + ) + + // Run auctions - Will detect excess (all will be minted in RToken, so no RToken auction) + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: rsrTrader, + name: 'TradeStarted', + emitted: true, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + emitted: false, + }, + ]) + + const auctionTimestamp: number = await getLatestBlockTimestamp() + + // Check auction registered + // RToken -> RSR Auction + await expectTrade(rsrTrader, { + sell: rToken.address, + buy: rsr.address, + endTime: auctionTimestamp + Number(config.batchAuctionLength), + externalId: bn('0'), + }) + + // Check trades + const trade = await getTrade(rsrTrader, rToken.address) + const auctionId = await trade.auctionId() + const [, , , auctionSellAmt, auctionbuyAmt] = await gnosis.auctions(auctionId) + expect(sellAmt).to.be.closeTo(auctionSellAmt, point5Pct(auctionSellAmt)) + expect(minBuyAmt).to.be.closeTo(auctionbuyAmt, point5Pct(auctionbuyAmt)) + + // Check Price (unchanged) and Assets value + await expectRTokenPrice( + rTokenAsset.address, + fp('1'), + ORACLE_ERROR, + await backingManager.maxTradeSlippage(), + config.minTradeVolume.mul((await assetRegistry.erc20s()).length) + ) + + // Value of backing doubled + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( + origAssetValue.mul(2), + point5Pct(origAssetValue.mul(2)) + ) + + // Supply now doubled + expect(await rToken.totalSupply()).to.be.closeTo( + currentTotalSupply.mul(2), + point5Pct(currentTotalSupply.mul(2)) + ) + + // Check destinations at this stage (RToken already sent to furnace) + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rToken.balanceOf(furnace.address)).to.be.closeTo( + expectedToFurnace, + point5Pct(expectedToFurnace) + ) + + // Check funds in Market and Traders + expect(await rToken.balanceOf(gnosis.address)).to.be.closeTo(sellAmt, point5Pct(sellAmt)) + + expect(await rToken.balanceOf(rsrTrader.address)).to.equal(bn(0)) + expect(await rToken.balanceOf(rTokenTrader.address)).to.equal(bn(0)) + + // Advance time till auction ended + await advanceTime(config.batchAuctionLength.add(100).toString()) + + // Mock auctions + await rsr.connect(addr1).approve(gnosis.address, auctionbuyAmt) + await gnosis.placeBid(0, { + bidder: addr1.address, + sellAmount: auctionSellAmt, + buyAmount: auctionbuyAmt, + }) + + // Close auctions - Will not open another auction + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: rsrTrader, + name: 'TradeSettled', + args: [anyValue, rToken.address, rsr.address, auctionSellAmt, auctionbuyAmt], + emitted: true, + }, + { + contract: rTokenTrader, + name: 'TradeSettled', + emitted: false, + }, + { + contract: rsrTrader, + name: 'TradeStarted', + emitted: false, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + emitted: false, + }, + ]) + + // Check Price (unchanged) and Assets value (unchanged) + await expectRTokenPrice( + rTokenAsset.address, + fp('1'), + ORACLE_ERROR, + await backingManager.maxTradeSlippage(), + config.minTradeVolume.mul((await assetRegistry.erc20s()).length) + ) + + expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( + origAssetValue.mul(2), + point5Pct(origAssetValue.mul(2)) + ) + + expect(await rToken.totalSupply()).to.be.closeTo( + currentTotalSupply.mul(2), + point5Pct(currentTotalSupply.mul(2)) + ) + + // Check destinations at this stage - RSR and RTokens already in StRSR and Furnace + expect(await rsr.balanceOf(stRSR.address)).to.be.closeTo( + auctionbuyAmt, + point5Pct(auctionbuyAmt) + ) + expect(await rToken.balanceOf(furnace.address)).to.be.closeTo( + expectedToFurnace, + point5Pct(expectedToFurnace) + ) + + // Check no more funds in Market and Traders + expect(await rToken.balanceOf(gnosis.address)).to.equal(0) + expect(await rToken.balanceOf(rsrTrader.address)).to.equal(0) + expect(await rToken.balanceOf(rTokenTrader.address)).to.equal(0) + }) + + it('Should recollateralize basket correctly', async () => { + // Set RSR price to 25 cts for less auctions + const rsrPrice = fp('0.25') // 0.25 usd + await setOraclePrice(rsrAsset.address, toBNDecimals(rsrPrice, 8)) + + // Stake some RSR + await rsr.connect(owner).mint(addr1.address, initialBal) + await rsr.connect(addr1).approve(stRSR.address, initialBal) + await stRSR.connect(addr1).stake(initialBal) + + // Issue + const issueAmount = bn('1e18') + + await rToken.connect(addr1).issue(issueAmount) + + expect(await basketHandler.fullyCollateralized()).to.equal(true) + + // Get quotes for RToken + const [, quotes] = await facade.connect(addr1).callStatic.issue(rToken.address, issueAmount) + const expectedTkn: BigNumber = toBNDecimals( + issueAmount.mul(targetAmt).div(await collateral.refPerTok()), + decimals + ) + + expect(quotes[0]).to.equal(toBNDecimals(fp('1'), decimals)) + expect(expectedTkn).to.equal(quotes[0]) + + // Set Backup to DAI + await basketHandler + .connect(owner) + .setBackupConfig(ethers.utils.formatBytes32String('USD'), bn(1), [usdToken.address]) + + // Basket Swapping - Default token - should be replaced by DAI + // Decrease rate to cause default + await token.setExchangeRate(fp('0.8')) + + // Mark Collateral as Defaulted + await collateral.refresh() + + expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) + + // Ensure valid basket + await basketHandler.refreshBasket() + + // Advance time post warmup period + await advanceTime(Number(config.warmupPeriod) + 1) + + const [, newQuotes] = await facade + .connect(addr1) + .callStatic.issue(rToken.address, issueAmount) + expect(newQuotes[0]).to.equal(fp('1')) + + // Check new basket status + expect(await basketHandler.fullyCollateralized()).to.equal(false) + expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) + + // Running auctions will trigger recollateralization - All balance of invalid tokens will be redeemed + const sellAmt: BigNumber = await token.balanceOf(backingManager.address) + const minBuyAmt = toMinBuyAmt( + sellAmt.div(pow10(decimals - 18)), // scale down to 18 decimals + fp('0.8'), // decrease 20% + fp('1'), + ORACLE_ERROR, + config.maxTradeSlippage + ) + + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: backingManager, + name: 'TradeStarted', + args: [anyValue, token.address, usdToken.address, sellAmt, minBuyAmt], + emitted: true, + }, + ]) + + const auctionTimestamp = await getLatestBlockTimestamp() + + // Token (Defaulted) -> DAI (only valid backup token for that target) + await expectTrade(backingManager, { + sell: token.address, + buy: usdToken.address, + endTime: auctionTimestamp + Number(config.batchAuctionLength), + externalId: bn('0'), + }) + + // Check trade + const trade = await getTrade(backingManager, token.address) + const auctionId = await trade.auctionId() + const [, , , auctionSellAmt] = await gnosis.auctions(auctionId) + expect(sellAmt).to.be.closeTo(auctionSellAmt, point5Pct(auctionSellAmt)) + + // Check funds in Market and Traders + expect(await token.balanceOf(gnosis.address)).to.be.closeTo(sellAmt, point5Pct(sellAmt)) + expect(await token.balanceOf(backingManager.address)).to.equal(bn(0)) + + // Advance time till auction ended + await advanceTime(config.batchAuctionLength.add(100).toString()) + + // Mock auction - Get 100% of value + const auctionbuyAmt = fp('1') + await usdToken.connect(addr1).approve(gnosis.address, auctionbuyAmt) + await gnosis.placeBid(0, { + bidder: addr1.address, + sellAmount: auctionSellAmt, + buyAmount: auctionbuyAmt, + }) + + // Close auctions - Will not open new auctions + await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [ + { + contract: backingManager, + name: 'TradeSettled', + args: [anyValue, token.address, usdToken.address, auctionSellAmt, auctionbuyAmt], + emitted: true, + }, + { + contract: backingManager, + name: 'TradeStarted', + emitted: false, + }, + { + contract: rsrTrader, + name: 'TradeStarted', + emitted: false, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + emitted: false, + }, + ]) + + // Check new status + expect(await basketHandler.fullyCollateralized()).to.equal(true) + expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) + }) + }) + }) +}) diff --git a/test/scenario/NestedRTokens.test.ts b/test/scenario/NestedRTokens.test.ts index eff96b200d..446aca601d 100644 --- a/test/scenario/NestedRTokens.test.ts +++ b/test/scenario/NestedRTokens.test.ts @@ -374,19 +374,29 @@ describe(`Nested RTokens - P${IMPLEMENTATION}`, () => { { contract: one.rToken, name: 'Transfer', - args: [ZERO_ADDRESS, one.backingManager.address, issueAmt.div(2).sub(75)], + args: [ + ZERO_ADDRESS, + one.backingManager.address, + withinTolerance(issueAmt.div(2).sub(75)), + ], emitted: true, }, { contract: one.rToken, name: 'Transfer', - args: [one.rTokenTrader.address, one.furnace.address, rTokSellAmt], + args: [one.rTokenTrader.address, one.furnace.address, withinTolerance(rTokSellAmt)], emitted: true, }, { contract: one.rsrTrader, name: 'TradeStarted', - args: [anyValue, one.rToken.address, one.rsr.address, rsrSellAmt, rsrMinBuyAmt], + args: [ + anyValue, + one.rToken.address, + one.rsr.address, + withinTolerance(rsrSellAmt), + withinTolerance(rsrMinBuyAmt), + ], emitted: true, }, ]) diff --git a/test/scenario/RevenueHiding.test.ts b/test/scenario/RevenueHiding.test.ts index 32b26a024e..fb04851245 100644 --- a/test/scenario/RevenueHiding.test.ts +++ b/test/scenario/RevenueHiding.test.ts @@ -229,7 +229,7 @@ describe(`RevenueHiding basket collateral (/w CTokenFiatCollateral) - P${IMPLEME expect(high).to.equal(mid.add(mid.mul(ORACLE_ERROR).div(fp('1')))) // BasketHandler BU price - should overprice at the high end - const [lowBaskets, highBaskets] = await basketHandler.price() + const [lowBaskets, highBaskets] = await basketHandler.price(false) mid = fp('2') // because DAI collateral const delta = mid.mul(ORACLE_ERROR).div(fp('1')) @@ -241,7 +241,7 @@ describe(`RevenueHiding basket collateral (/w CTokenFiatCollateral) - P${IMPLEME expect(highBaskets).to.be.closeTo(mid.add(delta), mid.add(delta).div(bn('1e6'))) // Same goes for RToken price - const [lowRToken, highRToken] = await basketHandler.price() + const [lowRToken, highRToken] = await basketHandler.price(false) expect(lowRToken).to.be.gt(mid.sub(delta)) expect(lowRToken).to.be.closeTo(mid.sub(delta), mid.sub(delta).div(bn('1e6'))) expect(highRToken).to.be.gt(mid.add(delta)) // should be above expected @@ -265,7 +265,7 @@ describe(`RevenueHiding basket collateral (/w CTokenFiatCollateral) - P${IMPLEME const t = await getTrade(rsrTrader, cDAI.address) const sellAmt = await t.initBal() const minBuyAmt = await toMinBuyAmt(sellAmt, fp('2').div(50), fp('1')) - const expectedPrice = minBuyAmt.mul(fp('1')).div(sellAmt) + const expectedPrice = minBuyAmt.mul(fp('1')).div(sellAmt).mul(bn('1e10')).mul(bn('1e9')) // shift 10 decimals for cDAI; D27 precision // price should be within 1 part in a 1 trillion of our discounted rate expect(await t.worstCasePrice()).to.be.closeTo(expectedPrice, expectedPrice.div(bn('1e9'))) }) diff --git a/test/scenario/cETH.test.ts b/test/scenario/cETH.test.ts index 590623d245..7c862d7b1b 100644 --- a/test/scenario/cETH.test.ts +++ b/test/scenario/cETH.test.ts @@ -325,7 +325,7 @@ describe(`CToken of self-referential collateral (eg cETH) - P${IMPLEMENTATION}`, await advanceTime(Number(config.warmupPeriod) + 1) // Should swap WETH in for cETH - const [tokens] = await basketHandler.quote(fp('1'), 2) + const [tokens] = await basketHandler.quote(fp('1'), true, 2) expect(tokens[0]).to.equal(token0.address) expect(tokens[1]).to.equal(weth.address) diff --git a/test/scenario/cWBTC.test.ts b/test/scenario/cWBTC.test.ts index df5478210c..963d909e0e 100644 --- a/test/scenario/cWBTC.test.ts +++ b/test/scenario/cWBTC.test.ts @@ -350,7 +350,7 @@ describe(`CToken of non-fiat collateral (eg cWBTC) - P${IMPLEMENTATION}`, () => await advanceTime(Number(config.warmupPeriod) + 1) // Should swap WBTC in for cWBTC - const [tokens] = await basketHandler.quote(fp('1'), 2) + const [tokens] = await basketHandler.quote(fp('1'), false, 2) expect(tokens[0]).to.equal(token0.address) expect(tokens[1]).to.equal(wbtc.address) diff --git a/test/utils/oracles.ts b/test/utils/oracles.ts index 6dde6dec02..6498315c3d 100644 --- a/test/utils/oracles.ts +++ b/test/utils/oracles.ts @@ -26,8 +26,14 @@ export const expectPrice = async ( near: boolean, overrideToleranceDiv?: BigNumber ) => { - const asset = await ethers.getContractAt('Asset', assetAddr) - const [lowPrice, highPrice] = await asset.price() + let lowPrice, highPrice + try { + const bh = await ethers.getContractAt('IBasketHandler', assetAddr) + ;[lowPrice, highPrice] = await bh.price(false) // without issuance premium + } catch { + const asset = await ethers.getContractAt('Asset', assetAddr) + ;[lowPrice, highPrice] = await asset.price() + } const delta = avgPrice.mul(oracleError).div(fp('1')) const expectedLow = avgPrice.sub(delta) const expectedHigh = avgPrice.add(delta) @@ -106,8 +112,14 @@ export const expectDecayedPrice = async (assetAddr: string) => { // Expects an unpriced asset with low = 0 and high = FIX_MAX export const expectUnpriced = async (assetAddr: string) => { - const asset = await ethers.getContractAt('Asset', assetAddr) - const [lowPrice, highPrice] = await asset.price() + let lowPrice, highPrice + try { + const asset = await ethers.getContractAt('Asset', assetAddr) + ;[lowPrice, highPrice] = await asset.price() + } catch (e) { + const bh = await ethers.getContractAt('IBasketHandler', assetAddr) + ;[lowPrice, highPrice] = await bh.price(false) // without issuance premium + } expect(lowPrice).to.equal(0) expect(highPrice).to.equal(MAX_UINT192) }