Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nars - snars - collateral plugin #1217

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions common/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ export interface ITokens {
// Mountain
USDM?: string
wUSDM?: string

// NUM
snARS?: string
nARS?: string
}

export type ITokensKeys = Array<keyof ITokens>
Expand Down Expand Up @@ -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
Expand All @@ -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',
Expand Down
33 changes: 33 additions & 0 deletions contracts/plugins/assets/num/README.md
Original file line number Diff line number Diff line change
@@ -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)`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please include a note on the oracle used here. iirc you run the oracle and update it rather frequently. detail the % deviation and timeout thresholds (assuming they are in your documentation, but would like to have it legible directly in this repo)

- `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
24 changes: 24 additions & 0 deletions contracts/plugins/assets/num/SnARSFiatCollateral.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}
81 changes: 81 additions & 0 deletions scripts/deployment/phase2-assets/collaterals/deploy_nARS.ts
Original file line number Diff line number Diff line change
@@ -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 = <IAssetCollDeployments>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 = <ICollateral>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
})
83 changes: 83 additions & 0 deletions scripts/deployment/phase2-assets/collaterals/deploy_snARS.ts
Original file line number Diff line number Diff line change
@@ -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 = <IAssetCollDeployments>getDeploymentFile(assetCollDeploymentFilename)

const deployedCollateral: string[] = []

/******** Deploy snARS Collateral - snARS **************************/

const nuARSCollateralFactory: ContractFactory = await hre.ethers.getContractFactory(
'FiatCollateral'
)

const collateral = <SnARSFiatCollateral>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
})
53 changes: 53 additions & 0 deletions scripts/verification/collateral-plugins/verify_snARS.ts
Original file line number Diff line number Diff line change
@@ -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 = <IAssetCollDeployments>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
})
29 changes: 29 additions & 0 deletions test/plugins/individual-collateral/collateralTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1003,6 +1004,34 @@ export default function fn<X extends CollateralFixtureContext>(
targetUnitOracle.address,
ORACLE_TIMEOUT
)
} else if (target == ethers.utils.formatBytes32String('ARS')) {
// ARS
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we want to throw an error if not targetting Base, right? seems to be the only chain on which nARS is deployed

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this removed? my point was that we just need to check if we're running the tests against base

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 <TestICollateral>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}`)
}
Expand Down
Loading