diff --git a/common/configuration.ts b/common/configuration.ts index d5164fa63..3bcca6e62 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -115,6 +115,10 @@ export interface ITokens { // Mountain USDM?: string wUSDM?: string + + // NUM + snARS?: string + nARS?: string } export type ITokensKeys = Array @@ -513,6 +517,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = { sUSDbC: '0x4c80e24119cfb836cdf0a6b53dc23f04f7e652ca', wstETH: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452', STG: '0xE3B53AF74a4BF62Ae5511055290838050bf764Df', + snARS: '0xC1F4C75e8925A67BE4F35D6b1c044B5ea8849a58', + nARS: '0x5e40f26E89213660514c51Fb61b2d357DBf63C85', }, chainlinkFeeds: { DAI: '0x591e79239a7d679378ec8c847e5038150364c78f', // 0.3%, 24hr @@ -529,6 +535,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { stETHETH: '0xf586d0728a47229e747d824a939000Cf21dEF5A0', // 0.5%, 24h ETHUSD: '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70', // 0.15%, 20min wstETHstETH: '0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061', // 0.5%, 24h + nARS: '0xC3a426Ef79fd60A4cA785FC04a2C3cB09d2FEeae', // 0.5%, 15min }, GNOSIS_EASY_AUCTION: '0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02', // mock COMET_REWARDS: '0x123964802e6ABabBE1Bc9547D72Ef1B69B00A6b1', diff --git a/contracts/plugins/assets/num/README.md b/contracts/plugins/assets/num/README.md new file mode 100644 index 000000000..4750be93b --- /dev/null +++ b/contracts/plugins/assets/num/README.md @@ -0,0 +1,33 @@ +# Collateral Plugins for nARS and snARS + +## Summary + +This plugin allows `nARS` and `snARS` holders to use their tokens as collateral in the Reserve Protocol. + +As described in the [Num Site](https://new.num.finance/) nTokens are ERC20 tokens, tracking the value of an underlying financial asset. +Each nToken issued by Num Finance is fully collateralized by an asset in the traditional market. This means that for every nToken in circulation, there is a real-world asset backing it, ensuring the token's value and stability. + +In this particular case we're incorporating through this plugin 2 nTokens. + +- `nARS` is a stablecoin pegged to the `Argentine Peso (ARS)`. +- `snARS` is the staked version of `nARS`. When users stake their `nARS`, they receive `snARS` in return, which grants them certain benefits in the form of yield or Numun Rewards. + +Staking of `nARS` is possible at: https://numun.fi/ +Official num website: https://num.finance/ +nStables documentation: https://docs.nstables.fi/ + +## Implementation + +### Units + +| tok | ref | target | UoA | +| ---- | --- | ------ | --- | +| nARS | ARS | ARS | USD | + +| tok | ref | target | UoA | +| ----- | ---- | ------ | --- | +| sNARS | nARS | ARS | USD | + +### claimRewards() + +There are no rewards to claim diff --git a/contracts/plugins/assets/num/SnARSFiatCollateral.sol b/contracts/plugins/assets/num/SnARSFiatCollateral.sol new file mode 100644 index 000000000..d63a6e804 --- /dev/null +++ b/contracts/plugins/assets/num/SnARSFiatCollateral.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import { CollateralConfig } from "../AppreciatingFiatCollateral.sol"; +import { ERC4626FiatCollateral } from "../ERC4626FiatCollateral.sol"; + +/** + * @title SnARSFiatCollateral + * @notice Collateral plugin for a Num vault with fiat collateral + * tok = sNARS + * ref = nARS + * tar = ARS + * UoA = USD + */ +contract SnARSFiatCollateral is ERC4626FiatCollateral { + /// config.erc20 must be a Num ERC4626 vault + /// @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/deployment/phase2-assets/collaterals/deploy_nARS.ts b/scripts/deployment/phase2-assets/collaterals/deploy_nARS.ts new file mode 100644 index 000000000..96d899e46 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_nARS.ts @@ -0,0 +1,81 @@ +import fs from 'fs' +import hre, { ethers } from 'hardhat' +import { ICollateral } from '@typechain/ICollateral' +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 { 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}`) + } + + // Only exists on Base chain + if (!baseL2Chains.includes(hre.network.name)) { + throw new Error(`Invalid network ${hre.network.name} - only available on Base chain`) + } + + // 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[] = [] + + let collateral: ICollateral + + /******** Deploy NARS Collateral - ARS **************************/ + + const { collateral: narsCollateral } = await hre.run('deploy-fiat-collateral', { + priceTimeout: priceTimeout.toString(), + priceFeed: networkConfig[chainId].chainlinkFeeds.nARS, + oracleError: fp('0.005').toString(), // 0.5% + tokenAddress: networkConfig[chainId].tokens.nARS, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '900', + targetName: hre.ethers.utils.formatBytes32String('ARS'), + defaultThreshold: fp('0.015').toString(), // 1.5% + delayUntilDefault: bn('86400').toString(), // 24h + }) + + collateral = await ethers.getContractAt('ICollateral', narsCollateral) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.nARS = narsCollateral + assetCollDeployments.erc20s.nARS = networkConfig[chainId].tokens.nARS + deployedCollateral.push(narsCollateral.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed nARS asset 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_snARS.ts b/scripts/deployment/phase2-assets/collaterals/deploy_snARS.ts new file mode 100644 index 000000000..93a2cc801 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_snARS.ts @@ -0,0 +1,83 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { bn, fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { priceTimeout } from '../../utils' +import { SnARSFiatCollateral } 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 snARS Collateral - snARS **************************/ + + const nuARSCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'FiatCollateral' + ) + + const collateral = await nuARSCollateralFactory.connect(deployer).deploy( + { + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.snARS, + oracleError: fp('0.005').toString(), // 0.5% + erc20: networkConfig[chainId].tokens.snARS, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '900', // 15min + targetName: hre.ethers.utils.formatBytes32String('ARS'), + defaultThreshold: fp('0.015').toString(), // 1.5% = 0.5% oracleError + 1% buffer + delayUntilDefault: bn('86400').toString(), // 24h + }, + '0' // revenueHiding = 0 + ) + await collateral.deployed() + + console.log(`Deployed snARS to ${hre.network.name} (${chainId}): ${collateral.address}`) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.snARS = collateral.address + assetCollDeployments.erc20s.snARS = networkConfig[chainId].tokens.snARS + 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_snARS.ts b/scripts/verification/collateral-plugins/verify_snARS.ts new file mode 100644 index 000000000..1f587bbe6 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_snARS.ts @@ -0,0 +1,53 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { fp, bn } from '../../../common/numbers' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { priceTimeout, verifyContract } from '../../deployment/utils' + +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 snARS **************************/ + await verifyContract( + chainId, + deployments.collateral.snARS, + [ + { + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.snARS, + oracleError: fp('0.01').toString(), // 1% + erc20: networkConfig[chainId].tokens.snARS, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '3600', // 1 hr + targetName: hre.ethers.utils.formatBytes32String('ARS'), + defaultThreshold: fp('0.02').toString(), // 2% + delayUntilDefault: bn('86400').toString(), // 24h + }, + '0', // revenueHiding = 0 + ], + 'contracts/plugins/assets/num/SnARSFiatCollateral.sol:SnARSFiatCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 5a1cc4692..4057a3bb4 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -58,6 +58,7 @@ import { } from '../../../typechain' import snapshotGasCost from '../../utils/snapshotGasCost' import { IMPLEMENTATION, Implementation, ORACLE_ERROR, PRICE_TIMEOUT } from '../../fixtures' +import { NUM_HOLDER } from './num/constants' const getDescribeFork = (targetNetwork = 'mainnet') => { return useEnv('FORK') && useEnv('FORK_NETWORK') === targetNetwork ? describe : describe.skip @@ -1003,6 +1004,34 @@ export default function fn( targetUnitOracle.address, ORACLE_TIMEOUT ) + } else if (target == ethers.utils.formatBytes32String('ARS')) { + // ARS + if (!onBase) throw new Error('nARS/snARS only available on base') + + const erc20 = await ethers.getContractAt( + 'IERC20Metadata', + onBase ? networkConfig[chainId].tokens.snARS! : networkConfig[chainId].tokens.snARS! + ) + const whale = NUM_HOLDER + await whileImpersonating(whale, async (signer) => { + await erc20 + .connect(signer) + .transfer(addr1.address, await erc20.balanceOf(signer.address)) + }) + const FiatCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'FiatCollateral' + ) + return await FiatCollateralFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: chainlinkFeed.address, + oracleError: ORACLE_ERROR, + oracleTimeout: ORACLE_TIMEOUT, + maxTradeVolume: MAX_UINT192, + erc20: erc20.address, + targetName: ethers.utils.formatBytes32String('ARS'), + defaultThreshold: fp('0.015'), // 1.5% + delayUntilDefault: bn('86400'), // 24h, + }) } else { throw new Error(`Unknown target: ${target}`) } diff --git a/test/plugins/individual-collateral/num/SNARSFiatCollateral.test.ts b/test/plugins/individual-collateral/num/SNARSFiatCollateral.test.ts new file mode 100644 index 000000000..fd2d68ad3 --- /dev/null +++ b/test/plugins/individual-collateral/num/SNARSFiatCollateral.test.ts @@ -0,0 +1,205 @@ +import { networkConfig } from '#/common/configuration' +import { bn, fp } from '#/common/numbers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { MockV3Aggregator } from '@typechain/MockV3Aggregator' +import { TestICollateral } from '@typechain/TestICollateral' +import { MockV3Aggregator__factory } from '@typechain/index' +import { expect } from 'chai' +import { BigNumber, BigNumberish, ContractFactory } from 'ethers' +import { ethers } from 'hardhat' +import collateralTests from '../collateralTests' +import { getResetFork } from '../helpers' +import { CollateralOpts, CollateralFixtureContext } from '../pluginTestTypes' +import { pushOracleForward } from '../../../utils/oracles' +import { MAX_UINT192 } from '#/common/constants' +import { + DELAY_UNTIL_DEFAULT, + FORK_BLOCK, + ORACLE_TIMEOUT, + ORACLE_ERROR, + PRICE_TIMEOUT, +} from './constants' +import { mintCollateralTo } from './helpers' + +interface MAFiatCollateralOpts extends CollateralOpts { + defaultPrice?: BigNumberish + defaultRefPerTok?: BigNumberish +} + +const makeFiatCollateralTestSuite = ( + collateralName: string, + defaultCollateralOpts: MAFiatCollateralOpts +) => { + const deployCollateral = async (opts: MAFiatCollateralOpts = {}): Promise => { + opts = { ...defaultCollateralOpts, ...opts } + const NumCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'SnARSFiatCollateral' + ) + + const collateral = await NumCollateralFactory.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 chainlink feed + await pushOracleForward(opts.chainlinkFeed!) + + await expect(collateral.refresh()) + + return collateral + } + + type Fixture = () => Promise + + const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + inOpts: MAFiatCollateralOpts = {} + ): Fixture => { + const makeCollateralFixtureContext = async () => { + const opts = { ...defaultCollateralOpts, ...inOpts } + + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const chainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, opts.defaultPrice!) + ) + opts.chainlinkFeed = chainlinkFeed.address + + // Hack: use wrapped vault by default unless the maxTradeVolume is infinite, in which + // case the mock would break things. Care! Fragile! + if (!opts.maxTradeVolume || !MAX_UINT192.eq(opts.maxTradeVolume)) { + const mockNumFactory = await ethers.getContractFactory('MockNum4626') + const mockERC4626 = await mockNumFactory.deploy(opts.erc20!) + opts.erc20 = mockERC4626.address + } + const collateral = await deployCollateral({ ...opts }) + const tok = await ethers.getContractAt('IERC20Metadata', await collateral.erc20()) + + return { + alice, + collateral, + chainlinkFeed, + tok, + } as CollateralFixtureContext + } + + return makeCollateralFixtureContext + } + + 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) + } + + const reduceRefPerTok = async (ctx: CollateralFixtureContext, pctDecrease: BigNumberish) => { + const mockERC4626 = await ethers.getContractAt('MockNum4626', ctx.tok.address) + await mockERC4626.applyMultiple(bn('100').sub(pctDecrease).mul(fp('1')).div(100)) + } + + const increaseRefPerTok = async (ctx: CollateralFixtureContext, pctIncrease: BigNumberish) => { + const mockERC4626 = await ethers.getContractAt('MockNum4626', ctx.tok.address) + await mockERC4626.applyMultiple(bn('100').add(pctIncrease).mul(fp('1')).div(100)) + } + + 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 () => {} + + 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, + resetFork: getResetFork(FORK_BLOCK), + collateralName, + chainlinkDefaultAnswer: defaultCollateralOpts.defaultPrice!, + itIsPricedByPeg: true, + toleranceDivisor: bn('1e7'), // 1 part in 10 million + targetNetwork: 'base', + } + + collateralTests(opts) +} + +const makeOpts = ( + vault: string, + chainlinkFeed: string, + oracleTimeout: BigNumber, + oracleError: BigNumber +): MAFiatCollateralOpts => { + return { + targetName: ethers.utils.formatBytes32String('ARS'), + priceTimeout: PRICE_TIMEOUT, + oracleTimeout: oracleTimeout, + oracleError: oracleError, + defaultThreshold: oracleError.add(fp('0.01')), + delayUntilDefault: DELAY_UNTIL_DEFAULT, + maxTradeVolume: fp('1e6'), + revenueHiding: fp('0'), + defaultPrice: bn('1e8'), + defaultRefPerTok: fp('1'), + erc20: vault, + chainlinkFeed, + } +} + +/* + Run the test suite +*/ +const { tokens, chainlinkFeeds } = networkConfig[8453] +makeFiatCollateralTestSuite( + 'FiatCollateral - snARS', + makeOpts(tokens.snARS!, chainlinkFeeds.snARS!, ORACLE_TIMEOUT, ORACLE_ERROR) +) diff --git a/test/plugins/individual-collateral/num/constants.ts b/test/plugins/individual-collateral/num/constants.ts new file mode 100644 index 000000000..2f58aab1a --- /dev/null +++ b/test/plugins/individual-collateral/num/constants.ts @@ -0,0 +1,15 @@ +import { bn, fp } from '../../../../common/numbers' +import { networkConfig } from '../../../../common/configuration' + +// oracle settings +export const ORACLE_FEED = networkConfig['8453'].chainlinkFeeds.nARS! +export const ORACLE_TIMEOUT = bn('900') +export const ORACLE_ERROR = fp('0.005') + +// general +export const PRICE_TIMEOUT = bn(604800) // 1 week +export const DELAY_UNTIL_DEFAULT = bn(86400) + +// tests +export const FORK_BLOCK = 20493295 +export const NUM_HOLDER = '0xF3F1a405bc844FB3322587a305B1a8b2EC916536' diff --git a/test/plugins/individual-collateral/num/helpers.ts b/test/plugins/individual-collateral/num/helpers.ts new file mode 100644 index 000000000..6e45e56a8 --- /dev/null +++ b/test/plugins/individual-collateral/num/helpers.ts @@ -0,0 +1,55 @@ +import { whileImpersonating } from '#/utils/impersonation' +import { BigNumberish } from 'ethers' +import { FORK_BLOCK, NUM_HOLDER } from './constants' +import { getResetFork } from '../helpers' +import { CollateralFixtureContext, MintCollateralFunc } from '../pluginTestTypes' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import hre, { ethers } from 'hardhat' + +/** + * Mint collateral to a recipient using a whale. + * @param ctx The CollateralFixtureContext object. + * @param amount The amount of collateral to mint. + * @param _ The signer with address (not used in this function). + * @param recipient The address of the recipient of the minted collateral. + */ +export const mintCollateralTo: MintCollateralFunc = async ( + ctx: CollateralFixtureContext, + amount: BigNumberish, + _: SignerWithAddress, + recipient: string +) => { + const tok = await ethers.getContractAt('MockNum4626', ctx.tok.address) + + // It can be a MockMetaMorpho4626 or the real ERC4626 + try { + // treat it as a wrapper to begin + const actual = await tok.actual() + const underlying = await ethers.getContractAt('IERC20Metadata', actual) + + // Transfer the underlying (real) ERC4626; wrapper is pass-through + await whileImpersonating(hre, NUM_HOLDER, async (whaleSigner) => { + await underlying.connect(whaleSigner).transfer(recipient, amount) + }) + } catch (e) { + // if we error out, then it's not the wrapper we're dealing with + await whileImpersonating(hre, NUM_HOLDER, async (whaleSigner) => { + await ctx.tok.connect(whaleSigner).transfer(recipient, amount) + }) + } +} + +export const mintNARSTo: MintCollateralFunc = async ( + ctx: CollateralFixtureContext, + amount: BigNumberish, + _: SignerWithAddress, + recipient: string +) => { + // treat it as a wrapper to begin + const underlying = await ethers.getContractAt('IERC20Metadata', ctx.tok.address) + await whileImpersonating(hre, NUM_HOLDER, async (whaleSigner) => { + await underlying.connect(whaleSigner).transfer(recipient, amount) + }) +} + +export const resetFork = getResetFork(FORK_BLOCK)