8000 WstETHPriceFeed + tests by scott-silver · Pull Request #600 · compound-finance/comet · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

WstETHPriceFeed + tests #600

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

Merged
merged 12 commits into from
Oct 20, 2022
Merged
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
76 changes: 76 additions & 0 deletions contracts/WstETHPriceFeed.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.15;

import "./vendor/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

interface IWstETH {
function decimals() external view returns (uint8);
function tokensPerStEth() external view returns (uint256);
}

contract WstETHPriceFeed is AggregatorV3Interface {
/** Custom errors **/
error InvalidInt256();
error NotImplemented();

string public constant override description = "Custom price feed for wstETH / USD";

uint public constant override version = 1;

/// @notice Scale for returned prices
uint8 public override decimals = 8;

/// @notice Chainlink stETH / USD price feed
address public immutable stETHtoUSDPriceFeed;

/// @notice WstETH contract address
address public immutable wstETH;

/// @notice scale for WstETH contract
uint public immutable wstETHScale;

constructor(address stETHtoUSDPriceFeed_, address wstETH_) {
stETHtoUSDPriceFeed = stETHtoUSDPriceFeed_;
wstETH = wstETH_;
wstETHScale = 10 ** IWstETH(wstETH).decimals();
}

function signed256(uint256 n) internal pure returns (int256) {
if (n > uint256(type(int256).max)) revert InvalidInt256();
return int256(n);
}

/**
* @notice Unimplemented function required to fulfill AggregatorV3Interface; always reverts
**/
function getRoundData(uint80 _roundId) override external view returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) {
revert NotImplemented();
}

/**
* @notice WstETH Price for the latest round
* @return roundId Round id from the stETH price feed
* @return answer Latest price for wstETH / USD
* @return startedAt Timestamp when the round was started; passed on from stETH price feed
* @return updatedAt Timestamp when the round was last updated; passed on from stETH price feed
* @return answeredInRound Round id in which the answer was computed; passed on from stETH price feed
**/
function latestRoundData() override external view returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) {
(uint80 roundId, int256 stETHPrice, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = AggregatorV3Interface(stETHtoUSDPriceFeed).latestRoundData();
uint256 tokensPerStEth = IWstETH(wstETH).tokensPerStEth();
int price = stETHPrice * signed256(wstETHScale) / signed256(tokensPerStEth);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

⛑️ Updated the logic of this line; I believe this is correct, but let me know if you agree.

The price is now:

stETHPrice * wstETHScale / tokensPerEth

e.g.:

1300e8 * 1e18 / .91e18 == ~1428e8

return (roundId, price, startedAt, updatedAt, answeredInRound);
}
}
50 changes: 26 additions & 24 deletions contracts/test/SimplePriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,42 @@ pragma solidity 0.8.15;
import "../vendor/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract SimplePriceFeed is AggregatorV3Interface {
int public price;

uint8 public immutable override decimals;

string public constant override description = "Mock Chainlink price aggregator";

uint public constant override version = 1;

