-
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
Unpriced collateral #1220
Closed
Closed
Unpriced collateral #1220
Changes from 9 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
e92cde8
UnpricedCollateral
tbrent cc3a807
add to factory
tbrent 879f44f
nit
tbrent bae1c19
reduce to just 1 parameter
tbrent be4b994
Merge branch 'demurrage-collateral' into unpriced-collateral
tbrent 39eebf2
scenario tests
tbrent 4f785bd
add revenue test
tbrent 2f9ebb0
Merge branch 'demurrage-collateral' into unpriced-collateral
tbrent c40b322
record deployments in factory
tbrent 947ee04
Merge branch 'demurrage-collateral' into unpriced-collateral
tbrent 3c3641e
Merge branch 'demurrage-collateral' into unpriced-collateral
tbrent b5557e8
Merge branch 'demurrage-collateral' into unpriced-collateral
tbrent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// SPDX-License-Identifier: BlueOak-1.0.0 | ||
pragma solidity 0.8.19; | ||
|
||
import "../../interfaces/IAsset.sol"; | ||
import "./Asset.sol"; | ||
|
||
/** | ||
* @title UnpricedCollateral | ||
* @notice Collateral plugin for tokens that are missing a price oracle | ||
* | ||
* Warning: This plugin CANNOT be used in an RToken that needs to rebalance | ||
* It should only go into immutable RTokens that cannot have their basket changed | ||
* | ||
* - tok = X | ||
* - ref = X | ||
* - target = X | ||
* - UoA = USD | ||
*/ | ||
contract UnpricedCollateral is ICollateral, VersionedAsset { | ||
using FixLib for uint192; | ||
|
||
CollateralStatus public constant status = CollateralStatus.SOUND; | ||
|
||
uint192 public constant refPerTok = FIX_ONE; | ||
|
||
uint192 public constant targetPerRef = FIX_ONE; | ||
|
||
uint192 public constant savedPegPrice = 0; | ||
|
||
uint192 public constant maxTradeVolume = 0; | ||
|
||
uint48 public constant lastSave = 0; | ||
|
||
// === Immutables === | ||
|
||
IERC20Metadata public immutable erc20; | ||
|
||
uint8 public immutable erc20Decimals; | ||
|
||
bytes32 public immutable targetName; | ||
|
||
constructor(IERC20Metadata _erc20) { | ||
require(address(_erc20) != address(0), "missing erc20"); | ||
erc20 = _erc20; | ||
erc20Decimals = _erc20.decimals(); | ||
targetName = bytes32(bytes(_erc20.symbol())); | ||
} | ||
|
||
// solhint-disable no-empty-blocks | ||
|
||
/// Should not revert | ||
/// Refresh saved prices | ||
function refresh() public virtual override {} | ||
|
||
function price() public view virtual override returns (uint192 low, uint192 high) { | ||
return (0, FIX_MAX); | ||
} | ||
|
||
/// @return {tok} The balance of the ERC20 in whole tokens | ||
function bal(address account) external view virtual returns (uint192) { | ||
return shiftl_toFix(erc20.balanceOf(account), -int8(erc20Decimals), FLOOR); | ||
} | ||
|
||
/// @return If the asset is an instance of ICollateral or not | ||
function isCollateral() external pure virtual returns (bool) { | ||
return true; | ||
} | ||
|
||
/// Claim rewards earned by holding a balance of the ERC20 token | ||
/// @custom:delegate-call | ||
function claimRewards() external virtual {} | ||
|
||
// solhint-enable no-empty-blocks | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
import { loadFixture, setStorageAt } from '@nomicfoundation/hardhat-network-helpers' | ||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' | ||
import { expect } from 'chai' | ||
import { ContractFactory } from 'ethers' | ||
import { ethers, upgrades } from 'hardhat' | ||
import { IConfig } from '../../common/configuration' | ||
import { bn, fp, toBNDecimals } from '../../common/numbers' | ||
import { MAX_UINT192 } from '../../common/constants' | ||
import { getTrade } from '../utils/trades' | ||
import { | ||
BasketLibP1, | ||
ERC20MockDecimals, | ||
IAssetRegistry, | ||
TestIBackingManager, | ||
TestIBasketHandler, | ||
TestIMain, | ||
TestIRevenueTrader, | ||
TestIRToken, | ||
RTokenAsset, | ||
UnpricedCollateral, | ||
} from '../../typechain' | ||
import { advanceTime } from '../utils/time' | ||
import { defaultFixtureNoBasket, IMPLEMENTATION, Implementation } from '../fixtures' | ||
import { CollateralStatus } from '../../common/constants' | ||
|
||
const describeP1 = IMPLEMENTATION == Implementation.P1 ? describe : describe.skip | ||
|
||
describeP1(`Unpriced Collateral - P${IMPLEMENTATION}`, () => { | ||
const amt = fp('1') | ||
|
||
let owner: SignerWithAddress | ||
let addr1: SignerWithAddress | ||
|
||
let tokens: ERC20MockDecimals[] | ||
let collateral: UnpricedCollateral[] | ||
let decimals: number[] | ||
|
||
let config: IConfig | ||
|
||
let main: TestIMain | ||
let backingManager: TestIBackingManager | ||
let rToken: TestIRToken | ||
let assetRegistry: IAssetRegistry | ||
let bh: TestIBasketHandler | ||
let rTokenAsset: RTokenAsset | ||
let rsrTrader: TestIRevenueTrader | ||
let rsr: ERC20MockDecimals | ||
|
||
describe('Unpriced Collateral', () => { | ||
beforeEach(async () => { | ||
;[owner, addr1] = await ethers.getSigners() | ||
|
||
// Deploy fixture | ||
let erc20s: ERC20MockDecimals[] | ||
;({ | ||
assetRegistry, | ||
backingManager, | ||
config, | ||
main, | ||
rToken, | ||
erc20s, | ||
rTokenAsset, | ||
rsrTrader, | ||
rsr, | ||
} = await loadFixture(defaultFixtureNoBasket)) | ||
|
||
// Setup Factories | ||
const BasketLibFactory: ContractFactory = await ethers.getContractFactory('BasketLibP1') | ||
const basketLib: BasketLibP1 = <BasketLibP1>await BasketLibFactory.deploy() | ||
const BasketHandlerFactory: ContractFactory = await ethers.getContractFactory( | ||
'BasketHandlerP1', | ||
{ libraries: { BasketLibP1: basketLib.address } } | ||
) | ||
const UnpricedCollateralFactory: ContractFactory = await ethers.getContractFactory( | ||
'UnpricedCollateral' | ||
) | ||
const ERC20Factory: ContractFactory = await ethers.getContractFactory('ERC20MockDecimals') | ||
|
||
// 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) | ||
|
||
decimals = [6, 8, 9, 18] | ||
|
||
tokens = <ERC20MockDecimals[]>( | ||
await Promise.all([ | ||
ERC20Factory.deploy('NAME1', 'TKN1', decimals[0]), | ||
ERC20Factory.deploy('NAME2', 'TKN2', decimals[1]), | ||
ERC20Factory.deploy('NAME3', 'TKN3', decimals[2]), | ||
ERC20Factory.deploy('NAME4', 'TKN4', decimals[3]), | ||
]) | ||
) | ||
|
||
collateral = <UnpricedCollateral[]>( | ||
await Promise.all(tokens.map((t) => UnpricedCollateralFactory.deploy(t.address))) | ||
) | ||
|
||
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) | ||
} | ||
|
||
// Append a priced collateral | ||
tokens.push(erc20s[0]) | ||
await erc20s[0].connect(addr1).mint(addr1.address, amt) | ||
await erc20s[0].connect(addr1).approve(rToken.address, amt) | ||
|
||
// Set basket to 4 UnpricedCollateral + 1 FiatCollateral, all unique targets | ||
await bh.connect(owner).setPrimeBasket( | ||
tokens.map((t) => t.address), | ||
[fp('1'), fp('1'), fp('1'), fp('1'), fp('1')] | ||
) | ||
await bh.connect(owner).refreshBasket() | ||
await advanceTime(Number(config.warmupPeriod) + 1) | ||
expect(await rToken.totalSupply()).to.equal(0) | ||
expect(await bh.status()).to.equal(CollateralStatus.SOUND) | ||
expect(await bh.fullyCollateralized()).to.equal(true) | ||
}) | ||
|
||
it('collateral interface is correct', async () => { | ||
for (let i = 0; i < collateral.length; i++) { | ||
expect(await collateral[i].erc20()).to.equal(tokens[i].address) | ||
expect(await collateral[i].erc20Decimals()).to.equal(decimals[i]) | ||
expect(await collateral[i].targetName()).to.equal( | ||
ethers.utils.formatBytes32String(`${await tokens[i].symbol()}`) | ||
) | ||
expect(await collateral[i].status()).to.equal(CollateralStatus.SOUND) | ||
expect(await collateral[i].refPerTok()).to.equal(fp('1')) | ||
expect(await collateral[i].targetPerRef()).to.equal(fp('1')) | ||
expect(await collateral[i].savedPegPrice()).to.equal(fp('0')) | ||
expect(await collateral[i].maxTradeVolume()).to.equal(fp('0')) | ||
expect(await collateral[i].lastSave()).to.equal(0) | ||
expect(await collateral[i].bal(addr1.address)).to.equal( | ||
(await tokens[i].balanceOf(addr1.address)).mul(bn('10').pow(18 - decimals[i])) | ||
) | ||
|
||
const [low, high] = await collateral[i].price() | ||
expect(low).to.equal(0) | ||
expect(high).to.equal(MAX_UINT192) | ||
} | ||
}) | ||
|
||
it('should issue and redeem RTokens correctly', async () => { | ||
// Issue | ||
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) | ||
for (let i = 0; i < collateral.length; i++) { | ||
expect(await tokens[i].balanceOf(backingManager.address)).to.equal( | ||
toBNDecimals(amt, decimals[i]) | ||
) | ||
} | ||
|
||
// Redeem | ||
await rToken.connect(addr1).redeem(amt) | ||
expect(await rToken.totalSupply()).to.equal(0) | ||
expect(await bh.status()).to.equal(CollateralStatus.SOUND) | ||
expect(await bh.fullyCollateralized()).to.equal(true) | ||
for (let i = 0; i < collateral.length; i++) { | ||
expect(await tokens[i].balanceOf(backingManager.address)).to.equal(0) | ||
expect(await tokens[i].balanceOf(addr1.address)).to.equal(amt) | ||
} | ||
}) | ||
|
||
it('should not be able to rebalance because never uncollateralized', async () => { | ||
await rToken.connect(addr1).issue(amt) | ||
await expect(backingManager.rebalance(0)).to.be.revertedWith('already collateralized') | ||
await expect(rTokenAsset.tryPrice()).to.be.revertedWith('invalid price') | ||
}) | ||
|
||
it('even IF it were to become undercollateralized by way of a hacked token, rebalance STILL should not haircut', async () => { | ||
await rToken.connect(addr1).issue(amt) | ||
expect(await rToken.basketsNeeded()).to.equal(amt) | ||
|
||
await tokens[0].burn(backingManager.address, 1) | ||
expect(await bh.fullyCollateralized()).to.equal(false) | ||
await expect(backingManager.rebalance(0)).to.be.revertedWith('BUs unpriced') | ||
await expect(rTokenAsset.tryPrice()).to.be.revertedWith('invalid price') | ||
}) | ||
|
||
it('can coexist with appreciating collateral and process RSR revenue auctions like normal', async () => { | ||
const initialStRSRBalance = await rsr.balanceOf(await main.stRSR()) | ||
|
||
// Simulate appreciation | ||
await tokens[4].mint(backingManager.address, amt) | ||
await backingManager.forwardRevenue([tokens[4].address]) | ||
await rsrTrader.manageTokens([tokens[4].address], [0]) | ||
expect(await rsrTrader.tradesOpen()).to.equal(1) | ||
|
||
// Bid in dutch auction | ||
await advanceTime(config.dutchAuctionLength.toNumber() - 5) | ||
const t = await ethers.getContractAt( | ||
'DutchTrade', | ||
( | ||
await getTrade(rsrTrader, tokens[4].address) | ||
).address | ||
) | ||
await rsr.connect(addr1).mint(addr1.address, amt) | ||
await rsr.connect(addr1).approve(t.address, amt) | ||
await t.connect(addr1).bid() | ||
|
||
// RSR should have been rewarded | ||
expect(await rsr.balanceOf(await main.stRSR())).to.be.gt(initialStRSRBalance) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
address erc20
=>uint256/uint192 fee
=>address collateral
is much better to query and track.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.
can't do that -- permissionless pass-in of all variables
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.
there is not just 1 collateral possible, per fee tier
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.
Sorry I meant for unpriced demurrage collateral. There aren't any specific parameters for those, I don't think right?
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.
that's 2 lines below and works the way you want it to
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.
well it's saving by address of the deployed collateral. maybe this comment should go in the other pr, but if we save a mapping like akshat suggested (and error on duplicates), then the fe can easily query the contract based on the erc20 addy. otherwise we're requiring a subgraph dependency