diff --git a/common/configuration.ts b/common/configuration.ts index ec5170739a..9330cef6ce 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -118,10 +118,21 @@ export interface ITokens { // Mountain USDM?: string wUSDM?: string + + PAXG?: string + cbBTC?: string + EURC?: string } export type ITokensKeys = Array +export interface IDemurrageCollateral { + DMR100PAXG?: string + DMR100cbBTC?: string + DMR100EURC?: string + DMR100ARB?: string +} + export interface IFeeds { stETHETH?: string stETHUSD?: string @@ -129,6 +140,8 @@ export interface IFeeds { cbETHETHexr?: string ETHUSD?: string wstETHstETH?: string + XAU?: string + EUR?: string } export interface IPools { @@ -260,6 +273,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { sdUSDCUSDCPlus: '0x9bbF31E99F30c38a5003952206C31EEa77540BeF', USDe: '0x4c9edd5852cd905f086c759e8383e09bff1e68b3', sUSDe: '0x9D39A5DE30e57443BfF2A8307A4256c8797A3497', + PAXG: '0x45804880De22913dAFE09f4980848ECE6EcbAf78', }, chainlinkFeeds: { RSR: '0x759bBC1be8F90eE6457C44abc7d443842a976d02', @@ -289,6 +303,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { pyUSD: '0x8f1dF6D7F2db73eECE86a18b4381F4707b918FB1', apxETH: '0x19219BC90F48DeE4d5cF202E09c438FAacFd8Bea', // apxETH/ETH USDe: '0xa569d910839Ae8865Da8F8e70FfFb0cBA869F961', + XAU: '0x214eD9Da11D2fbe465a6fc601a91E62EbEc1a0D6', }, AAVE_INCENTIVES: '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5', AAVE_EMISSIONS_MGR: '0xEE56e2B3D491590B5b31738cC34d5232F378a8D5', @@ -517,6 +532,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = { sUSDbC: '0x4c80e24119cfb836cdf0a6b53dc23f04f7e652ca', wstETH: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452', STG: '0xE3B53AF74a4BF62Ae5511055290838050bf764Df', + cbBTC: '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf', + EURC: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42', eUSD: '0xCfA3Ef56d303AE4fAabA0592388F19d7C3399FB4', meUSD: '0xbb819D845b573B5D7C538F5b85057160cfb5f313', }, @@ -535,6 +552,10 @@ export const networkConfig: { [key: string]: INetworkConfig } = { stETHETH: '0xf586d0728a47229e747d824a939000Cf21dEF5A0', // 0.5%, 24h ETHUSD: '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70', // 0.15%, 20min wstETHstETH: '0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061', // 0.5%, 24h + BTC: '0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F', // 0.1%, 1200s + cbBTC: '0x07DA0E54543a844a80ABE69c8A12F22B3aA59f9D', // 0.5%, 24h + EURC: '0xDAe398520e2B67cd3f27aeF9Cf14D93D927f8250', // 0.3%, 24h + EUR: '0xc91D87E81faB8f93699ECf7Ee9B44D11e1D53F0F', // 0.3%, 24h eUSD: '0x9b2C948dbA5952A1f5Ab6fA16101c1392b8da1ab', // 0.5%, 24h }, GNOSIS_EASY_AUCTION: '0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02', // mock diff --git a/contracts/facade/factories/DemurrageCollateralFactory.sol b/contracts/facade/factories/DemurrageCollateralFactory.sol new file mode 100644 index 0000000000..220b753774 --- /dev/null +++ b/contracts/facade/factories/DemurrageCollateralFactory.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "../../plugins/assets/DemurrageCollateral.sol"; + +/** + * @title DemurrageCollateralFactory + */ +contract DemurrageCollateralFactory { + event DemurrageCollateralDeployed(address indexed collateral); + + // collateral address => fee per second + mapping(address => uint192) public demurrageDeployments; + + bytes32 public constant USD = bytes32("USD"); + + function deployNewDemurrageCollateral( + CollateralConfig memory config, + DemurrageConfig memory demurrageConfig + ) external returns (address newCollateral) { + if (demurrageConfig.isFiat) { + require(config.targetName == USD, "isFiat only compatible with USD"); + } + + newCollateral = address(new DemurrageCollateral(config, demurrageConfig)); + demurrageDeployments[newCollateral] = demurrageConfig.fee; + emit DemurrageCollateralDeployed(newCollateral); + } +} diff --git a/contracts/interfaces/IAsset.sol b/contracts/interfaces/IAsset.sol index 4cc223a63d..76025bdd9b 100644 --- a/contracts/interfaces/IAsset.sol +++ b/contracts/interfaces/IAsset.sol @@ -130,6 +130,9 @@ interface ICollateral is IAsset { // Used only in Testing. Strictly speaking a Collateral does not need to adhere to this interface interface TestICollateral is TestIAsset, ICollateral { + /// deprecated + function lotPrice() external view returns (uint192 lotLow, uint192 lotHigh); + /// @return The epoch timestamp when the collateral will default from IFFY to DISABLED function whenDefault() external view returns (uint256); diff --git a/contracts/plugins/assets/DemurrageCollateral.sol b/contracts/plugins/assets/DemurrageCollateral.sol new file mode 100644 index 0000000000..74e4becebf --- /dev/null +++ b/contracts/plugins/assets/DemurrageCollateral.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "./FiatCollateral.sol"; + +struct DemurrageConfig { + uint192 fee; // {1/s} per-second deflation of the target unit + // + bool isFiat; // if true: {target} == {UoA} + bool targetUnitFeed0; // if true: feed0 is {target/tok} + // + // optional extra feed + AggregatorV3Interface feed1; // empty or {UoA/target} + uint48 timeout1; // {s} + uint192 error1; // {1} +} + +/** + * @title DemurrageCollateral + * @notice Collateral plugin for a genneralized demurrage collateral (i.e /w management fee) + * Warning: Do NOT use the standard targetName() format + * - Use: DMR{annual_demurrage_in_basis_points}{token_symbol} + * + * under 1 feed: + * - feed0/chainlinkFeed must be {UoA/tok} + * - apply issuance premium IFF isFiat is true + * 2 feeds: + * - feed0: targetUnitFeed0 ? {target/tok} : {UoA/tok} + * - feed1: {UoA/target} + * - apply issuance premium + * + * - tok = Tokenized X + * - ref = Decayed X (since 2024-01-01 00:00:00 GMT+0000) + * - target = Decayed X (since 2024-01-01 00:00:00 GMT+0000) + * - UoA = USD + */ +contract DemurrageCollateral is FiatCollateral { + using FixLib for uint192; + using OracleLib for AggregatorV3Interface; + + uint48 public constant T0 = 1704067200; // {s} Jan 1st 2024 00:00:00 GMT+0000 + + bool internal immutable isFiat; + bool internal immutable targetUnitFeed0; // if true: feed0 is {target/tok} + + // up to 2 feeds/timeouts/errors + AggregatorV3Interface internal immutable feed0; // targetUnitFeed0 ? {target/tok} : {UoA/tok} + AggregatorV3Interface internal immutable feed1; // empty or {UoA/target} + uint48 internal immutable timeout0; // {s} + uint48 internal immutable timeout1; // {s} + uint192 internal immutable error0; // {1} + uint192 internal immutable error1; // {1} + + // immutable in spirit -- cannot be because of FiatCollateral's targetPerRef() call + uint192 public fee; // {1/s} demurrage fee; target unit deflation + + /// @param config.chainlinkFeed => feed0: {UoA/tok} or {target/tok} + /// @param config.oracleTimeout => timeout0 + /// @param config.oracleError => error0 + /// @param demurrageConfig.feed1 empty or {UoA/target} + /// @param demurrageConfig.isFiat true iff {target} == {UoA} + /// @param demurrageConfig.targetUnitfeed0 true iff feed0 is {target/tok} units + /// @param demurrageConfig.fee {1/s} fraction of the target unit to deflate each second + constructor(CollateralConfig memory config, DemurrageConfig memory demurrageConfig) + FiatCollateral(config) + { + isFiat = demurrageConfig.isFiat; + targetUnitFeed0 = demurrageConfig.targetUnitFeed0; + + if (demurrageConfig.feed1 != AggregatorV3Interface(address(0))) { + require(demurrageConfig.timeout1 != 0, "missing timeout1"); + require(demurrageConfig.error1 > 0 && demurrageConfig.error1 < FIX_ONE, "bad error1"); + } else { + require(!demurrageConfig.targetUnitFeed0, "missing UoA info"); + } + + feed0 = config.chainlinkFeed; + feed1 = demurrageConfig.feed1; + timeout0 = config.oracleTimeout; + timeout1 = demurrageConfig.timeout1; + error0 = config.oracleError; + error1 = demurrageConfig.error1; + + fee = demurrageConfig.fee; + } + + /// 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/tok} The un-decayed pegPrice + function tryPrice() + external + view + override + returns ( + uint192 low, + uint192 high, + uint192 pegPrice + ) + { + // This plugin handles pegPrice differently than most -- since FiatCollateral saves + // valid peg ranges at deployment time, they do not account for the decay due to the + // demurrage fee. + // + // The pegPrice should not account for demurrage + + pegPrice = FIX_ONE; // undecayed rate that won't trigger default or issuance premium + + uint192 x = feed0.price(timeout0); // {UoA/tok} + uint192 xErr = error0; + + low = x.mul(FIX_ONE - xErr); // {UoA/tok} + high = x.mul(FIX_ONE + xErr); // {UoA/tok} + + if (address(feed1) != address(0)) { + if (targetUnitFeed0) { + pegPrice = x; // {target/tok} + + uint192 y = feed1.price(timeout1); // {UoA/target} + uint192 yErr = error1; + + // Multiply x and y + low = low.mul(y.mul(FIX_ONE - yErr), FLOOR); + high = high.mul(y.mul(FIX_ONE + yErr), CEIL); + } else { + // {target/tok} = {UoA/tok} / {UoA/target} + pegPrice = x.div(feed1.price(timeout1), ROUND); + } + } else if (isFiat) { + // {target/tok} = {UoA/tok} because {target} == {UoA} + pegPrice = x; + } + + assert(low <= high); + } + + // === Demurrage rates === + + /// @return {ref/tok} Quantity of whole reference units per whole collateral tokens + function refPerTok() public view override returns (uint192) { + // Monotonically increasing due to target unit (and reference unit) deflation + + uint192 denominator = FIX_ONE.minus(fee).powu(uint48(block.timestamp - T0)); + if (denominator == 0) return FIX_MAX; // TODO + + // up-only + return FIX_ONE.div(denominator, FLOOR); + } +} diff --git a/docs/demurrage-collateral.md b/docs/demurrage-collateral.md new file mode 100644 index 0000000000..e10213c253 --- /dev/null +++ b/docs/demurrage-collateral.md @@ -0,0 +1,81 @@ +# Demurrage Collateral Plugins + +**Demurrage** is a general term for a per-unit-time fee on assets-under-management (aka management fees) + +## Background + +Many assets on-chain do not have yield. While the Reserve Protocol is compatible with non-yielding assets, this introduces downsides: an RToken naively composed entirely of non-yielding collateral assets lacks RSR overcollateralization and governance. + +In this case a revenue stream can be created by composing an inflationary reference + target units that refer to a falling quantity of the token unit. This results in a monotonically increasing `refPerTok()` that can be consumed by the protocol to measure appreciation. + +There are side-effects to the `targetName`, however the rest of the collateral plugin remains much the same. + +In principle demurrage can be added to any type of collateral, even already yield-bearing collateral. + +**Units** + +```solidity +/** + * - tok = Tokenized X + * - ref = Decayed X (since 2024-01-01 00:00:00 GMT+0000) + * - target = Decayed X (since 2024-01-01 00:00:00 GMT+0000) + * - UoA = USD + */ +``` + +### Reference Unit (inflationary) + +The reference unit becomes naturally inflationary, resulting in a `refPerTok` of: + +``` +refPerTok(): 1 / (1 - demurrage_rate_per_second) ^ t + where t is seconds since 01/01/2024 00:00:00 GMT+0000 +``` + +The timestamp of 01/01/2024 00:00:00 GMT+0000 is chosen arbitrarily. It's not important what this value is, but there are benefits to using a common anchor (and 1970 is wastefully far). + +In unix time this is `1704067200` + +### Target Unit (inflationary) + +The reference unit maintains a 1:1 rate against the target unit + +``` +targetPerRef(): 1 +``` + +As a naming convention, we suggest: +`DMR{annual_demurrage_in_basis_points}{token_symbol}` or `DMR100USD`, for example + +1. The `DMR` prefix is short for demurrage +2. The `annual_demurrage_in_basis_points` is a number such as 100 for 1% annually +3. The `token_symbol` is the symbol of the unit absent any demurrage + +Collateral can only be automatically substituted in the basket with collateral that share the _exact_ same target unit. This unfortunately means a standard WETH collateral cannot be backup for a demurrage ETH collateral. Both the unit type and rate must be identical in order for two collateral to be in the same target unit class. + +This also means there can be multiple demurrage collateral for a single token. We refer to these as tiers. + +### Setting the basket weights + +Prime basket weights are in units of January 1st 2024 collateral, not today's collateral. It doesn't matter if the collateral wasn't around in Jan 2024 -- when setting the basket weights the setter must take into account how much demurrage has occurred since January 1st 2024. + +This is identical to the calculation for the `refPerTok()` function in the [DemurrageCollateral.sol](../contracts/plugins/assets/DemurrageCollateral.sol) contract, but calculating for an arbitrary timestamp. + +``` +weight = 1 / (1 - fee) ^ seconds; +``` + +`fee()` available on DemurrageCollateral contract + +### Implementation + +[DemurrageCollateral.sol](../contracts/plugins/assets/DemurrageCollateral.sol) implements a generalized demurrage collateral plugin that should support almost all use-cases + +Sample usage: + +- [deploy_cbbtc_100.ts](../scripts/deployment/phase2-assets/collaterals/deploy_cbbtc_100.ts) +- [deploy_eurc_100.ts](../scripts/deployment/phase2-assets/collaterals/deploy_eurc_100.ts) +- [deploy_paxg_100.ts](../scripts/deployment/phase2-assets/collaterals/deploy_paxg_100.ts) +- [deploy_arb_100.ts](../scripts/deployment/phase2-assets/collaterals/deploy_arb_100.ts) + +TODO link to demurrage collateral factory address after deployment diff --git a/scripts/deployment/common.ts b/scripts/deployment/common.ts index 0dfe37e4bf..9d47f1c702 100644 --- a/scripts/deployment/common.ts +++ b/scripts/deployment/common.ts @@ -1,5 +1,11 @@ import fs from 'fs' -import { ITokens, IComponents, IImplementations, IPools } from '../../common/configuration' +import { + IDemurrageCollateral, + ITokens, + IComponents, + IImplementations, + IPools, +} from '../../common/configuration' // This file is intended to have minimal imports, so that it can be used from tasks if necessary @@ -32,8 +38,8 @@ export interface IDeployments { export interface IAssetCollDeployments { assets: ITokens - collateral: ITokens & IPools - erc20s: ITokens & IPools + collateral: ITokens & IPools & IDemurrageCollateral + erc20s: ITokens & IPools & IDemurrageCollateral } export interface IRTokenDeployments { diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_arb_100.ts b/scripts/deployment/phase2-assets/collaterals/deploy_arb_100.ts new file mode 100644 index 0000000000..61de7ccad1 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_arb_100.ts @@ -0,0 +1,100 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { arbitrumL2Chains, networkConfig } from '../../../../common/configuration' +import { bn, fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus, ZERO_ADDRESS } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { + DELAY_UNTIL_DEFAULT, + ONE_PERCENT_FEE, +} from '../../../../test/plugins/individual-collateral/dtf/constants' +import { priceTimeout, getArbOracleError } from '../../utils' +import { DemurrageCollateral } from '../../../../typechain' +import { ContractFactory } from 'ethers' + +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 ARB Demurrage Collateral - ARB **************************/ + + if (!arbitrumL2Chains.includes(hre.network.name)) { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + const DemurrageCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'DemurrageCollateral' + ) + + const oracleError = getArbOracleError(hre.network.name) + + const collateral = await DemurrageCollateralFactory.connect(deployer).deploy( + { + erc20: networkConfig[chainId].tokens.ARB, + targetName: hre.ethers.utils.formatBytes32String('DMR100ARB'), + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.ARB, // {UoA/tok} + oracleError: oracleError.toString(), + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: bn('0'), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: ZERO_ADDRESS, + timeout1: bn('0'), + error1: bn('0'), + } + ) + await collateral.deployed() + + console.log(`Deployed ARB to ${hre.network.name} (${chainId}): ${collateral.address}`) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.DMR100ARB = collateral.address + assetCollDeployments.erc20s.DMR100ARB = networkConfig[chainId].tokens.ARB + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_cbbtc_100.ts b/scripts/deployment/phase2-assets/collaterals/deploy_cbbtc_100.ts new file mode 100644 index 0000000000..6a19f2edaa --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_cbbtc_100.ts @@ -0,0 +1,98 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { baseL2Chains, networkConfig } from '../../../../common/configuration' +import { bn, fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { + DELAY_UNTIL_DEFAULT, + ONE_PERCENT_FEE, +} from '../../../../test/plugins/individual-collateral/dtf/constants' +import { priceTimeout } from '../../utils' +import { DemurrageCollateral } from '../../../../typechain' +import { ContractFactory } from 'ethers' + +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 cbBTC Demurrage Collateral - cbBTC **************************/ + + if (!baseL2Chains.includes(hre.network.name)) { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + const DemurrageCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'DemurrageCollateral' + ) + + const collateral = await DemurrageCollateralFactory.connect(deployer).deploy( + { + erc20: networkConfig[chainId].tokens.cbBTC, + targetName: hre.ethers.utils.formatBytes32String('DMR100BTC'), + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.cbBTC, // {UoA/tok} + oracleError: fp('0.005').toString(), // 0.5% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.02').add(fp('0.005')).toString(), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: networkConfig[chainId].chainlinkFeeds.BTC, // {UoA/target} + timeout1: bn('1200'), // 20 min + error1: fp('0.001').toString(), // 0.1% + } + ) + await collateral.deployed() + + console.log(`Deployed cbBTC to ${hre.network.name} (${chainId}): ${collateral.address}`) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.DMR100cbBTC = collateral.address + assetCollDeployments.erc20s.DMR100cbBTC = networkConfig[chainId].tokens.cbBTC + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_eurc_100.ts b/scripts/deployment/phase2-assets/collaterals/deploy_eurc_100.ts new file mode 100644 index 0000000000..67c6a6d31a --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_eurc_100.ts @@ -0,0 +1,98 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { baseL2Chains, networkConfig } from '../../../../common/configuration' +import { bn, fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { + DELAY_UNTIL_DEFAULT, + ONE_PERCENT_FEE, +} from '../../../../test/plugins/individual-collateral/dtf/constants' +import { priceTimeout } from '../../utils' +import { DemurrageCollateral } from '../../../../typechain' +import { ContractFactory } from 'ethers' + +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 EURC Demurrage Collateral - EURC **************************/ + + if (!baseL2Chains.includes(hre.network.name)) { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + const DemurrageCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'DemurrageCollateral' + ) + + const collateral = await DemurrageCollateralFactory.connect(deployer).deploy( + { + erc20: networkConfig[chainId].tokens.EURC, + targetName: hre.ethers.utils.formatBytes32String('DMR100EUR'), + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.EURC, + oracleError: fp('0.003').toString(), // 0.3% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.02').add(fp('0.003')).toString(), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: networkConfig[chainId].chainlinkFeeds.EUR, + timeout1: bn('86400').toString(), // 24 hr + error1: fp('0.003').toString(), // 0.3% + } + ) + await collateral.deployed() + + console.log(`Deployed EURC to ${hre.network.name} (${chainId}): ${collateral.address}`) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.DMR100EURC = collateral.address + assetCollDeployments.erc20s.DMR100EURC = networkConfig[chainId].tokens.EURC + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_paxg_100.ts b/scripts/deployment/phase2-assets/collaterals/deploy_paxg_100.ts new file mode 100644 index 0000000000..83a8d8b209 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_paxg_100.ts @@ -0,0 +1,98 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { arbitrumL2Chains, baseL2Chains, networkConfig } from '../../../../common/configuration' +import { bn, fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus, ZERO_ADDRESS } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { + DELAY_UNTIL_DEFAULT, + ONE_PERCENT_FEE, +} from '../../../../test/plugins/individual-collateral/dtf/constants' +import { priceTimeout } from '../../utils' +import { DemurrageCollateral } from '../../../../typechain' +import { ContractFactory } from 'ethers' + +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 PAXG Demurrage Collateral - PAXG **************************/ + + if (baseL2Chains.includes(hre.network.name) || arbitrumL2Chains.includes(hre.network.name)) { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + const DemurrageCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'DemurrageCollateral' + ) + + const collateral = await DemurrageCollateralFactory.connect(deployer).deploy( + { + erc20: networkConfig[chainId].tokens.PAXG, + targetName: hre.ethers.utils.formatBytes32String('DMR100XAU'), + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.XAU, // {UoA/tok} + oracleError: fp('0.003').toString(), // 0.3% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: bn('0'), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: ZERO_ADDRESS, + timeout1: bn('0'), + error1: bn('0'), + } + ) + await collateral.deployed() + + console.log(`Deployed PAXG to ${hre.network.name} (${chainId}): ${collateral.address}`) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.DMR100PAXG = collateral.address + assetCollDeployments.erc20s.DMR100PAXG = networkConfig[chainId].tokens.PAXG + 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/verification/collateral-plugins/verify_arb_100.ts b/scripts/verification/collateral-plugins/verify_arb_100.ts new file mode 100644 index 0000000000..0c4a4433f5 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_arb_100.ts @@ -0,0 +1,81 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { arbitrumL2Chains, networkConfig } from '../../../common/configuration' +import { bn, fp } from '../../../common/numbers' +import { ZERO_ADDRESS } from '../../../common/constants' +import { verifyContract } from '../../deployment/utils' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../deployment/common' +import { + DELAY_UNTIL_DEFAULT, + ONE_PERCENT_FEE, +} from '../../../test/plugins/individual-collateral/dtf/constants' +import { priceTimeout, getArbOracleError } from '../../deployment/utils' + +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) + + /******** Verify ARB Demurrage Collateral - ARB **************************/ + + if (!arbitrumL2Chains.includes(hre.network.name)) { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + const oracleError = getArbOracleError(hre.network.name) + + await verifyContract( + chainId, + assetCollDeployments.collateral.DMR100ARB, + [ + { + erc20: networkConfig[chainId].tokens.ARB, + targetName: hre.ethers.utils.formatBytes32String('DMR100ARB'), + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.ARB, // {UoA/tok} + oracleError: oracleError.toString(), + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: bn('0'), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: ZERO_ADDRESS, + timeout1: bn('0'), + error1: bn('0'), + }, + ], + 'contracts/plugins/assets/DemurrageCollateral.sol:DemurrageCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_cbbtc_100.ts b/scripts/verification/collateral-plugins/verify_cbbtc_100.ts new file mode 100644 index 0000000000..c1809ce3f3 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_cbbtc_100.ts @@ -0,0 +1,83 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { baseL2Chains, networkConfig } from '../../../common/configuration' +import { bn, fp } from '../../../common/numbers' +import { verifyContract } from '../../deployment/utils' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../deployment/common' +import { + DELAY_UNTIL_DEFAULT, + ONE_PERCENT_FEE, +} from '../../../test/plugins/individual-collateral/dtf/constants' +import { priceTimeout } from '../../deployment/utils' + +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) + + /******** Verify cbBTC Demurrage Collateral - cbBTC **************************/ + + if (!baseL2Chains.includes(hre.network.name)) { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + const collateral = await hre.ethers.getContractAt( + 'ICollateral', + assetCollDeployments.collateral.DMR100cbBTC! + ) + + await verifyContract( + chainId, + assetCollDeployments.collateral.DMR100cbBTC, + [ + { + erc20: networkConfig[chainId].tokens.cbBTC, + targetName: await collateral.targetName(), + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.cbBTC, // {UoA/tok} + oracleError: fp('0.005').toString(), // 0.5% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.02').add(fp('0.005')).toString(), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: networkConfig[chainId].chainlinkFeeds.BTC, // {UoA/target} + timeout1: bn('1200'), // 20 min + error1: fp('0.001').toString(), // 0.1% + }, + ], + 'contracts/plugins/assets/DemurrageCollateral.sol:DemurrageCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_eurc_100.ts b/scripts/verification/collateral-plugins/verify_eurc_100.ts new file mode 100644 index 0000000000..5886dd3e31 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_eurc_100.ts @@ -0,0 +1,83 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { baseL2Chains, networkConfig } from '../../../common/configuration' +import { bn, fp } from '../../../common/numbers' +import { verifyContract } from '../../deployment/utils' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../deployment/common' +import { + DELAY_UNTIL_DEFAULT, + ONE_PERCENT_FEE, +} from '../../../test/plugins/individual-collateral/dtf/constants' +import { priceTimeout } from '../../deployment/utils' + +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) + + /******** Verify EURC Demurrage Collateral - EURC **************************/ + + if (!baseL2Chains.includes(hre.network.name)) { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + const collateral = await hre.ethers.getContractAt( + 'ICollateral', + assetCollDeployments.collateral.DMR100EURC! + ) + + await verifyContract( + chainId, + assetCollDeployments.collateral.DMR100EURC, + [ + { + erc20: networkConfig[chainId].tokens.EURC, + targetName: await collateral.targetName(), + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.EURC, // {UoA/tok} + oracleError: fp('0.003').toString(), // 0.3% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.02').add(fp('0.003')).toString(), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: networkConfig[chainId].chainlinkFeeds.EUR, // {UoA/target} + timeout1: bn('86400').toString(), // 24 hr + error1: fp('0.003').toString(), // 0.3% + }, + ], + 'contracts/plugins/assets/DemurrageCollateral.sol:DemurrageCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_paxg_100.ts b/scripts/verification/collateral-plugins/verify_paxg_100.ts new file mode 100644 index 0000000000..cbf0a250dc --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_paxg_100.ts @@ -0,0 +1,84 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { arbitrumL2Chains, baseL2Chains, networkConfig } from '../../../common/configuration' +import { bn, fp } from '../../../common/numbers' +import { ZERO_ADDRESS } from '../../../common/constants' +import { verifyContract } from '../../deployment/utils' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../deployment/common' +import { + DELAY_UNTIL_DEFAULT, + ONE_PERCENT_FEE, +} from '../../../test/plugins/individual-collateral/dtf/constants' +import { priceTimeout } from '../../deployment/utils' + +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) + + /******** Verify PAXG Demurrage Collateral - PAXG **************************/ + + if (baseL2Chains.includes(hre.network.name) || arbitrumL2Chains.includes(hre.network.name)) { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + const collateral = await hre.ethers.getContractAt( + 'ICollateral', + assetCollDeployments.collateral.DMR100PAXG! + ) + + await verifyContract( + chainId, + assetCollDeployments.collateral.DMR100PAXG, + [ + { + erc20: networkConfig[chainId].tokens.PAXG, + targetName: await collateral.targetName(), + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.XAU, // {UoA/tok} + oracleError: fp('0.003').toString(), // 0.3% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: bn('0'), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: ZERO_ADDRESS, + timeout1: bn('0'), + error1: bn('0'), + }, + ], + 'contracts/plugins/assets/DemurrageCollateral.sol:DemurrageCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/tasks/deployment/create-demurrage-collateral-factory.ts b/tasks/deployment/create-demurrage-collateral-factory.ts new file mode 100644 index 0000000000..039f1897f1 --- /dev/null +++ b/tasks/deployment/create-demurrage-collateral-factory.ts @@ -0,0 +1,57 @@ +import { getChainId } from '../../common/blockchain-utils' +import { task, types } from 'hardhat/config' +import { DemurrageCollateralFactory } from '../../typechain' + +task('create-demurrage-collateral-factory', 'Deploys a DemurrageCollateralFactory') + .addOptionalParam('noOutput', 'Suppress output', false, types.boolean) + .setAction(async (params, hre) => { + const [wallet] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + if (!params.noOutput) { + console.log( + `Deploying DemurrageCollateralFactory to ${hre.network.name} (${chainId}) with burner account ${wallet.address}` + ) + } + + const FactoryFactory = await hre.ethers.getContractFactory('DemurrageCollateralFactory') + const demurrageCollateralFactory = ( + await FactoryFactory.connect(wallet).deploy() + ) + await demurrageCollateralFactory.deployed() + + if (!params.noOutput) { + console.log( + `Deployed DemurrageCollateralFactory to ${hre.network.name} (${chainId}): ${demurrageCollateralFactory.address}` + ) + } + + // Uncomment to verify + if (!params.noOutput) { + console.log('sleeping 30s') + } + + // Sleep to ensure API is in sync with chain + await new Promise((r) => setTimeout(r, 30000)) // 30s + + if (!params.noOutput) { + console.log('verifying') + } + + /** ******************** Verify DemurrageCollateralFactory ****************************************/ + console.time('Verifying DemurrageCollateralFactory') + await hre.run('verify:verify', { + address: demurrageCollateralFactory.address, + constructorArguments: [], + contract: + 'contracts/facade/factories/DemurrageCollateralFactory.sol:DemurrageCollateralFactory', + }) + console.timeEnd('Verifying DemurrageCollateralFactory') + + if (!params.noOutput) { + console.log('verified') + } + + return { demurrageCollateralFactory: demurrageCollateralFactory.address } + }) diff --git a/tasks/index.ts b/tasks/index.ts index c4c0e13c4a..dc95eebb14 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -17,6 +17,7 @@ import './deployment/mock/deploy-mock-wbtc' import './deployment/deploy-easyauction' import './deployment/create-deployer-registry' import './deployment/create-curve-oracle-factory' +import './deployment/create-demurrage-collateral-factory' import './deployment/deploy-facade-monitor' import './deployment/empty-wallet' import './deployment/cancel-tx' diff --git a/test/plugins/individual-collateral/aave-v3/common.ts b/test/plugins/individual-collateral/aave-v3/common.ts index 77e11f1be0..0b095e5ae8 100644 --- a/test/plugins/individual-collateral/aave-v3/common.ts +++ b/test/plugins/individual-collateral/aave-v3/common.ts @@ -213,6 +213,7 @@ export const makeTests = (defaultCollateralOpts: CollateralParams, altParams: Al itIsPricedByPeg: true, chainlinkDefaultAnswer: 1e8, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, getExpectedPrice, toleranceDivisor: altParams.toleranceDivisor ?? bn('1e9'), // 1e15 adjusted for ((x + 1)/x) timestamp precision targetNetwork: altParams.targetNetwork, diff --git a/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts b/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts index 8a3a07a83f..3c64bbe17b 100644 --- a/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts @@ -289,6 +289,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itHasRevenueHiding: it, itChecksNonZeroDefaultThreshold: it, resetFork, diff --git a/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts b/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts index cd35ee5c0c..47de1fafbb 100644 --- a/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts +++ b/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts @@ -246,6 +246,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itHasRevenueHiding: it, itChecksNonZeroDefaultThreshold: it, resetFork, diff --git a/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts b/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts index a4a9c32425..00accddee6 100644 --- a/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts +++ b/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts @@ -277,6 +277,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itHasRevenueHiding: it, itChecksNonZeroDefaultThreshold: it, resetFork, diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 47f89b767f..e6036eaf0b 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -92,6 +92,7 @@ export default function fn( itChecksTargetPerRefDefaultUp, itChecksRefPerTokDefault, itChecksPriceChanges, + itChecksPriceChangesRefPerTok, itChecksNonZeroDefaultThreshold, itHasRevenueHiding, itIsPricedByPeg, @@ -301,7 +302,7 @@ export default function fn( } ) - itChecksPriceChanges('prices change as refPerTok changes', async () => { + itChecksPriceChangesRefPerTok('prices change as refPerTok changes', async () => { const initRefPerTok = await collateral.refPerTok() const oracleError = await collateral.oracleError() @@ -1027,6 +1028,38 @@ export default function fn( targetUnitOracle.address, ORACLE_TIMEOUT ) + } else if (target.indexOf(ethers.utils.formatBytes32String('XAU'))) { + if (onBase || onArbitrum) throw new Error('PAXG only supported on mainnet') + + // PAXG + const ERC20Factory = await ethers.getContractFactory('ERC20MockDecimals') + const erc20 = await ERC20Factory.deploy('PAXG', 'PAXG', 18) + await erc20.mint(addr1.address, bn('1e30')) + + const DemurrageFactory: ContractFactory = await ethers.getContractFactory( + 'DemurrageCollateral' + ) + return await DemurrageFactory.deploy( + { + erc20: erc20.address, + targetName: target, + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + oracleTimeout: ORACLE_TIMEOUT, + maxTradeVolume: MAX_UINT192, + defaultThreshold: bn('0'), + delayUntilDefault: bn('0'), + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: bn('0'), + feed1: ZERO_ADDRESS, + timeout1: bn(0), + error1: bn(0), + } + ) } else { throw new Error(`Unknown target: ${target}`) } diff --git a/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts b/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts index c0062e93db..9618459e34 100644 --- a/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts +++ b/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts @@ -383,6 +383,7 @@ allTests.forEach((curr: CTokenV3Enumeration) => { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it.skip, // implemented in this file itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it.skip, // implemented in this file itIsPricedByPeg: true, diff --git a/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts b/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts index 2919f0ebc4..5459736ce8 100644 --- a/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts @@ -214,6 +214,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it.skip, resetFork, diff --git a/test/plugins/individual-collateral/dtf/PAXGCollateralTestSuite.test.ts b/test/plugins/individual-collateral/dtf/PAXGCollateralTestSuite.test.ts new file mode 100644 index 0000000000..c5666d1641 --- /dev/null +++ b/test/plugins/individual-collateral/dtf/PAXGCollateralTestSuite.test.ts @@ -0,0 +1,183 @@ +import collateralTests from '../collateralTests' +import { CollateralFixtureContext, CollateralOpts, MintCollateralFunc } from '../pluginTestTypes' +import { resetFork, mintPAXG } from './helpers' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { ContractFactory, BigNumberish, BigNumber } from 'ethers' +import { MockV3Aggregator, MockV3Aggregator__factory, TestICollateral } from '../../../../typechain' +import { bn, fp } from '../../../../common/numbers' +import { ZERO_ADDRESS } from '../../../../common/constants' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + DELAY_UNTIL_DEFAULT, + PAXG, + ONE_PERCENT_FEE, + ORACLE_ERROR, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, + MAX_TRADE_VOL, + XAU_USD_PRICE_FEED, +} from './constants' + +/* + Define deployment functions +*/ + +interface PAXGCollateralOpts extends CollateralOpts { + fee?: BigNumberish +} + +export const defaultPAXGCollateralOpts: PAXGCollateralOpts = { + erc20: PAXG, + targetName: ethers.utils.formatBytes32String('DMR100XAU'), + rewardERC20: ZERO_ADDRESS, + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: XAU_USD_PRICE_FEED, + oracleTimeout: ORACLE_TIMEOUT, + oracleError: ORACLE_ERROR, + maxTradeVolume: MAX_TRADE_VOL, + fee: ONE_PERCENT_FEE, +} + +export const deployCollateral = async (opts: PAXGCollateralOpts = {}): Promise => { + opts = { ...defaultPAXGCollateralOpts, ...opts } + + const PAXGCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'DemurrageCollateral' + ) + const collateral = await PAXGCollateralFactory.deploy( + { + erc20: opts.erc20, + targetName: opts.targetName, + priceTimeout: opts.priceTimeout, + chainlinkFeed: opts.chainlinkFeed, + oracleError: opts.oracleError, + oracleTimeout: opts.oracleTimeout, + maxTradeVolume: opts.maxTradeVolume, + defaultThreshold: bn('0'), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: opts.fee, + feed1: ZERO_ADDRESS, + timeout1: bn(0), + error1: bn(0), + }, + { gasLimit: 2000000000 } + ) + await collateral.deployed() + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return collateral +} + +const chainlinkDefaultAnswer = bn('266347300000') // $2,663.473 + +type Fixture = () => Promise + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: PAXGCollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultPAXGCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const chainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, chainlinkDefaultAnswer) + ) + collateralOpts.chainlinkFeed = chainlinkFeed.address + + const collateral = await deployCollateral(collateralOpts) + const tok = await ethers.getContractAt('IERC20Metadata', await collateral.erc20()) + + return { + alice, + collateral, + chainlinkFeed, + tok, + } + } + + return makeCollateralFixtureContext +} + +const mintCollateralTo: MintCollateralFunc = async ( + ctx: CollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintPAXG(ctx.tok, amount, recipient) +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const reduceTargetPerRef = async () => {} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const increaseTargetPerRef = async () => {} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const reduceRefPerTok = async () => {} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const increaseRefPerTok = async () => {} + +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 = () => {} + +// 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.skip, + itChecksTargetPerRefDefaultUp: it.skip, + itChecksNonZeroDefaultThreshold: it.skip, + itChecksRefPerTokDefault: it.skip, + itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it.skip, + itHasRevenueHiding: it.skip, + resetFork, + collateralName: 'PAXG Demurrage Collateral', + chainlinkDefaultAnswer, + itIsAXGCricedByPeg: true, + toleranceDivisor: bn('1e8'), // 1-part in 100 million +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/dtf/constants.ts b/test/plugins/individual-collateral/dtf/constants.ts new file mode 100644 index 0000000000..047f1addea --- /dev/null +++ b/test/plugins/individual-collateral/dtf/constants.ts @@ -0,0 +1,21 @@ +import { bn, fp } from '../../../../common/numbers' +import { networkConfig } from '../../../../common/configuration' + +// Mainnet Addresses +export const XAU_USD_PRICE_FEED = networkConfig['31337'].chainlinkFeeds.XAU as string +export const PAXG = networkConfig['31337'].tokens.PAXG as string + +export const PRICE_TIMEOUT = bn('604800') // 1 week +export const ORACLE_TIMEOUT = bn('86400') // 24 hours in seconds +export const ORACLE_ERROR = fp('0.003') // 0.3% +export const DELAY_UNTIL_DEFAULT = bn('86400') // 24h +export const MAX_TRADE_VOL = fp('1e6') + +// to compute: 1 - (1 - annual_fee) ^ (1/31536000) +export const TWO_PERCENT_FEE = bn('640623646') // 2% annually +export const ONE_PERCENT_FEE = bn('318694059') // 1% annually +export const FIFTY_BPS_FEE = bn('158946658') // 0.5% annually +export const TWENTY_FIVE_BPS_FEE = bn('79373738') // 0.25% annually +export const TEN_BPS_FEE = bn('31725657') // 0.1% annually + +export const FORK_BLOCK = 20963623 diff --git a/test/plugins/individual-collateral/dtf/helpers.ts b/test/plugins/individual-collateral/dtf/helpers.ts new file mode 100644 index 0000000000..50b9f6294c --- /dev/null +++ b/test/plugins/individual-collateral/dtf/helpers.ts @@ -0,0 +1,21 @@ +import { ethers } from 'hardhat' +import { IERC20Metadata } from '../../../../typechain' +import { whileImpersonating } from '../../../utils/impersonation' +import { BigNumberish } from 'ethers' +import { FORK_BLOCK } from './constants' +import { getResetFork } from '../helpers' + +export const mintPAXG = async (paxg: IERC20Metadata, amount: BigNumberish, recipient: string) => { + const supplyControllerAddr = '0xE25a329d385f77df5D4eD56265babe2b99A5436e' + + await whileImpersonating(supplyControllerAddr, async (supplyController) => { + const paxg2 = new ethers.Contract(paxg.address, [ + 'function increaseSupply(uint256 _value) external returns (bool success)', + ]) + + await paxg2.connect(supplyController).increaseSupply(amount) + await paxg.connect(supplyController).transfer(recipient, amount) + }) +} + +export const resetFork = getResetFork(FORK_BLOCK) diff --git a/test/plugins/individual-collateral/ethena/USDeFiatCollateral.test.ts b/test/plugins/individual-collateral/ethena/USDeFiatCollateral.test.ts index 9624aaa70a..1278ad168a 100644 --- a/test/plugins/individual-collateral/ethena/USDeFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/ethena/USDeFiatCollateral.test.ts @@ -210,6 +210,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, collateralName: 'USDe Fiat Collateral', diff --git a/test/plugins/individual-collateral/ethx/ETHxCollateral.test.ts b/test/plugins/individual-collateral/ethx/ETHxCollateral.test.ts index 10b4da87c3..76e9b022b6 100644 --- a/test/plugins/individual-collateral/ethx/ETHxCollateral.test.ts +++ b/test/plugins/individual-collateral/ethx/ETHxCollateral.test.ts @@ -281,6 +281,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, resetFork, diff --git a/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts b/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts index 180a889352..8d6a18ccb0 100644 --- a/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts @@ -257,6 +257,7 @@ all.forEach((curr: FTokenEnumeration) => { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, resetFork, diff --git a/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts b/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts index b15e1df41e..045c30766b 100644 --- a/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts +++ b/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts @@ -309,6 +309,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it.skip, itChecksRefPerTokDefault: it.skip, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itHasRevenueHiding: it.skip, // implemented in this file itChecksNonZeroDefaultThreshold: it, resetFork, diff --git a/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts b/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts index 2ed9bc5e4d..79db72699b 100644 --- a/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts @@ -197,6 +197,7 @@ const opts = { itChecksNonZeroDefaultThreshold: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itHasRevenueHiding: it.skip, resetFork, collateralName: 'SFraxCollateral', diff --git a/test/plugins/individual-collateral/lido/L2LidoStakedEthTestSuite.test.ts b/test/plugins/individual-collateral/lido/L2LidoStakedEthTestSuite.test.ts index 184b1090aa..937cb647ec 100644 --- a/test/plugins/individual-collateral/lido/L2LidoStakedEthTestSuite.test.ts +++ b/test/plugins/individual-collateral/lido/L2LidoStakedEthTestSuite.test.ts @@ -277,6 +277,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, resetFork: getResetFork(FORK_BLOCK_BASE), diff --git a/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts b/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts index 366c8c81c2..29f16a6cfb 100644 --- a/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts +++ b/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts @@ -268,6 +268,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, resetFork, diff --git a/test/plugins/individual-collateral/meta-morpho/MetaMorphoFiatCollateral.test.ts b/test/plugins/individual-collateral/meta-morpho/MetaMorphoFiatCollateral.test.ts index f8138befb4..e4c3870a55 100644 --- a/test/plugins/individual-collateral/meta-morpho/MetaMorphoFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/meta-morpho/MetaMorphoFiatCollateral.test.ts @@ -174,6 +174,7 @@ const makeFiatCollateralTestSuite = ( itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, resetFork: getResetFork(FORK_BLOCK), diff --git a/test/plugins/individual-collateral/meta-morpho/MetaMorphoSelfReferentialCollateral.test.ts b/test/plugins/individual-collateral/meta-morpho/MetaMorphoSelfReferentialCollateral.test.ts index d243ec85aa..d430c1d90a 100644 --- a/test/plugins/individual-collateral/meta-morpho/MetaMorphoSelfReferentialCollateral.test.ts +++ b/test/plugins/individual-collateral/meta-morpho/MetaMorphoSelfReferentialCollateral.test.ts @@ -170,6 +170,7 @@ const makeFiatCollateralTestSuite = ( itChecksTargetPerRefDefaultUp: it.skip, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it.skip, itHasRevenueHiding: it, resetFork: getResetFork(FORK_BLOCK), diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts index 9a9b94fad7..0e88789da7 100644 --- a/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts @@ -367,6 +367,7 @@ const makeAaveFiatCollateralTestSuite = ( itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, resetFork: getResetFork(FORK_BLOCK), diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts index 937ec99e70..ef459b934d 100644 --- a/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts @@ -228,6 +228,7 @@ const makeAaveNonFiatCollateralTestSuite = ( itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, itIsPricedByPeg: true, diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts index 81404fe208..1480aeb115 100644 --- a/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts @@ -228,6 +228,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it.skip, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it.skip, itHasRevenueHiding: it, resetFork: getResetFork(FORK_BLOCK), diff --git a/test/plugins/individual-collateral/mountain/USDMCollateral.test.ts b/test/plugins/individual-collateral/mountain/USDMCollateral.test.ts index 570c60345f..417f7ba662 100644 --- a/test/plugins/individual-collateral/mountain/USDMCollateral.test.ts +++ b/test/plugins/individual-collateral/mountain/USDMCollateral.test.ts @@ -310,6 +310,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it.skip, // implemented in this file collateralName: 'USDM Collateral', diff --git a/test/plugins/individual-collateral/pirex-eth/ApxEthCollateral.test.ts b/test/plugins/individual-collateral/pirex-eth/ApxEthCollateral.test.ts index db143ad603..86afe5a9ad 100644 --- a/test/plugins/individual-collateral/pirex-eth/ApxEthCollateral.test.ts +++ b/test/plugins/individual-collateral/pirex-eth/ApxEthCollateral.test.ts @@ -377,6 +377,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it.skip, // implemented in this file itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it.skip, // implemented in this file resetFork, diff --git a/test/plugins/individual-collateral/pluginTestTypes.ts b/test/plugins/individual-collateral/pluginTestTypes.ts index aa70a23c10..58cbab572c 100644 --- a/test/plugins/individual-collateral/pluginTestTypes.ts +++ b/test/plugins/individual-collateral/pluginTestTypes.ts @@ -100,6 +100,9 @@ export interface CollateralTestSuiteFixtures // toggle on or off: tests that focus on price changes itChecksPriceChanges: Mocha.TestFunction | Mocha.PendingTestFunction + // toggle on or off: tests that focus on price change around refPerTok manipulation + itChecksPriceChangesRefPerTok: Mocha.TestFunction | Mocha.PendingTestFunction + // toggle on or off: tests that focus on revenue hiding (off if plugin does not hide revenue) itHasRevenueHiding: Mocha.TestFunction | Mocha.PendingTestFunction diff --git a/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts b/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts index f766a3bc08..27bd5a29d3 100644 --- a/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts @@ -275,6 +275,7 @@ const opts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, + itChecksPriceChangesRefPerTok: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, resetFork, diff --git a/test/scenario/DemurrageCollateral.test.ts b/test/scenario/DemurrageCollateral.test.ts new file mode 100644 index 0000000000..f5517836ff --- /dev/null +++ b/test/scenario/DemurrageCollateral.test.ts @@ -0,0 +1,656 @@ +import { loadFixture, setStorageAt } from '@nomicfoundation/hardhat-network-helpers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { getLatestBlockTimestamp } from '../utils/time' +import { expect } from 'chai' +import { BigNumber, ContractFactory } from 'ethers' +import { makeDecayFn } from '../utils/rewards' +import { ethers, upgrades } from 'hardhat' +import { IConfig } from '../../common/configuration' +import { bn, fp } from '../../common/numbers' +import { setOraclePrice } from '../utils/oracles' +import { + TEN_BPS_FEE, + ONE_PERCENT_FEE, + TWO_PERCENT_FEE, + FIFTY_BPS_FEE, +} from '../plugins/individual-collateral/dtf/constants' +import { + Asset, + BasketLibP1, + ERC20Mock, + IAssetRegistry, + RTokenAsset, + MockV3Aggregator, + TestIBackingManager, + TestIBasketHandler, + TestIMain, + TestIRevenueTrader, + TestIRToken, + DemurrageCollateral, +} from '../../typechain' +import { advanceTime } from '../utils/time' +import { defaultFixtureNoBasket, IMPLEMENTATION, Implementation } from '../fixtures' +import { CollateralStatus, ZERO_ADDRESS } from '../../common/constants' + +const describeP1 = IMPLEMENTATION == Implementation.P1 ? describe : describe.skip + +describeP1(`Demurrage Collateral - P${IMPLEMENTATION}`, () => { + const amt = fp('1') + + let owner: SignerWithAddress + let addr1: SignerWithAddress + + let tokens: ERC20Mock[] + let collateral: DemurrageCollateral[] + let initialWeights: BigNumber[] + + let uoaPerTokFeed: MockV3Aggregator + let uoaPerTargetFeed: MockV3Aggregator + let targetPerTokFeed: MockV3Aggregator + + let config: IConfig + + let main: TestIMain + let backingManager: TestIBackingManager + let rToken: TestIRToken + let assetRegistry: IAssetRegistry + let bh: TestIBasketHandler + let rTokenTrader: TestIRevenueTrader + let rsrTrader: TestIRevenueTrader + let rsrAsset: Asset + let rTokenAsset: RTokenAsset + + const calcBasketWeight = async ( + coll: DemurrageCollateral, + decayedAmt: BigNumber + ): Promise => { + const elapsed = (await getLatestBlockTimestamp()) - (await coll.T0()) + const decayFn = makeDecayFn(await coll.fee()) + return fp('1e18').div(decayFn(decayedAmt, elapsed)) + } + + describe('Demurrage Collateral', () => { + beforeEach(async () => { + ;[owner, addr1] = await ethers.getSigners() + + // Deploy fixture + ;({ assetRegistry, backingManager, config, main, rToken, rTokenTrader, rsrTrader, rsrAsset } = + await loadFixture(defaultFixtureNoBasket)) + + // Setup Factories + const BasketLibFactory: ContractFactory = await ethers.getContractFactory('BasketLibP1') + const basketLib: BasketLibP1 = await BasketLibFactory.deploy() + const BasketHandlerFactory: ContractFactory = await ethers.getContractFactory( + 'BasketHandlerP1', + { libraries: { BasketLibP1: basketLib.address } } + ) + + // Replace with reweightable basket handler + bh = await ethers.getContractAt( + 'TestIBasketHandler', + ( + await upgrades.deployProxy( + BasketHandlerFactory, + [main.address, config.warmupPeriod, true, true], + { + initializer: 'init', + kind: 'uups', + } + ) + ).address + ) + await setStorageAt(main.address, 204, bh.address) + await setStorageAt(rToken.address, 355, bh.address) + await setStorageAt(backingManager.address, 302, bh.address) + await setStorageAt(assetRegistry.address, 201, bh.address) + + // Update RTokenAsset + const RTokenAssetFactory: ContractFactory = await ethers.getContractFactory('RTokenAsset') + rTokenAsset = await RTokenAssetFactory.deploy(rToken.address, fp('1e6')) + await assetRegistry.connect(owner).swapRegistered(rTokenAsset.address) + }) + + context('Asymmetric DMRs', () => { + beforeEach(async () => { + const DemurrageCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'DemurrageCollateral' + ) + const ERC20Factory: ContractFactory = await ethers.getContractFactory('ERC20Mock') + const ChainlinkFactory: ContractFactory = await ethers.getContractFactory( + 'MockV3Aggregator' + ) + + /***** Replace the original 4 tokens with 4 demurrage collateral with asymmetric DMRs ***********/ + // The 4 versions of DemurrageCollateral, each with different DMR rate: + // 1. isFiat = false: {UoA/tok} (no default detection) + // 2. isFiat = true: {UoA/tok} (/w default detection) + // 3. targetUnitFeed0 = false: {UoA/tok} and {UoA/target} (/w default detection) + // 4. targetUnitFeed0 = true: {target/tok} and {UoA/target} (/w default detection) + + tokens = ( + await Promise.all([ + ERC20Factory.deploy('NAME1', 'TKN1'), + ERC20Factory.deploy('NAME2', 'TKN2'), + ERC20Factory.deploy('NAME3', 'TKN3'), + ERC20Factory.deploy('NAME4', 'TKN4'), + ]) + ) + + uoaPerTokFeed = await ChainlinkFactory.deploy(8, bn('1e8')) + uoaPerTargetFeed = await ChainlinkFactory.deploy(8, bn('1e8')) + targetPerTokFeed = await ChainlinkFactory.deploy(8, bn('1e8')) + + collateral = await Promise.all([ + await DemurrageCollateralFactory.deploy( + { + erc20: tokens[0].address, + targetName: ethers.utils.formatBytes32String('DMR10USD'), + priceTimeout: bn('604800'), + chainlinkFeed: uoaPerTokFeed.address, // {UoA/tok} + oracleError: fp('0.01').toString(), // 1% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.01'), + delayUntilDefault: bn('86400'), + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: TEN_BPS_FEE, + feed1: ZERO_ADDRESS, + timeout1: bn('0'), + error1: bn('0'), + } + ), + await DemurrageCollateralFactory.deploy( + { + erc20: tokens[1].address, + targetName: ethers.utils.formatBytes32String('DMR50EUR'), + priceTimeout: bn('604800'), + chainlinkFeed: uoaPerTokFeed.address, // {UoA/tok} + oracleError: fp('0.01').toString(), // 1% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.01'), + delayUntilDefault: bn('86400'), + }, + { + isFiat: true, + targetUnitFeed0: false, + fee: FIFTY_BPS_FEE, + feed1: ZERO_ADDRESS, + timeout1: bn('0'), + error1: bn('0'), + } + ), + await DemurrageCollateralFactory.deploy( + { + erc20: tokens[2].address, + targetName: ethers.utils.formatBytes32String('DMR100XAU'), + priceTimeout: bn('604800'), + chainlinkFeed: uoaPerTokFeed.address, // {UoA/tok} + oracleError: fp('0.01').toString(), // 1% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.01'), + delayUntilDefault: bn('86400'), + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: uoaPerTargetFeed.address, // {UoA/target} + timeout1: bn('86400').toString(), // 24 hr + error1: fp('0.01').toString(), // 1% + } + ), + await DemurrageCollateralFactory.deploy( + { + erc20: tokens[3].address, + targetName: ethers.utils.formatBytes32String('DMR200SPY'), + priceTimeout: bn('604800'), + chainlinkFeed: targetPerTokFeed.address, // {target/tok} + oracleError: fp('0.01').toString(), // 1% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.01'), + delayUntilDefault: bn('86400'), + }, + { + isFiat: false, + targetUnitFeed0: true, + fee: TWO_PERCENT_FEE, + feed1: uoaPerTargetFeed.address, // {UoA/target} + timeout1: bn('86400').toString(), // 24 hr + error1: fp('0.01').toString(), // 1% + } + ), + ]) + + for (let i = 0; i < collateral.length; i++) { + await assetRegistry.connect(owner).register(collateral[i].address) + await tokens[i].mint(addr1.address, amt) + await tokens[i].connect(addr1).approve(rToken.address, amt) + } + + initialWeights = await Promise.all( + collateral.map((coll) => calcBasketWeight(coll, fp('1'))) + ) + + await bh.connect(owner).setPrimeBasket( + tokens.map((t) => t.address), + initialWeights + ) + await bh.connect(owner).refreshBasket() + await advanceTime(Number(config.warmupPeriod) + 1) + await rToken.connect(addr1).issue(amt) + expect(await rToken.totalSupply()).to.equal(amt) + expect(await bh.status()).to.equal(CollateralStatus.SOUND) + expect(await bh.fullyCollateralized()).to.equal(true) + }) + + it('prices/pegPrices should be correct', async () => { + for (let i = 0; i < 3; i++) { + const [low, high, pegPrice] = await collateral[i].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('1')) + expect(pegPrice).to.equal(fp('1')) + } + const [low, high, pegPrice] = await collateral[3].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('1.0001')) // asymmetry from multiplying oracles together + expect(pegPrice).to.equal(fp('1')) + }) + + it('quantities in basket should start out near fp(1)', async () => { + const [erc20s, quantities] = await bh.quote(fp('1'), false, 2) + for (let i = 0; i < collateral.length; i++) { + expect(erc20s[i]).to.equal(tokens[i].address) + expect(quantities[i]).to.be.closeTo(fp('1'), fp('1').div(bn('1e5'))) + } + }) + + context('after 1 year', () => { + beforeEach(async () => { + await advanceTime(Number(bn('31535940'))) // 1 year - 60s + await uoaPerTokFeed.updateAnswer(bn('1e8')) + await uoaPerTargetFeed.updateAnswer(bn('1e8')) + await targetPerTokFeed.updateAnswer(bn('1e8')) + await setOraclePrice(rsrAsset.address, bn('1e8')) + + await assetRegistry.refresh() + expect(await bh.status()).to.equal(CollateralStatus.SOUND) + expect(await bh.fullyCollateralized()).to.equal(true) + }) + + it('oracle prices shouldnt change', async () => { + for (let i = 0; i < 3; i++) { + const [low, high, pegPrice] = await collateral[i].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('1')) + expect(pegPrice).to.equal(fp('1')) + } + const [low, high, pegPrice] = await collateral[3].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('1.0001')) // asymmetry from multiplying oracles together + expect(pegPrice).to.equal(fp('1')) + }) + + it('RToken quantities should decrease correctly per fee tier: [0.1%, 0.50%, 1%, 2%]', async () => { + const expected = [fp('0.999'), fp('0.995'), fp('0.99'), fp('0.98')] + + const [erc20s, quantities] = await bh.quote(fp('1'), false, 2) + for (let i = 0; i < collateral.length; i++) { + expect(erc20s[i]).to.equal(tokens[i].address) + expect(quantities[i]).to.be.closeTo(expected[i], expected[i].div(bn('1e6'))) + } + }) + + it('refreshBasket() should not restore the RToken to genesis peg', async () => { + const [erc20s, quantities] = await bh.quote(fp('1'), false, 2) + await expect(bh.connect(owner).refreshBasket()).to.emit(bh, 'BasketSet') + const [newERC20s, newQuantities] = await bh.quote(fp('1'), false, 2) + + expect(await bh.status()).to.equal(CollateralStatus.SOUND) + expect(await bh.fullyCollateralized()).to.equal(true) + for (let i = 0; i < collateral.length; i++) { + expect(erc20s[i]).to.equal(newERC20s[i]) + expect(quantities[i]).to.be.gt(newQuantities[i]) + expect(quantities[i]).to.be.lt(newQuantities[i].add(fp('1e-6'))) + } + }) + + it('setPrimeBasket() should not restore the RToken to genesis peg', async () => { + // First try refreshBasket() in isolation + const [erc20s, quantities] = await bh.quote(fp('1'), false, 2) + await bh.connect(owner).refreshBasket() + const [newERC20s, newQuantities] = await bh.quote(fp('1'), false, 2) + + expect(await bh.status()).to.equal(CollateralStatus.SOUND) + expect(await bh.fullyCollateralized()).to.equal(true) + for (let i = 0; i < collateral.length; i++) { + expect(erc20s[i]).to.equal(newERC20s[i]) + expect(quantities[i]).to.be.gt(newQuantities[i]) + expect(quantities[i]).to.be.lt(newQuantities[i].add(fp('1e-6'))) + } + + // Then try refreshBasket() after setPrimeBasket() + await bh.connect(owner).setPrimeBasket( + tokens.map((t) => t.address), + initialWeights + ) + await bh.connect(owner).refreshBasket() + const [newerERC20s, newerQuantities] = await bh.quote(fp('1'), false, 2) + + expect(await bh.status()).to.equal(CollateralStatus.SOUND) + expect(await bh.fullyCollateralized()).to.equal(true) + for (let i = 0; i < collateral.length; i++) { + expect(erc20s[i]).to.equal(newerERC20s[i]) + expect(quantities[i]).to.be.gt(newerQuantities[i]) + expect(quantities[i]).to.be.lt(newerQuantities[i].add(fp('1e-6'))) + } + }) + + it('should detect default and propagate through to prices/pegPrices correctly', async () => { + // 1. break uoaPerTokFeed + await uoaPerTokFeed.updateAnswer(bn('1e8').div(2)) + await assetRegistry.refresh() + + // token1 + let [low, high, pegPrice] = await collateral[0].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('0.5')) + expect(pegPrice).to.equal(fp('1')) + expect(await collateral[0].status()).to.equal(CollateralStatus.SOUND) + + // token2 + ;[low, high, pegPrice] = await collateral[1].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('0.5')) + expect(pegPrice).to.equal(fp('0.5')) + expect(await collateral[1].status()).to.equal(CollateralStatus.IFFY) + + // token3 + ;[low, high, pegPrice] = await collateral[2].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('0.5')) + expect(pegPrice).to.equal(fp('0.5')) + expect(await collateral[2].status()).to.equal(CollateralStatus.IFFY) + + // token4 + ;[low, high, pegPrice] = await collateral[3].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('1.0001')) + expect(pegPrice).to.equal(fp('1')) + expect(await collateral[3].status()).to.equal(CollateralStatus.SOUND) + + // 2. break uoaPerTargetFeed + await uoaPerTokFeed.updateAnswer(bn('1e8')) + await uoaPerTargetFeed.updateAnswer(bn('1e8').div(2)) + await assetRegistry.refresh() + + // token1 + ;[low, high, pegPrice] = await collateral[0].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('1')) + expect(pegPrice).to.equal(fp('1')) + expect(await collateral[0].status()).to.equal(CollateralStatus.SOUND) + + // token2 + ;[low, high, pegPrice] = await collateral[1].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('1')) + expect(pegPrice).to.equal(fp('1')) + expect(await collateral[1].status()).to.equal(CollateralStatus.SOUND) + + // token3 + ;[low, high, pegPrice] = await collateral[2].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('1')) + expect(pegPrice).to.equal(fp('2')) + expect(await collateral[2].status()).to.equal(CollateralStatus.IFFY) + + // token4 + ;[low, high, pegPrice] = await collateral[3].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('0.50005')) + expect(pegPrice).to.equal(fp('1')) + expect(await collateral[3].status()).to.equal(CollateralStatus.SOUND) + + // 3. break targetPerTokFeed + await uoaPerTargetFeed.updateAnswer(bn('1e8')) + await targetPerTokFeed.updateAnswer(bn('1e8').div(2)) + await assetRegistry.refresh() + + // token1 + ;[low, high, pegPrice] = await collateral[0].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('1')) + expect(pegPrice).to.equal(fp('1')) + expect(await collateral[0].status()).to.equal(CollateralStatus.SOUND) + + // token2 + ;[low, high, pegPrice] = await collateral[1].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('1')) + expect(pegPrice).to.equal(fp('1')) + expect(await collateral[1].status()).to.equal(CollateralStatus.SOUND) + + // token3 + ;[low, high, pegPrice] = await collateral[2].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('1')) + expect(pegPrice).to.equal(fp('1')) + expect(await collateral[2].status()).to.equal(CollateralStatus.SOUND) + + // token4 + ;[low, high, pegPrice] = await collateral[3].tryPrice() + expect(low.add(high).div(2)).to.equal(fp('0.50005')) + expect(pegPrice).to.equal(fp('0.5')) + expect(await collateral[3].status()).to.equal(CollateralStatus.IFFY) + }) + + it('should open revenue auctions with asymmetric tokens and minted RToken', async () => { + // Forward balances + const all = tokens.map((t) => t.address) + all.push(rToken.address) + await backingManager.forwardRevenue(all) + + // 0th token will have no balance because smallest DMR; rest should have some + for (let i = 1; i < tokens.length; i++) { + expect(await tokens[i].balanceOf(rTokenTrader.address)).to.be.gt(0) + expect(await tokens[i].balanceOf(rsrTrader.address)).to.be.gt(0) + } + expect(await rToken.balanceOf(rTokenTrader.address)).to.be.gt(0) + expect(await rToken.balanceOf(rsrTrader.address)).to.be.gt(0) + + // RTokenTrader should be able to open auctions for tokens[1], tokens[2], and tokens[3], as well as distribute RToken + await rTokenTrader.manageTokens( + [tokens[1].address, tokens[2].address, tokens[3].address, rToken.address], + [0, 0, 0, 0] + ) + expect(await rTokenTrader.tradesOpen()).to.equal(3) + await expect(rTokenTrader.manageTokens([tokens[0].address], [0])).to.be.revertedWith( + '0 balance' + ) + + // RSRTrader should be able to open auctions for tokens[1], tokens[2], and tokens[3], and RToken + await rsrTrader.manageTokens( + [tokens[1].address, tokens[2].address, tokens[3].address, rToken.address], + [0, 0, 0, 0] + ) + expect(await rsrTrader.tradesOpen()).to.equal(4) + await expect(rsrTrader.manageTokens([tokens[0].address], [0])).to.be.revertedWith( + '0 balance' + ) + }) + }) + }) + + context('Symmetric DMRs', () => { + beforeEach(async () => { + const DemurrageCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'DemurrageCollateral' + ) + const ERC20Factory: ContractFactory = await ethers.getContractFactory('ERC20Mock') + const ChainlinkFactory: ContractFactory = await ethers.getContractFactory( + 'MockV3Aggregator' + ) + + /***** Replace the original 4 tokens with demurrage collateral with the same DMR rate ***********/ + // The 4 versions of DemurrageCollateral: + // 1. isFiat = false: {UoA/tok} (no default detection) + // 2. isFiat = true: {UoA/tok} (/w default detection) + // 3. targetUnitFeed0 = false: {UoA/tok} and {UoA/target} (/w default detection) + // 4. targetUnitFeed0 = true: {target/tok} and {UoA/target} (/w default detection) + + tokens = ( + await Promise.all([ + ERC20Factory.deploy('NAME1', 'TKN1'), + ERC20Factory.deploy('NAME2', 'TKN2'), + ERC20Factory.deploy('NAME3', 'TKN3'), + ERC20Factory.deploy('NAME4', 'TKN4'), + ]) + ) + + uoaPerTokFeed = await ChainlinkFactory.deploy(8, bn('1e8')) + uoaPerTargetFeed = await ChainlinkFactory.deploy(8, bn('1e8')) + targetPerTokFeed = await ChainlinkFactory.deploy(8, bn('1e8')) + + collateral = await Promise.all([ + await DemurrageCollateralFactory.deploy( + { + erc20: tokens[0].address, + targetName: ethers.utils.formatBytes32String('DMR100USD'), + priceTimeout: bn('604800'), + chainlinkFeed: uoaPerTokFeed.address, // {UoA/tok} + oracleError: fp('0.01').toString(), // 1% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.01'), + delayUntilDefault: bn('86400'), + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: ZERO_ADDRESS, + timeout1: bn('0'), + error1: bn('0'), + } + ), + await DemurrageCollateralFactory.deploy( + { + erc20: tokens[1].address, + targetName: ethers.utils.formatBytes32String('DMR100EUR'), + priceTimeout: bn('604800'), + chainlinkFeed: uoaPerTokFeed.address, // {UoA/tok} + oracleError: fp('0.01').toString(), // 1% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.01'), + delayUntilDefault: bn('86400'), + }, + { + isFiat: true, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: ZERO_ADDRESS, + timeout1: bn('0'), + error1: bn('0'), + } + ), + await DemurrageCollateralFactory.deploy( + { + erc20: tokens[2].address, + targetName: ethers.utils.formatBytes32String('DMR100XAU'), + priceTimeout: bn('604800'), + chainlinkFeed: uoaPerTokFeed.address, // {UoA/tok} + oracleError: fp('0.01').toString(), // 1% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.01'), + delayUntilDefault: bn('86400'), + }, + { + isFiat: false, + targetUnitFeed0: false, + fee: ONE_PERCENT_FEE, + feed1: uoaPerTargetFeed.address, // {UoA/target} + timeout1: bn('86400').toString(), // 24 hr + error1: fp('0.01').toString(), // 1% + } + ), + await DemurrageCollateralFactory.deploy( + { + erc20: tokens[3].address, + targetName: ethers.utils.formatBytes32String('DMR100SPY'), + priceTimeout: bn('604800'), + chainlinkFeed: targetPerTokFeed.address, // {target/tok} + oracleError: fp('0.01').toString(), // 1% + oracleTimeout: bn('86400').toString(), // 24 hr + maxTradeVolume: fp('1e6').toString(), // $1m, + defaultThreshold: fp('0.01'), + delayUntilDefault: bn('86400'), + }, + { + isFiat: false, + targetUnitFeed0: true, + fee: ONE_PERCENT_FEE, + feed1: uoaPerTargetFeed.address, // {UoA/target} + timeout1: bn('86400').toString(), // 24 hr + error1: fp('0.01').toString(), // 1% + } + ), + ]) + + for (let i = 0; i < collateral.length; i++) { + await assetRegistry.connect(owner).register(collateral[i].address) + await tokens[i].mint(addr1.address, amt) + await tokens[i].connect(addr1).approve(rToken.address, amt) + } + + initialWeights = await Promise.all( + collateral.map((coll) => calcBasketWeight(coll, fp('1'))) + ) + + await bh.connect(owner).setPrimeBasket( + tokens.map((t) => t.address), + initialWeights + ) + await bh.connect(owner).refreshBasket() + await advanceTime(Number(config.warmupPeriod) + 1) + await rToken.connect(addr1).issue(amt) + expect(await rToken.totalSupply()).to.equal(amt) + expect(await bh.status()).to.equal(CollateralStatus.SOUND) + expect(await bh.fullyCollateralized()).to.equal(true) + }) + + context('after 1 year', () => { + beforeEach(async () => { + await advanceTime(Number(bn('31535940'))) // 1 year - 60s + await uoaPerTokFeed.updateAnswer(bn('1e8')) + await uoaPerTargetFeed.updateAnswer(bn('1e8')) + await targetPerTokFeed.updateAnswer(bn('1e8')) + await setOraclePrice(rsrAsset.address, bn('1e8')) + + await assetRegistry.refresh() + expect(await bh.status()).to.equal(CollateralStatus.SOUND) + expect(await bh.fullyCollateralized()).to.equal(true) + }) + + it('should open revenue auctions with minted RToken', async () => { + // Forward balances + const all = tokens.map((t) => t.address) + all.push(rToken.address) + await backingManager.forwardRevenue(all) + + // No tokens should have balances at traders + for (let i = 0; i < tokens.length; i++) { + expect(await tokens[i].balanceOf(rTokenTrader.address)).to.equal(0) + expect(await tokens[i].balanceOf(rsrTrader.address)).to.equal(0) + } + + // RTokenTrader should distribute its RToken + await rTokenTrader.manageTokens([rToken.address], [0]) + expect(await rTokenTrader.tradesOpen()).to.equal(0) + await expect(rTokenTrader.manageTokens([tokens[3].address], [0])).to.be.revertedWith( + '0 balance' + ) + + // RSRTrader should be able to open auctions for RToken + await rsrTrader.manageTokens([rToken.address], [0]) + expect(await rsrTrader.tradesOpen()).to.equal(1) + await expect(rsrTrader.manageTokens([tokens[3].address], [0])).to.be.revertedWith( + '0 balance' + ) + }) + }) + }) + }) +})