constructor(int initialPrice, uint8 decimals_) {
price = initialPrice;
uint8 public immutable override decimals;

uint80 internal roundId;
int256 internal answer;
uint256 internal startedAt;
uint256 internal updatedAt;
uint80 internal answeredInRound;

constructor(int answer_, uint8 decimals_) {
answer = answer_;
decimals = decimals_;
}

function setPrice(int price_) public {
price = price_;
function setRoundData(
uint80 roundId_,
int256 answer_,
uint256 startedAt_,
uint256 updatedAt_,
uint80 answeredInRound_
) public {
roundId = roundId_;
answer = answer_;
startedAt = startedAt_;
updatedAt = updatedAt_;
answeredInRound = answeredInRound_;
}

function getRoundData(uint80 _roundId) override external view returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) {
return (_roundId, price, 0, 0, 0);
function getRoundData(uint80 roundId_) override external view returns (uint80, int256, uint256, uint256, uint80) {
return (roundId_, answer, startedAt, updatedAt, answeredInRound);
}

function latestRoundData() override external view returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) {
return (0, price, 0, 0, 0);
function latestRoundData() override external view returns (uint80, int256, uint256, uint256, uint80) {
return (roundId, answer, startedAt, updatedAt, answeredInRound);
}
}
12 changes: 12 additions & 0 deletions contracts/test/SimpleWstETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.15;

contract SimpleWstETH {
uint8 public constant decimals = 18;

uint public immutable tokensPerStEth;

constructor(uint tokensPerStEth_) {
tokensPerStEth = tokensPerStEth_;
}
}
20 changes: 10 additions & 10 deletions test/absorb-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('absorb', function () {
expect(lU1.numAbsorbed).to.be.equal(0);
expect(lU1.approxSpend).to.be.equal(0);

const usdcPrice = await priceFeeds['USDC'].price();
const [_, usdcPrice] = await priceFeeds['USDC'].latestRoundData();
Copy link
Contributor

Choose a reason for hiding this comment

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

So we were accessing the stored price directly before? I guess latestRoundData is actually more correct since that's what we use in Comet and the official interface?

Copy link
Contributor Author

Choose a reason for hiding 5D40 this comment

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

yeah, i had made price a public variable, which was a mistake.

all the variables are now internal and you have to access them via latestRoundData or getRoundData, which are the functions defined by the AggregatorV3Interface

const baseScale = await comet.baseScale();
expect(event(a0, 0)).to.be.deep.equal({
AbsorbDebt: {
Expand Down Expand Up @@ -136,7 +136,7 @@ describe('absorb', function () {
//expect(lA1.approxSpend).to.be.equal(459757131288n);
expect(lA1.approxSpend).to.be.lt(a0.receipt.gasUsed.mul(a0.receipt.effectiveGasPrice));

const usdcPrice = await priceFeeds['USDC'].price();
const [_, usdcPrice] = await priceFeeds['USDC'].latestRoundData();
const baseScale = await comet.baseScale();
expect(event(a0, 0)).to.be.deep.equal({
AbsorbDebt: {
Expand Down Expand Up @@ -247,10 +247,10 @@ describe('absorb', function () {
//expect(lA1.approxSpend).to.be.equal(130651238630n);
expect(lA1.approxSpend).to.be.lt(a0.receipt.gasUsed.mul(a0.receipt.effectiveGasPrice));

const usdcPrice = await priceFeeds['USDC'].price();
const compPrice = await priceFeeds['COMP'].price();
const wbtcPrice = await priceFeeds['WBTC'].price();
const wethPrice = await priceFeeds['WETH'].price();
const [_a, usdcPrice] = await priceFeeds['USDC'].latestRoundData();
const [_b, compPrice] = await priceFeeds['COMP'].latestRoundData();
const [_c, wbtcPrice] = await priceFeeds['WBTC'].latestRoundData();
const [_d, wethPrice] = await priceFeeds['WETH'].latestRoundData();
const baseScale = await comet.baseScale();
const compScale = exp(1, await COMP.decimals());
const wbtcScale = exp(1, await WBTC.decimals());
Expand Down Expand Up @@ -406,10 +406,10 @@ describe('absorb', function () {
//expect(lA1.approxSpend).to.be.equal(1672498842684n);
expect(lA1.approxSpend).to.be.lt(a0.receipt.gasUsed.mul(a0.receipt.effectiveGasPrice));

const usdcPrice = await priceFeeds['USDC'].price();
const compPrice = await priceFeeds['COMP'].price();
const wbtcPrice = await priceFeeds['WBTC'].price();
const wethPrice = await priceFeeds['WETH'].price();
const [_a, usdcPrice] = await priceFeeds['USDC'].latestRoundData();
const [_b, compPrice] = await priceFeeds['COMP'].latestRoundData();
const [_c, wbtcPrice] = await priceFeeds['WBTC'].latestRoundData();
const [_d, wethPrice] = await priceFeeds['WETH'].latestRoundData();
const baseScale = await comet.baseScale();
const compScale = exp(1, await COMP.decimals());
const wbtcScale = exp(1, await WBTC.decimals());
Expand Down
8 changes: 7 additions & 1 deletion test/is-borrow-collateralized-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,13 @@ describe('isBorrowCollateralized', function () {

expect(await comet.isBorrowCollateralized(alice.address)).to.be.true;

await priceFeeds.COMP.setPrice(exp(0.5, 8));
await priceFeeds.COMP.setRoundData(
0, // roundId
exp(0.5, 8), // answer
0, // startedAt
0, // updatedAt
0 // answeredInRound
);

expect(await comet.isBorrowCollateralized(alice.address)).to.be.false;
});
Expand Down
9 changes: 8 additions & 1 deletion test/is-liquidatable-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,14 @@ describe('isLiquidatable', function () {
expect(await comet.isLiquidatable(alice.address)).to.be.false;

// price drops
await priceFeeds.COMP.setPrice(exp(0.5, 8));
await priceFeeds.COMP.setRoundData(
0, // roundId
exp(0.5, 8), // answer
0, // startedAt
0, // updatedAt
0 // answeredInRound
);

expect(await comet.isLiquidatable(alice.address)).to.be.true;
});
});
107 changes: 107 additions & 0 deletions test/wsteth-price-feed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { ethers, exp, expect } from './helpers';
import {
SimplePriceFeed__factory,
SimpleWstETH__factory,
WstETHPriceFeed__factory
} from '../build/types';

export async function makeWstETH({ stEthPrice, tokensPerStEth }) {
const SimplePriceFeedFactory = (await ethers.getContractFactory('SimplePriceFeed')) as SimplePriceFeed__factory;
const stETHPriceFeed = await SimplePriceFeedFactory.deploy(stEthPrice, 8);

const SimpleWstETHFactory = (await ethers.getContractFactory('SimpleWstETH')) as SimpleWstETH__factory;
const simpleWstETH = await SimpleWstETHFactory.deploy(tokensPerStEth);

const wstETHPriceFeedFactory = (await ethers.getContractFactory('WstETHPriceFeed')) as WstETHPriceFeed__factory;
const wstETHPriceFeed = await wstETHPriceFeedFactory.deploy(
stETHPriceFeed.address,
simpleWstETH.address
);
await wstETHPriceFeed.deployed();

return {
simpleWstETH,
stETHPriceFeed,
wstETHPriceFeed
};
}

const testCases = [
{
stEthPrice: exp(1300, 8),
tokensPerStEth: exp(.9, 18),
result: 144444444444n
},
{
stEthPrice: exp(1000, 8),
tokensPerStEth: exp(.9, 18),
result: 111111111111n
},
{
stEthPrice: exp(1000, 8),
tokensPerStEth: exp(.2, 18),
result: exp(5000, 8)
},
{
stEthPrice: exp(1000, 8),
tokensPerStEth: exp(.5, 18),
result: exp(2000, 8)
},
{
stEthPrice: exp(1000, 8),
tokensPerStEth: exp(.8, 18),
result: exp(1250, 8)
},
];

describe('wstETH price feed', function () {
describe('latestRoundData', function () {
for (const { stEthPrice, tokensPerStEth, result } of testCases) {
it(`stEthPrice (${stEthPrice}), tokensPerStEth (${tokensPerStEth}) -> ${result}`, async () => {
const { wstETHPriceFeed } = await makeWstETH({ stEthPrice, tokensPerStEth });
const latestRoundData = await wstETHPriceFeed.latestRoundData();
const price = latestRoundData.answer.toBigInt();

expect(price).to.eq(result);
});
}

it('passes along roundId, startedAt, updatedAt and answeredInRound values from stETH price feed', async () => {
const { stETHPriceFeed, wstETHPriceFeed } = await makeWstETH({
stEthPrice: exp(1000, 8),
tokensPerStEth: exp(.8, 18),
});

await stETHPriceFeed.setRoundData(
exp(15, 18), // roundId_,
1, // answer_,
exp(16, 8), // startedAt_,
exp(17, 8), // updatedAt_,
exp(18, 18) // answeredInRound_
);

const {
roundId,
startedAt,
updatedAt,
answeredInRound
} = await wstETHPriceFeed.latestRoundData();

expect(roundId.toBigInt()).to.eq(exp(15, 18));
expect(startedAt.toBigInt()).to.eq(exp(16, 8));
expect(updatedAt.toBigInt()).to.eq(exp(17, 8));
expect(answeredInRound.toBigInt()).to.eq(exp(18, 18));
});
});

it(`getRoundData > always reverts`, async () => {
const { wstETHPriceFeed } = await makeWstETH({
stEthPrice: exp(1000, 8),
tokensPerStEth: exp(.2, 18)
});

await expect(
wstETHPriceFeed.getRoundData(1)
).to.be.revertedWith("custom error 'NotImplemented()'");
});
});
0