From abe8dcea8156cdd32f39c1549d36f1cf09f9ed78 Mon Sep 17 00:00:00 2001 From: Nicolas Lecouflet Date: Wed, 2 Oct 2024 18:27:22 +0200 Subject: [PATCH 1/4] feat: add USDA plugin and scripts --- common/configuration.ts | 6 + contracts/plugins/assets/angle/README.md | 21 ++ .../assets/angle/USDAFiatCollateral.sol | 25 ++ .../addresses/1-tmp-assets-collateral.json | 8 +- .../phase2-assets/collaterals/deploy_usda.ts | 92 ++++++++ .../collateral-plugins/verify_usda.ts | 59 +++++ scripts/verify_etherscan.ts | 3 +- .../angle/USDAFiatCollateral.test.ts | 223 ++++++++++++++++++ .../individual-collateral/angle/constants.ts | 17 ++ .../individual-collateral/angle/helpers.ts | 30 +++ 10 files changed, 480 insertions(+), 4 deletions(-) create mode 100644 contracts/plugins/assets/angle/README.md create mode 100644 contracts/plugins/assets/angle/USDAFiatCollateral.sol create mode 100644 scripts/deployment/phase2-assets/collaterals/deploy_usda.ts create mode 100644 scripts/verification/collateral-plugins/verify_usda.ts create mode 100644 test/plugins/individual-collateral/angle/USDAFiatCollateral.test.ts create mode 100644 test/plugins/individual-collateral/angle/constants.ts create mode 100644 test/plugins/individual-collateral/angle/helpers.ts diff --git a/common/configuration.ts b/common/configuration.ts index d5164fa63..38a2cc980 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -115,6 +115,10 @@ export interface ITokens { // Mountain USDM?: string wUSDM?: string + + // Angle + USDA?: string + stUSD?: string } export type ITokensKeys = Array @@ -256,6 +260,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = { sdUSDCUSDCPlus: '0x9bbF31E99F30c38a5003952206C31EEa77540BeF', USDe: '0x4c9edd5852cd905f086c759e8383e09bff1e68b3', sUSDe: '0x9D39A5DE30e57443BfF2A8307A4256c8797A3497', + USDA: '0x0000206329b97DB379d5E1Bf586BbDB969C63274', + stUSD: '0x0022228a2cc5E7eF0274A7Baa600d44da5aB5776', }, chainlinkFeeds: { RSR: '0x759bBC1be8F90eE6457C44abc7d443842a976d02', diff --git a/contracts/plugins/assets/angle/README.md b/contracts/plugins/assets/angle/README.md new file mode 100644 index 000000000..544c9479d --- /dev/null +++ b/contracts/plugins/assets/angle/README.md @@ -0,0 +1,21 @@ +# Angle USDA Collateral Plugin + +## Summary + +`USDA` is a stablecoin built by [Angle Labs](https://github.com/AngleProtocol) and pegged to the value of the dollar. `USDA` holders can stake their stablecoins for `stUSD` in order to earn a native yield. + +This plugin allows `stUSD` holders to use their tokens as collateral in the Reserve Protocol. + +`stUSD`is an ERC4626 vault, most similar to the DAI savings module. The redeemable `USDA` amount can be obtained by dividing `stUSD.totalAssets()` by `stUSD.totalSupply()`. + +`USDA` contract: + +`stUSD` contract: + +## Implementation + +### Units + +| tok | ref | target | UoA | +| ----- | ---- | ------ | --- | +| stUSD | USDA | USD | USD | \ No newline at end of file diff --git a/contracts/plugins/assets/angle/USDAFiatCollateral.sol b/contracts/plugins/assets/angle/USDAFiatCollateral.sol new file mode 100644 index 000000000..95904fece --- /dev/null +++ b/contracts/plugins/assets/angle/USDAFiatCollateral.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import { CollateralConfig } from "../AppreciatingFiatCollateral.sol"; +import { ERC4626FiatCollateral } from "../ERC4626FiatCollateral.sol"; + +/** + * @title USDA Fiat Collateral + * @notice Collateral plugin for USDA (Angle) + * tok = stUSD (ERC4626 vault) + * ref = USDA + * tar = USD + * UoA = USD + */ + +contract USDAFiatCollateral is ERC4626FiatCollateral { + /// config.erc20 must be stUSD + /// @param config.chainlinkFeed Feed units: {UoA/ref} + /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide + constructor(CollateralConfig memory config, uint192 revenueHiding) + ERC4626FiatCollateral(config, revenueHiding) + { + require(config.defaultThreshold != 0, "defaultThreshold zero"); + } +} diff --git a/scripts/addresses/1-tmp-assets-collateral.json b/scripts/addresses/1-tmp-assets-collateral.json index 32d89934a..72c0d2b38 100644 --- a/scripts/addresses/1-tmp-assets-collateral.json +++ b/scripts/addresses/1-tmp-assets-collateral.json @@ -61,7 +61,8 @@ "sfrxETH": "0x4c891fCa6319d492866672E3D2AfdAAA5bDcfF67", "apxETH": "0x05ffDaAA2aF48e1De1CE34d633db018a28e3B3F5", "sUSDe": "0x35081Ca24319835e5f759163F7e75eaB753e0b7E", - "pyUSD": "0xa5cde4fB1132daF8f4a0D3140859271208d944E9" + "pyUSD": "0xa5cde4fB1132daF8f4a0D3140859271208d944E9", + "stUSD": "0xb6aD2903fAC2db628624DeE61e217F50Ad54638E" }, "erc20s": { "stkAAVE": "0x4da27a545c0c5B758a6BA100e3a049001de870f5", @@ -123,6 +124,7 @@ "CVX": "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B", "apxETH": "0x9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6", "sUSDe": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497", - "pyUSD": "0x6c3ea9036406852006290770bedfcaba0e23a0e8" + "pyUSD": "0x6c3ea9036406852006290770bedfcaba0e23a0e8", + "stUSD": "0x0022228a2cc5E7eF0274A7Baa600d44da5aB5776" } -} +} \ No newline at end of file diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_usda.ts b/scripts/deployment/phase2-assets/collaterals/deploy_usda.ts new file mode 100644 index 000000000..640e7aaef --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_usda.ts @@ -0,0 +1,92 @@ +import fs from 'fs' +import hre from 'hardhat' +import { ContractFactory } from 'ethers' +import { expect } from 'chai' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { fp } from '../../../../common/numbers' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { USDAFiatCollateral } from '../../../../typechain' +import { + DELAY_UNTIL_DEFAULT, + PRICE_TIMEOUT, + ORACLE_TIMEOUT, + ORACLE_ERROR, +} from '../../../../test/plugins/individual-collateral/angle/constants' + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy USDA Collateral - stUSD **************************/ + let collateral: USDAFiatCollateral + + const USDAFiatCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'USDAFiatCollateral' + ) + + collateral = await USDAFiatCollateralFactory.connect(deployer).deploy( + { + priceTimeout: PRICE_TIMEOUT.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDC, + oracleError: ORACLE_ERROR.toString(), + erc20: networkConfig[chainId].tokens.stUSD, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: ORACLE_TIMEOUT.toString(), // 24 hr + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01').add(ORACLE_ERROR).toString(), // ~1.5% + delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), // 72h + }, + fp('1e-6').toString() + ) + + await collateral.deployed() + + console.log( + `Deployed USDA (stUSD) Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.stUSD = collateral.address + assetCollDeployments.erc20s.stUSD = networkConfig[chainId].tokens.stUSD + 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_usda.ts b/scripts/verification/collateral-plugins/verify_usda.ts new file mode 100644 index 000000000..05c0824f5 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_usda.ts @@ -0,0 +1,59 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { fp } from '../../../common/numbers' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { verifyContract } from '../../deployment/utils' +import { + PRICE_TIMEOUT, + ORACLE_ERROR, + ORACLE_TIMEOUT, + DELAY_UNTIL_DEFAULT, +} from '../../../test/plugins/individual-collateral/angle/constants' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + /******** Verify stUSD COllateral **************************/ + await verifyContract( + chainId, + deployments.collateral.stUSD, + [ + { + priceTimeout: PRICE_TIMEOUT.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDC, + oracleError: ORACLE_ERROR.toString(), + erc20: networkConfig[chainId].tokens.stUSD, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: ORACLE_TIMEOUT.toString(), + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01').add(ORACLE_ERROR).toString(), + delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), + }, + fp('1e-6').toString(), + ], + 'contracts/plugins/assets/angle/USDAFiatCollateral.sol:USDAFiatCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verify_etherscan.ts b/scripts/verify_etherscan.ts index 1266c5ce2..68ed0225a 100644 --- a/scripts/verify_etherscan.ts +++ b/scripts/verify_etherscan.ts @@ -79,7 +79,8 @@ async function main() { 'collateral-plugins/verify_re7weth.ts', 'collateral-plugins/verify_apxeth.ts', 'collateral-plugins/verify_USDe.ts', - 'collateral-plugins/verify_pyusd.ts' + 'collateral-plugins/verify_pyusd.ts', + 'collateral-plugins/verify_usda.ts' ) } else if (chainId == '8453' || chainId == '84531') { // Base L2 chains diff --git a/test/plugins/individual-collateral/angle/USDAFiatCollateral.test.ts b/test/plugins/individual-collateral/angle/USDAFiatCollateral.test.ts new file mode 100644 index 000000000..e43fe1b33 --- /dev/null +++ b/test/plugins/individual-collateral/angle/USDAFiatCollateral.test.ts @@ -0,0 +1,223 @@ +import collateralTests from '../collateralTests' +import { CollateralFixtureContext, CollateralOpts, MintCollateralFunc } from '../pluginTestTypes' +import { mintStUSD, mintUSDA } from './helpers' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { ContractFactory, BigNumberish, BigNumber } from 'ethers' +import { + ERC20Mock, + IERC20Metadata, + MockV3Aggregator, + MockV3Aggregator__factory, + TestICollateral, +} from '../../../../typechain' +import { pushOracleForward } from '../../../utils/oracles' +import { bn, fp } from '../../../../common/numbers' + +import { ONE_ADDRESS, ZERO_ADDRESS } from '../../../../common/constants' +import { whileImpersonating } from '../../../utils/impersonation' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + USDA, + StUSD, + USDA_USD_PRICE_FEED, + ORACLE_ERROR, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, + MAX_TRADE_VOL, + DELAY_UNTIL_DEFAULT, + DEFAULT_THRESHOLD, + FORK_BLOCK, +} from './constants' +import { getResetFork } from '../helpers' + +/* + Define deployment functions +*/ + +export const defaultUSDACollateralOpts: CollateralOpts = { + erc20: StUSD, + targetName: ethers.utils.formatBytes32String('USD'), + rewardERC20: ZERO_ADDRESS, + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: USDA_USD_PRICE_FEED, + oracleTimeout: ORACLE_TIMEOUT, + oracleError: ORACLE_ERROR, + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, // 72 hs + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: fp('0'), +} + +export const deployCollateral = async (opts: CollateralOpts = {}): Promise => { + opts = { ...defaultUSDACollateralOpts, ...opts } + + const USDAFiatCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'USDAFiatCollateral' + ) + const collateral = await USDAFiatCollateralFactory.deploy( + { + erc20: opts.erc20, + targetName: opts.targetName, + priceTimeout: opts.priceTimeout, + chainlinkFeed: opts.chainlinkFeed, + oracleError: opts.oracleError, + oracleTimeout: opts.oracleTimeout, + maxTradeVolume: opts.maxTradeVolume, + defaultThreshold: opts.defaultThreshold, + delayUntilDefault: opts.delayUntilDefault, + }, + opts.revenueHiding, + { gasLimit: 2000000000 } + ) + await collateral.deployed() + + // Push forward feed + await pushOracleForward(opts.chainlinkFeed!) + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return collateral +} + +const chainlinkDefaultAnswer = bn('1e8') + +type Fixture = () => Promise + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultUSDACollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const chainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, chainlinkDefaultAnswer) + ) + collateralOpts.chainlinkFeed = chainlinkFeed.address + + const stUSD = (await ethers.getContractAt('IERC20Metadata', StUSD)) as IERC20Metadata + const rewardToken = (await ethers.getContractAt('ERC20Mock', ZERO_ADDRESS)) as ERC20Mock + const collateral = await deployCollateral(collateralOpts) + const tok = await ethers.getContractAt('IERC20Metadata', await collateral.erc20()) + + return { + alice, + collateral, + chainlinkFeed, + tok, + stUSD, + rewardToken, + } + } + + return makeCollateralFixtureContext +} + +const mintCollateralTo: MintCollateralFunc = async ( + ctx: CollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintStUSD(ctx.tok, user, amount, recipient) +} + +const reduceTargetPerRef = async (ctx: CollateralFixtureContext, pctDecrease: BigNumberish) => { + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.sub(lastRound.answer.mul(pctDecrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) +} + +const increaseTargetPerRef = async (ctx: CollateralFixtureContext, pctIncrease: BigNumberish) => { + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.add(lastRound.answer.mul(pctIncrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) +} + +// prettier-ignore +const reduceRefPerTok = async ( + ctx: CollateralFixtureContext, + pctDecrease: BigNumberish +) => { + const usda = await ethers.getContractAt("IERC20Metadata", USDA) + const currentBal = await usda.balanceOf(ctx.tok.address) + const removeBal = currentBal.mul(pctDecrease).div(100) + await whileImpersonating(ctx.tok.address, async (stUSDSigner) => { + await usda.connect(stUSDSigner).transfer(ONE_ADDRESS, removeBal) + }) +} + +// prettier-ignore +const increaseRefPerTok = async ( + ctx: CollateralFixtureContext, + pctIncrease: BigNumberish +) => { + const usda = await ethers.getContractAt("IERC20Metadata", USDA) + const currentBal = await usda.balanceOf(ctx.tok.address) + const addBal = currentBal.mul(pctIncrease).div(100) + await mintUSDA(usda, ctx.alice!, addBal, ctx.tok.address) +} + +const getExpectedPrice = async (ctx: CollateralFixtureContext): Promise => { + const clData = await ctx.chainlinkFeed.latestRoundData() + const clDecimals = await ctx.chainlinkFeed.decimals() + + const refPerTok = await ctx.collateral.refPerTok() + + return clData.answer + .mul(bn(10).pow(18 - clDecimals)) + .mul(refPerTok) + .div(fp('1')) +} + +/* + 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, + itChecksTargetPerRefDefaultUp: it, + itChecksRefPerTokDefault: it, + itChecksPriceChanges: it, + itChecksNonZeroDefaultThreshold: it, + itHasRevenueHiding: it, + collateralName: 'USDA Fiat Collateral', + chainlinkDefaultAnswer, + itIsPricedByPeg: true, + resetFork: getResetFork(FORK_BLOCK), + toleranceDivisor: bn('1e8'), // 1 part in 100 million +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/angle/constants.ts b/test/plugins/individual-collateral/angle/constants.ts new file mode 100644 index 000000000..bee1693a5 --- /dev/null +++ b/test/plugins/individual-collateral/angle/constants.ts @@ -0,0 +1,17 @@ +import { bn, fp } from '../../../../common/numbers' +import { networkConfig } from '../../../../common/configuration' + +// Mainnet Addresses +export const USDA = networkConfig['31337'].tokens.USDA as string +export const StUSD = networkConfig['31337'].tokens.stUSD as string +export const USDA_USD_PRICE_FEED = networkConfig['31337'].chainlinkFeeds.USDC as string // we use USDC feed as USDA/USD is not available in chainlink, and USDA/USDC is always 1 through the transmuter +export const USDA_HOLDER = '0xEc0B13b2271E212E1a74D55D51932BD52A002961' +export const stUSD_HOLDER = '0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb' +export const PRICE_TIMEOUT = bn('604800') // 1 week +export const ORACLE_TIMEOUT = bn(86400) // 24h +export const ORACLE_ERROR = fp('0.005') // 0.5% +export const DEFAULT_THRESHOLD = fp('0.05') // 5% +export const DELAY_UNTIL_DEFAULT = bn(259200) // 72h +export const MAX_TRADE_VOL = bn(1000) + +export const FORK_BLOCK = 20871587 diff --git a/test/plugins/individual-collateral/angle/helpers.ts b/test/plugins/individual-collateral/angle/helpers.ts new file mode 100644 index 000000000..727b3d1c2 --- /dev/null +++ b/test/plugins/individual-collateral/angle/helpers.ts @@ -0,0 +1,30 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { IERC20Metadata } from '../../../../typechain' +import { whileImpersonating } from '../../../utils/impersonation' +import { BigNumberish } from 'ethers' +import { FORK_BLOCK, stUSD_HOLDER, USDA_HOLDER } from './constants' +import { getResetFork } from '../helpers' + +export const mintStUSD = async ( + stUSD: IERC20Metadata, + account: SignerWithAddress, + amount: BigNumberish, + recipient: string +) => { + await whileImpersonating(stUSD_HOLDER, async (whale) => { + await stUSD.connect(whale).transfer(recipient, amount) + }) +} + +export const mintUSDA = async ( + USDA: IERC20Metadata, + account: SignerWithAddress, + amount: BigNumberish, + recipient: string +) => { + await whileImpersonating(USDA_HOLDER, async (whale) => { + await USDA.connect(whale).transfer(recipient, amount) + }) +} + +export const resetFork = getResetFork(FORK_BLOCK) From f863486a0af0cb43ed496c9b9167ab550496bf21 Mon Sep 17 00:00:00 2001 From: Nicolas Lecouflet Date: Wed, 2 Oct 2024 18:35:04 +0200 Subject: [PATCH 2/4] docs: update plugin readme --- contracts/plugins/assets/angle/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/plugins/assets/angle/README.md b/contracts/plugins/assets/angle/README.md index 544c9479d..0a97166e5 100644 --- a/contracts/plugins/assets/angle/README.md +++ b/contracts/plugins/assets/angle/README.md @@ -2,11 +2,11 @@ ## Summary -`USDA` is a stablecoin built by [Angle Labs](https://github.com/AngleProtocol) and pegged to the value of the dollar. `USDA` holders can stake their stablecoins for `stUSD` in order to earn a native yield. +`USDA` is a stablecoin pegged to the dollar built by [Angle Labs](https://github.com/AngleProtocol). `USDA` holders can stake their stablecoins for `stUSD` in order to earn a native yield. This plugin allows `stUSD` holders to use their tokens as collateral in the Reserve Protocol. -`stUSD`is an ERC4626 vault, most similar to the DAI savings module. The redeemable `USDA` amount can be obtained by dividing `stUSD.totalAssets()` by `stUSD.totalSupply()`. +`stUSD` is an ERC4626 vault, most similar to the DAI savings module. The redeemable `USDA` amount can be obtained by dividing `stUSD.totalAssets()` by `stUSD.totalSupply()`. `USDA` contract: From 715c1c2f216591c839efc2ed9a66df451bdcf745 Mon Sep 17 00:00:00 2001 From: Nicolas Lecouflet Date: Thu, 3 Oct 2024 09:20:25 +0200 Subject: [PATCH 3/4] feat: add tokens config for arb/base --- common/configuration.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/configuration.ts b/common/configuration.ts index 38a2cc980..75a97d16f 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -519,6 +519,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = { sUSDbC: '0x4c80e24119cfb836cdf0a6b53dc23f04f7e652ca', wstETH: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452', STG: '0xE3B53AF74a4BF62Ae5511055290838050bf764Df', + USDA: '0x0000206329b97DB379d5E1Bf586BbDB969C63274', + stUSD: '0x0022228a2cc5E7eF0274A7Baa600d44da5aB5776', }, chainlinkFeeds: { DAI: '0x591e79239a7d679378ec8c847e5038150364c78f', // 0.3%, 24hr @@ -566,6 +568,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = { saArbUSDT: '', // TODO our wrapper. remove from deployment script after placing here USDM: '0x59d9356e565ab3a36dd77763fc0d87feaf85508c', wUSDM: '0x57f5e098cad7a3d1eed53991d4d66c45c9af7812', + USDA: '0x0000206329b97DB379d5E1Bf586BbDB969C63274', + stUSD: '0x0022228a2cc5E7eF0274A7Baa600d44da5aB5776', }, chainlinkFeeds: { ARB: '0xb2A824043730FE05F3DA2efaFa1CBbe83fa548D6', From 08237b8519812424ab7ebcff9df89803f526e7f6 Mon Sep 17 00:00:00 2001 From: Nicolas Lecouflet Date: Mon, 7 Oct 2024 11:41:25 +0200 Subject: [PATCH 4/4] refactor: collateral naming & rework test expected price function --- contracts/plugins/assets/angle/README.md | 2 +- ...ateral.sol => StakedUSDAFiatCollateral.sol} | 8 ++++---- scripts/addresses/1-tmp-assets-collateral.json | 3 +-- .../phase2-assets/collaterals/deploy_usda.ts | 14 +++++++------- .../collateral-plugins/verify_usda.ts | 2 +- ...est.ts => StakedUSDAFiatCollateral.test.ts} | 18 +++++++++--------- .../individual-collateral/angle/constants.ts | 2 +- 7 files changed, 24 insertions(+), 25 deletions(-) rename contracts/plugins/assets/angle/{USDAFiatCollateral.sol => StakedUSDAFiatCollateral.sol} (72%) rename test/plugins/individual-collateral/angle/{USDAFiatCollateral.test.ts => StakedUSDAFiatCollateral.test.ts} (92%) diff --git a/contracts/plugins/assets/angle/README.md b/contracts/plugins/assets/angle/README.md index 0a97166e5..c1d3ce65e 100644 --- a/contracts/plugins/assets/angle/README.md +++ b/contracts/plugins/assets/angle/README.md @@ -1,4 +1,4 @@ -# Angle USDA Collateral Plugin +# Angle Staked USDA (stUSD) Collateral Plugin ## Summary diff --git a/contracts/plugins/assets/angle/USDAFiatCollateral.sol b/contracts/plugins/assets/angle/StakedUSDAFiatCollateral.sol similarity index 72% rename from contracts/plugins/assets/angle/USDAFiatCollateral.sol rename to contracts/plugins/assets/angle/StakedUSDAFiatCollateral.sol index 95904fece..b8a86763a 100644 --- a/contracts/plugins/assets/angle/USDAFiatCollateral.sol +++ b/contracts/plugins/assets/angle/StakedUSDAFiatCollateral.sol @@ -2,18 +2,18 @@ pragma solidity 0.8.19; import { CollateralConfig } from "../AppreciatingFiatCollateral.sol"; -import { ERC4626FiatCollateral } from "../ERC4626FiatCollateral.sol"; +import { ERC4626FiatCollateral, IERC4626 } from "../ERC4626FiatCollateral.sol"; /** - * @title USDA Fiat Collateral - * @notice Collateral plugin for USDA (Angle) + * @title Staked USDA Fiat Collateral + * @notice Collateral plugin for Staked USDA (stUSD) * tok = stUSD (ERC4626 vault) * ref = USDA * tar = USD * UoA = USD */ -contract USDAFiatCollateral is ERC4626FiatCollateral { +contract StakedUSDAFiatCollateral is ERC4626FiatCollateral { /// config.erc20 must be stUSD /// @param config.chainlinkFeed Feed units: {UoA/ref} /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide diff --git a/scripts/addresses/1-tmp-assets-collateral.json b/scripts/addresses/1-tmp-assets-collateral.json index 72c0d2b38..720e401f6 100644 --- a/scripts/addresses/1-tmp-assets-collateral.json +++ b/scripts/addresses/1-tmp-assets-collateral.json @@ -61,8 +61,7 @@ "sfrxETH": "0x4c891fCa6319d492866672E3D2AfdAAA5bDcfF67", "apxETH": "0x05ffDaAA2aF48e1De1CE34d633db018a28e3B3F5", "sUSDe": "0x35081Ca24319835e5f759163F7e75eaB753e0b7E", - "pyUSD": "0xa5cde4fB1132daF8f4a0D3140859271208d944E9", - "stUSD": "0xb6aD2903fAC2db628624DeE61e217F50Ad54638E" + "pyUSD": "0xa5cde4fB1132daF8f4a0D3140859271208d944E9" }, "erc20s": { "stkAAVE": "0x4da27a545c0c5B758a6BA100e3a049001de870f5", diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_usda.ts b/scripts/deployment/phase2-assets/collaterals/deploy_usda.ts index 640e7aaef..96cad474c 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_usda.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_usda.ts @@ -13,7 +13,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { USDAFiatCollateral } from '../../../../typechain' +import { StakedUSDAFiatCollateral } from '../../../../typechain' import { DELAY_UNTIL_DEFAULT, PRICE_TIMEOUT, @@ -45,14 +45,14 @@ async function main() { const deployedCollateral: string[] = [] - /******** Deploy USDA Collateral - stUSD **************************/ - let collateral: USDAFiatCollateral + /******** Deploy Staked USDA (stUSD) Collateral **************************/ + let collateral: StakedUSDAFiatCollateral - const USDAFiatCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( - 'USDAFiatCollateral' + const StakedUSDAFiatCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'StakedUSDAFiatCollateral' ) - collateral = await USDAFiatCollateralFactory.connect(deployer).deploy( + collateral = await StakedUSDAFiatCollateralFactory.connect(deployer).deploy( { priceTimeout: PRICE_TIMEOUT.toString(), chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDC, @@ -70,7 +70,7 @@ async function main() { await collateral.deployed() console.log( - `Deployed USDA (stUSD) Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + `Deployed Staked USDA (stUSD) Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` ) await (await collateral.refresh()).wait() expect(await collateral.status()).to.equal(CollateralStatus.SOUND) diff --git a/scripts/verification/collateral-plugins/verify_usda.ts b/scripts/verification/collateral-plugins/verify_usda.ts index 05c0824f5..49ab9e205 100644 --- a/scripts/verification/collateral-plugins/verify_usda.ts +++ b/scripts/verification/collateral-plugins/verify_usda.ts @@ -49,7 +49,7 @@ async function main() { }, fp('1e-6').toString(), ], - 'contracts/plugins/assets/angle/USDAFiatCollateral.sol:USDAFiatCollateral' + 'contracts/plugins/assets/angle/StakedUSDAFiatCollateral.sol:StakedUSDAFiatCollateral' ) } diff --git a/test/plugins/individual-collateral/angle/USDAFiatCollateral.test.ts b/test/plugins/individual-collateral/angle/StakedUSDAFiatCollateral.test.ts similarity index 92% rename from test/plugins/individual-collateral/angle/USDAFiatCollateral.test.ts rename to test/plugins/individual-collateral/angle/StakedUSDAFiatCollateral.test.ts index e43fe1b33..379f26bae 100644 --- a/test/plugins/individual-collateral/angle/USDAFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/angle/StakedUSDAFiatCollateral.test.ts @@ -28,6 +28,7 @@ import { DELAY_UNTIL_DEFAULT, DEFAULT_THRESHOLD, FORK_BLOCK, + REVENUE_HIDING, } from './constants' import { getResetFork } from '../helpers' @@ -46,16 +47,16 @@ export const defaultUSDACollateralOpts: CollateralOpts = { maxTradeVolume: MAX_TRADE_VOL, defaultThreshold: DEFAULT_THRESHOLD, // 72 hs delayUntilDefault: DELAY_UNTIL_DEFAULT, - revenueHiding: fp('0'), + revenueHiding: REVENUE_HIDING, } export const deployCollateral = async (opts: CollateralOpts = {}): Promise => { opts = { ...defaultUSDACollateralOpts, ...opts } - const USDAFiatCollateralFactory: ContractFactory = await ethers.getContractFactory( - 'USDAFiatCollateral' + const StakedUSDAFiatCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'StakedUSDAFiatCollateral' ) - const collateral = await USDAFiatCollateralFactory.deploy( + const collateral = await StakedUSDAFiatCollateralFactory.deploy( { erc20: opts.erc20, targetName: opts.targetName, @@ -165,15 +166,15 @@ const increaseRefPerTok = async ( await mintUSDA(usda, ctx.alice!, addBal, ctx.tok.address) } +// Calculate the expected price based on the StakedUSDAFiatCollateral's tryPrice() implementation (inherited from AppreciatingFiatCollateral) const getExpectedPrice = async (ctx: CollateralFixtureContext): Promise => { const clData = await ctx.chainlinkFeed.latestRoundData() const clDecimals = await ctx.chainlinkFeed.decimals() - const refPerTok = await ctx.collateral.refPerTok() - + const underlyingRefPerTok = await ctx.collateral.underlyingRefPerTok() return clData.answer .mul(bn(10).pow(18 - clDecimals)) - .mul(refPerTok) + .mul(underlyingRefPerTok) .div(fp('1')) } @@ -213,11 +214,10 @@ const opts = { itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, itHasRevenueHiding: it, - collateralName: 'USDA Fiat Collateral', + collateralName: 'Staked USDA (stUSD) Fiat Collateral', chainlinkDefaultAnswer, itIsPricedByPeg: true, resetFork: getResetFork(FORK_BLOCK), - toleranceDivisor: bn('1e8'), // 1 part in 100 million } collateralTests(opts) diff --git a/test/plugins/individual-collateral/angle/constants.ts b/test/plugins/individual-collateral/angle/constants.ts index bee1693a5..a3b9019e4 100644 --- a/test/plugins/individual-collateral/angle/constants.ts +++ b/test/plugins/individual-collateral/angle/constants.ts @@ -13,5 +13,5 @@ export const ORACLE_ERROR = fp('0.005') // 0.5% export const DEFAULT_THRESHOLD = fp('0.05') // 5% export const DELAY_UNTIL_DEFAULT = bn(259200) // 72h export const MAX_TRADE_VOL = bn(1000) - +export const REVENUE_HIDING = fp('1e-6') export const FORK_BLOCK = 20871587