diff --git a/common/configuration.ts b/common/configuration.ts index 9a561261b..1bea82955 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -117,7 +117,10 @@ export interface ITokens { USDM?: string wUSDM?: string + // Demurrage collateral PAXG?: string + cbBTC?: string + EURC?: string } export type ITokensKeys = Array @@ -130,6 +133,7 @@ export interface IFeeds { ETHUSD?: string wstETHstETH?: string XAU?: string + EUR?: string } export interface IPools { @@ -519,6 +523,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = { sUSDbC: '0x4c80e24119cfb836cdf0a6b53dc23f04f7e652ca', wstETH: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452', STG: '0xE3B53AF74a4BF62Ae5511055290838050bf764Df', + cbBTC: '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf', + EURC: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42', }, chainlinkFeeds: { DAI: '0x591e79239a7d679378ec8c847e5038150364c78f', // 0.3%, 24hr @@ -535,6 +541,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 }, GNOSIS_EASY_AUCTION: '0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02', // mock COMET_REWARDS: '0x123964802e6ABabBE1Bc9547D72Ef1B69B00A6b1', diff --git a/contracts/plugins/assets/DemurrageCollateral.sol b/contracts/plugins/assets/DemurrageCollateral.sol index 462ffb578..e42365a85 100644 --- a/contracts/plugins/assets/DemurrageCollateral.sol +++ b/contracts/plugins/assets/DemurrageCollateral.sol @@ -4,8 +4,10 @@ pragma solidity 0.8.19; import "./FiatCollateral.sol"; struct DemurrageConfig { - bool isFiat; - uint192 fee; // {1/s} per-second inflation/deflation of refPerTok/targetPerRef + 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} @@ -16,18 +18,20 @@ struct DemurrageConfig { /** * @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} * - * 1 feed: - * - feed0 {UoA/tok} + * under 1 feed: + * - feed0/chainlinkFeed must be {UoA/tok} * - apply issuance premium IFF isFiat is true * 2 feeds: - * - feed0 {target/tok} - * - feed1 {UoA/target} - * - apply issuance premium using feed0 + * - feed0: targetUnitFeed0 ? {target/tok} : {UoA/tok} + * - feed1: {UoA/target} + * - apply issuance premium * * - tok = Tokenized X - * - ref = Virtual (inflationary) X - * - target = 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 { @@ -35,9 +39,10 @@ contract DemurrageCollateral is FiatCollateral { using OracleLib for AggregatorV3Interface; bool internal immutable isFiat; + bool internal immutable targetUnitFeed0; // if true: feed0 is {target/tok} - // For each token, we maintain up to two feeds/timeouts/errors - AggregatorV3Interface internal immutable feed0; // {UoA/tok} or {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} @@ -45,25 +50,28 @@ contract DemurrageCollateral is FiatCollateral { uint192 internal immutable error1; // {1} // immutable in spirit -- cannot be because of FiatCollateral's targetPerRef() call - // TODO would love to find a way to make these immutable + // TODO would love to find a way to make these immutable for gas reasons uint48 public t0; // {s} deployment timestamp - uint192 public fee; // {1/s} demurrage fee; manifests as reference unit inflation + uint192 public fee; // {1/s} demurrage fee; target unit deflation - /// @param config.chainlinkFeed unused - /// @param config.oracleTimeout unused - /// @param config.oracleError unused - /// @param demurrageConfig.fee {1/s} fraction of the reference unit to inflate each second - /// @param demurrageConfig.feed0 {UoA/tok} or {target/tok} + /// @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; @@ -81,8 +89,7 @@ contract DemurrageCollateral is FiatCollateral { /// 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 unadjusted price observed in the peg - /// can be 0 if only 1 feed AND not fiat + /// @return pegPrice {target/tok} The un-decayed pegPrice function tryPrice() external view @@ -93,45 +100,52 @@ contract DemurrageCollateral is FiatCollateral { uint192 pegPrice ) { - pegPrice = FIX_ONE; // undecayed rate that won't trigger default or issuance premium + // 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 - // Use only 1 feed if 2nd feed not defined; else multiply together - // if only 1 feed: `y` is FIX_ONE and `yErr` is 0 + pegPrice = FIX_ONE; // undecayed rate that won't trigger default or issuance premium - uint192 x = feed0.price(timeout0); // initially {UoA/tok} + uint192 x = feed0.price(timeout0); // {UoA/tok} uint192 xErr = error0; - uint192 y = FIX_ONE; - uint192 yErr; - if (address(feed1) != address(0)) { - y = feed1.price(timeout1); // {UoA/target} - yErr = error1; - // {target/ref} = {UoA/target} - pegPrice = y; // no demurrage needed + 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/ref} = {UoA/tok} - pegPrice = x; // no demurrage needed + // {target/tok} = {UoA/tok} because {target} == {UoA} + pegPrice = x; } - // {UoA/tok} = {UoA/ref} * {ref/tok} - low = x.mul(FIX_ONE - xErr).mul(y.mul(FIX_ONE - yErr), FLOOR); - high = x.mul(FIX_ONE + xErr).mul(y.mul(FIX_ONE + yErr), CEIL); assert(low <= high); } // === Demurrage rates === - // invariant: targetPerRef() * refPerTok() ~= FIX_ONE - /// @return {ref/tok} Quantity of whole reference units per whole collateral tokens function refPerTok() public view override returns (uint192) { - // up-only - return FIX_ONE.div(targetPerRef(), FLOOR); - } + // Monotonically increasing due to target unit (and reference unit) deflation - /// @return {target/ref} Quantity of whole target units per whole reference unit in the peg - function targetPerRef() public view override returns (uint192) { - // down-only - return FIX_ONE.minus(fee).powu(uint48(block.timestamp - t0)); + 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 000000000..82a1a4ae2 --- /dev/null +++ b/docs/demurrage-collateral.md @@ -0,0 +1,56 @@ +# 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 a synthetic reference unit that refers to a falling quantity of the collateral token. This causes the reference unit to become inflationary with respect to the collateral unit, resulting in a monotonically increasing `refPerTok()` by definition. + +There are side-effects to the `targetName`, however the rest of the collateral plugin remains the same. + +### 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 `1704085200` + +### Target Unit + +``` +targetPerRef(): 1 +``` + +`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 what would have otherwise been the target unit had the collateral been purely SelfReferential + +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. + +### 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. + +For example, say an asset has had 2% total demurrage since January 1st 2024 and you want to (on today's date) create a basket of that is worth $1: the correct basket weight to provide to `setPrimeBasket()` would be `1 / (1 - 0.02) = ~1.0204`. + +To calculate total demurrage since 2024-01-01 00:00:00 UTC, use: + +``` +fee() ^ (seconds_since_2024_01_01) +``` + +(where `fee()` is the per-second demurrage rate found on the `DemurrageCollateral` contract below) + +### Implementation + +[DemurrageCollateral.sol](../contracts/plugins/assets/DemurrageCollateral.sol) implements a generalized demurrage collateral plugin that should support almost all use-cases diff --git a/scripts/addresses/8453-tmp-assets-collateral.json b/scripts/addresses/8453-tmp-assets-collateral.json index 4602fbd2d..9b5fcc8af 100644 --- a/scripts/addresses/8453-tmp-assets-collateral.json +++ b/scripts/addresses/8453-tmp-assets-collateral.json @@ -11,7 +11,9 @@ "cbETH": "0x851B461a9744f4c9E996C03072cAB6f44Fa04d0D", "saBasUSDC": "0xC19f5d60e2Aca1174f3D5Fe189f0A69afaB76f50", "cUSDCv3": "0xf7a9D27c3B60c78c6F6e2c2d6ED6E8B94b352461", - "wstETH": "0x8b4374005291B8FCD14C4E947604b2FB3C660A73" + "wstETH": "0x8b4374005291B8FCD14C4E947604b2FB3C660A73", + "EURC": "0x7321485aA1D0439296B882bBc85Eb0BD350F8381", + "cbBTC": "0x06f7D10f5842fc5816DF9A9DD65f84481B1490E3" }, "erc20s": { "COMP": "0x9e1028F5F1D5eDE59748FFceE5532509976840E0", @@ -23,6 +25,8 @@ "saBasUSDC": "0x6F6f81e5E66f503184f2202D83a79650c3285759", "STG": "0xE3B53AF74a4BF62Ae5511055290838050bf764Df", "cUSDCv3": "0x53f1Df4E5591Ae35Bf738742981669c3767241FA", - "wstETH": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452" + "wstETH": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", + "EURC": "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42", + "cbBTC": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" } -} +} \ No newline at end of file diff --git a/scripts/deploy.ts b/scripts/deploy.ts index a1347953a..89a30a347 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -90,7 +90,8 @@ async function main() { 'phase2-assets/collaterals/deploy_USDe.ts', 'phase2-assets/assets/deploy_crv.ts', 'phase2-assets/assets/deploy_cvx.ts', - 'phase2-assets/collaterals/deploy_pyusd.ts' + 'phase2-assets/collaterals/deploy_pyusd.ts', + 'phase2-assets/collaterals/deploy_paxg.ts' ) } else if (chainId == '8453' || chainId == '84531') { // Base L2 chains @@ -103,6 +104,8 @@ async function main() { 'phase2-assets/collaterals/deploy_aave_v3_usdc.ts', 'phase2-assets/collaterals/deploy_lido_wsteth_collateral.ts', 'phase2-assets/collaterals/deploy_cbeth_collateral.ts', + 'phase2-assets/collaterals/deploy_cbbtc.ts', + 'phase2-assets/collaterals/deploy_eurc.ts', 'phase2-assets/assets/deploy_stg.ts' ) } else if (chainId == '42161' || chainId == '421614') { diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_cbbtc.ts b/scripts/deployment/phase2-assets/collaterals/deploy_cbbtc.ts new file mode 100644 index 000000000..63ef1b77d --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_cbbtc.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.cbBTC = collateral.address + assetCollDeployments.erc20s.cbBTC = 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.ts b/scripts/deployment/phase2-assets/collaterals/deploy_eurc.ts new file mode 100644 index 000000000..b3559219b --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_eurc.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.EURC = collateral.address + assetCollDeployments.erc20s.EURC = 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.ts b/scripts/deployment/phase2-assets/collaterals/deploy_paxg.ts new file mode 100644 index 000000000..95333b35e --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_paxg.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.PAXG = collateral.address + assetCollDeployments.erc20s.PAXG = 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_cbbtc.ts b/scripts/verification/collateral-plugins/verify_cbbtc.ts new file mode 100644 index 000000000..112a85a28 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_cbbtc.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.cbBTC! + ) + + await verifyContract( + chainId, + assetCollDeployments.collateral.cbBTC, + [ + { + 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.ts b/scripts/verification/collateral-plugins/verify_eurc.ts new file mode 100644 index 000000000..759cce433 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_eurc.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.EURC! + ) + + await verifyContract( + chainId, + assetCollDeployments.collateral.EURC, + [ + { + 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.ts b/scripts/verification/collateral-plugins/verify_paxg.ts new file mode 100644 index 000000000..f99deaba2 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_paxg.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.PAXG! + ) + + await verifyContract( + chainId, + assetCollDeployments.collateral.PAXG, + [ + { + 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/scripts/verify_etherscan.ts b/scripts/verify_etherscan.ts index a448a9ee8..5df61ad6e 100644 --- a/scripts/verify_etherscan.ts +++ b/scripts/verify_etherscan.ts @@ -80,7 +80,8 @@ async function main() { 'collateral-plugins/verify_ethx.ts', 'collateral-plugins/verify_apxeth.ts', 'collateral-plugins/verify_USDe.ts', - 'collateral-plugins/verify_pyusd.ts' + 'collateral-plugins/verify_pyusd.ts', + 'collateral-plugins/verify_paxg.ts' ) } else if (chainId == '8453' || chainId == '84531') { // Base L2 chains @@ -89,7 +90,9 @@ async function main() { 'collateral-plugins/verify_aave_v3_usdc.ts', 'collateral-plugins/verify_wsteth.ts', 'collateral-plugins/verify_cbeth.ts', - 'assets/verify_stg.ts' + 'assets/verify_stg.ts', + 'collateral-plugins/verify_cbbtc.ts', + 'collateral-plugins/verify_eurc.ts' ) } else if (chainId == '42161' || chainId == '421614') { // Arbitrum One diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index fcc719230..e934bcbaa 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -1016,7 +1016,7 @@ export default function fn( targetUnitOracle.address, ORACLE_TIMEOUT ) - } else if (target === ethers.utils.formatBytes32String('XAU')) { + } else if (target.indexOf(ethers.utils.formatBytes32String('XAU'))) { if (onBase || onArbitrum) throw new Error('PAXG only supported on mainnet') // PAXG @@ -1030,7 +1030,7 @@ export default function fn( return await DemurrageFactory.deploy( { erc20: erc20.address, - targetName: ethers.utils.formatBytes32String('XAU'), + targetName: target, priceTimeout: PRICE_TIMEOUT, chainlinkFeed: chainlinkFeed.address, oracleError: ORACLE_ERROR, @@ -1041,6 +1041,7 @@ export default function fn( }, { isFiat: false, + targetUnitFeed0: false, fee: bn('0'), feed1: ZERO_ADDRESS, timeout1: bn(0), diff --git a/test/plugins/individual-collateral/dtf/PAXGCollateralTestSuite.test.ts b/test/plugins/individual-collateral/dtf/PAXGCollateralTestSuite.test.ts index 673fa5ea6..f6543bcd9 100644 --- a/test/plugins/individual-collateral/dtf/PAXGCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/dtf/PAXGCollateralTestSuite.test.ts @@ -11,6 +11,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { DELAY_UNTIL_DEFAULT, PAXG, + ONE_PERCENT_FEE, ORACLE_ERROR, ORACLE_TIMEOUT, PRICE_TIMEOUT, @@ -28,14 +29,14 @@ interface PAXGCollateralOpts extends CollateralOpts { export const defaultPAXGCollateralOpts: PAXGCollateralOpts = { erc20: PAXG, - targetName: ethers.utils.formatBytes32String('XAU'), + 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: fp('1e-9'), // about 3.1% annually + fee: ONE_PERCENT_FEE, } export const deployCollateral = async (opts: PAXGCollateralOpts = {}): Promise => { @@ -58,6 +59,7 @@ export const deployCollateral = async (opts: PAXGCollateralOpts = {}): Promise { + const amt = fp('1') + + let owner: SignerWithAddress + let addr1: SignerWithAddress + + let tokens: ERC20Mock[] + let collateral: DemurrageCollateral[] + + 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 + + describe('Demurrage Collateral', () => { + beforeEach(async () => { + ;[owner, addr1] = await ethers.getSigners() + + // Deploy fixture + ;({ assetRegistry, backingManager, config, main, rToken } = 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 } } + ) + const DemurrageCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'DemurrageCollateral' + ) + const ERC20Factory: ContractFactory = await ethers.getContractFactory('ERC20Mock') + const ChainlinkFactory: ContractFactory = await ethers.getContractFactory('MockV3Aggregator') + + // 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) + + /***** Replace the original 4 tokens with 4 demurrage collateral ***********/ + // 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('DMR5000USD'), + 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: FIFTY_PERCENT_ANNUALLY, + feed1: ZERO_ADDRESS, + timeout1: bn('0'), + error1: bn('0'), + } + ), + await DemurrageCollateralFactory.deploy( + { + erc20: tokens[1].address, + targetName: ethers.utils.formatBytes32String('DMR5000EUR'), + 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_PERCENT_ANNUALLY, + feed1: ZERO_ADDRESS, + timeout1: bn('0'), + error1: bn('0'), + } + ), + await DemurrageCollateralFactory.deploy( + { + erc20: tokens[2].address, + targetName: ethers.utils.formatBytes32String('DMR5000XAU'), + 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: FIFTY_PERCENT_ANNUALLY, + 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('DMR5000SPY'), + 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: FIFTY_PERCENT_ANNUALLY, + 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) + } + + await bh.connect(owner).setPrimeBasket( + tokens.map((t) => t.address), + [fp('1'), fp('1'), fp('1'), fp('1')] + ) + 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 should be correct', 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('31535955'))) // 1 year - 45s + await uoaPerTokFeed.updateAnswer(bn('1e8')) + await uoaPerTargetFeed.updateAnswer(bn('1e8')) + await targetPerTokFeed.updateAnswer(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 have decreased ~50%', 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) + const expected = fp('1').div(2) + expect(quantities[i]).to.be.closeTo(expected, expected.div(bn('1e6'))) + } + }) + + it('Excess should accrue as revenue', async () => { + const [bottom] = await bh.basketsHeldBy(backingManager.address) + expect(bottom).to.be.closeTo(amt.mul(2), amt.div(bn('1e3'))) + }) + + it('refreshBasket() should not restore the RToken back 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 () => { + const [erc20s, quantities] = await bh.quote(fp('1'), false, 2) + await bh.connect(owner).setPrimeBasket( + tokens.map((t) => t.address), + [fp('1'), fp('1'), fp('1'), fp('1')] + ) + 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'))) + } + }) + + 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) + }) + }) + }) +})