-
Notifications
You must be signed in to change notification settings - Fork 111
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
base: master
Are you sure you want to change the base?
Changes from all commits
6292c0b
34bec49
8e5470b
2259af3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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)`. | ||
- `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 |
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"); | ||
} | ||
} |
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 | ||
}) |
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 | ||
}) |
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 | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<X extends CollateralFixtureContext>( | |
targetUnitOracle.address, | ||
ORACLE_TIMEOUT | ||
) | ||
} else if (target == ethers.utils.formatBytes32String('ARS')) { | ||
// ARS | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}`) | ||
} | ||
|
There was a problem hiding this comment.
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)