-
Notifications
You must be signed in to change notification settings - Fork 133
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
💿 Add tests for PoR pausing to contracts-por (#1202)
* Remove redundant dependencies * Add tests for PoR from root (with pausing changes) * Remove redundant gasLimit params
- Loading branch information
Showing
6 changed files
with
313 additions
and
675 deletions.
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
93 changes: 93 additions & 0 deletions
93
packages/contracts-por/contracts/mocks/MockV3Aggregator.sol
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,93 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.6.10; | ||
|
||
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"; | ||
|
||
/** | ||
* @title MockV3Aggregator | ||
* @notice Based on the FluxAggregator contract | ||
* @notice Use this contract when you need to test | ||
* other contract's ability to read data from an | ||
* aggregator contract, but how the aggregator got | ||
* its answer is unimportant | ||
*/ | ||
contract MockV3Aggregator is AggregatorV3Interface { | ||
uint256 public constant override version = 0; | ||
|
||
uint8 public override decimals; | ||
int256 public latestAnswer; | ||
uint256 public latestTimestamp; | ||
uint256 public latestRound; | ||
|
||
mapping(uint256 => int256) public getAnswer; | ||
mapping(uint256 => uint256) public getTimestamp; | ||
mapping(uint256 => uint256) private getStartedAt; | ||
|
||
constructor(uint8 _decimals, int256 _initialAnswer) public { | ||
decimals = _decimals; | ||
updateAnswer(_initialAnswer); | ||
} | ||
|
||
function updateAnswer(int256 _answer) public { | ||
latestAnswer = _answer; | ||
latestTimestamp = block.timestamp; | ||
latestRound++; | ||
getAnswer[latestRound] = _answer; | ||
getTimestamp[latestRound] = block.timestamp; | ||
getStartedAt[latestRound] = block.timestamp; | ||
} | ||
|
||
function updateRoundData( | ||
uint80 _roundId, | ||
int256 _answer, | ||
uint256 _timestamp, | ||
uint256 _startedAt | ||
) public { | ||
latestRound = _roundId; | ||
latestAnswer = _answer; | ||
latestTimestamp = _timestamp; | ||
getAnswer[latestRound] = _answer; | ||
getTimestamp[latestRound] = _timestamp; | ||
getStartedAt[latestRound] = _startedAt; | ||
} | ||
|
||
function getRoundData(uint80 _roundId) | ||
external | ||
view | ||
override | ||
returns ( | ||
uint80 roundId, | ||
int256 answer, | ||
uint256 startedAt, | ||
uint256 updatedAt, | ||
uint80 answeredInRound | ||
) | ||
{ | ||
return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId); | ||
} | ||
|
||
function latestRoundData() | ||
external | ||
view | ||
override | ||
returns ( | ||
uint80 roundId, | ||
int256 answer, | ||
uint256 startedAt, | ||
uint256 updatedAt, | ||
uint80 answeredInRound | ||
) | ||
{ | ||
return ( | ||
uint80(latestRound), | ||
getAnswer[latestRound], | ||
getStartedAt[latestRound], | ||
getTimestamp[latestRound], | ||
uint80(latestRound) | ||
); | ||
} | ||
|
||
function description() external view override returns (string memory) { | ||
return "MockV3Aggregator.sol"; | ||
} | ||
} |
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
177 changes: 177 additions & 0 deletions
177
packages/contracts-por/test/TrueCurrencyWithPoR.test.ts
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,177 @@ | ||
import { BigNumber, BigNumberish, Wallet, utils, providers } from 'ethers' | ||
import { AddressZero } from '@ethersproject/constants' | ||
import { expect, use } from 'chai' | ||
import { waffle, network } from 'hardhat' | ||
|
||
import { timeTravel } from 'utils/timeTravel' | ||
import { | ||
MockV3Aggregator, | ||
MockV3Aggregator__factory, | ||
TrueCurrencyWithPoR, | ||
TrueUSDWithPoR__factory, | ||
} from 'contracts' | ||
|
||
use(waffle.solidity) | ||
|
||
// = base * 10^{exponent} | ||
const exp = (base: BigNumberish, exponent: BigNumberish): BigNumber => { | ||
return BigNumber.from(base).mul(BigNumber.from(10).pow(exponent)) | ||
} | ||
|
||
describe('TrueCurrency with Proof-of-reserves check', () => { | ||
const ONE_DAY_SECONDS = 24 * 60 * 60 // seconds in a day | ||
const TUSD_FEED_INITIAL_ANSWER = exp(1_000_000, 18).toString() // "1M TUSD in reserves" | ||
const AMOUNT_TO_MINT = utils.parseEther('1000000') | ||
let token: TrueCurrencyWithPoR | ||
let mockV3Aggregator: MockV3Aggregator | ||
let owner: Wallet | ||
|
||
before(async () => { | ||
const provider = waffle.provider; | ||
[owner] = provider.getWallets() | ||
|
||
token = (await new TrueUSDWithPoR__factory(owner).deploy()) as TrueCurrencyWithPoR | ||
|
||
// Deploy a mock aggregator to mock Proof of Reserve feed answers | ||
mockV3Aggregator = await new MockV3Aggregator__factory(owner).deploy( | ||
'18', | ||
TUSD_FEED_INITIAL_ANSWER, | ||
) | ||
}) | ||
|
||
beforeEach(async () => { | ||
// Reset pool Proof Of Reserve feed defaults | ||
const currentFeed = await token.chainReserveFeed() | ||
if (currentFeed.toLowerCase() !== mockV3Aggregator.address.toLowerCase()) { | ||
await token.setChainReserveFeed(mockV3Aggregator.address) | ||
await token.setChainReserveHeartbeat(ONE_DAY_SECONDS) | ||
await token.enableProofOfReserve() | ||
} | ||
|
||
// Set fresh, valid answer on mock Proof of Reserve feed | ||
const tusdSupply = await token.totalSupply() | ||
await mockV3Aggregator.updateAnswer(tusdSupply.add(AMOUNT_TO_MINT)) | ||
}) | ||
|
||
it('should mint successfully when feed is unset', async () => { | ||
// Make sure feed is unset | ||
await token.setChainReserveFeed(AddressZero) | ||
expect(await token.chainReserveFeed()).to.equal(AddressZero) | ||
|
||
// Mint TUSD | ||
const balanceBefore = await token.balanceOf(owner.address) | ||
await token.mint(owner.address, AMOUNT_TO_MINT) | ||
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore.add(AMOUNT_TO_MINT)) | ||
}) | ||
|
||
it('should mint successfully when feed is set, but heartbeat is default', async () => { | ||
// Mint TUSD | ||
const balanceBefore = await token.balanceOf(owner.address) | ||
await token.mint(owner.address, AMOUNT_TO_MINT) | ||
expect(await token.balanceOf(owner.address)).to.equal(AMOUNT_TO_MINT.add(balanceBefore)) | ||
}) | ||
|
||
it('should mint successfully when both feed and heartbeat are set', async () => { | ||
// Set heartbeat to 1 day | ||
await token.setChainReserveHeartbeat(ONE_DAY_SECONDS) | ||
expect(await token.chainReserveHeartbeat()).to.equal(ONE_DAY_SECONDS) | ||
|
||
// Mint TUSD | ||
const balanceBefore = await token.balanceOf(owner.address) | ||
await token.mint(owner.address, AMOUNT_TO_MINT) | ||
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore.add(AMOUNT_TO_MINT)) | ||
}) | ||
|
||
it('should revert mint when feed decimals < TrueCurrency decimals', async () => { | ||
const currentTusdSupply = await token.totalSupply() | ||
const validReserve = currentTusdSupply.div(exp(1, 12)).add(AMOUNT_TO_MINT) | ||
|
||
// Re-deploy a mock aggregator with fewer decimals | ||
const mockV3AggregatorWith6Decimals = await new MockV3Aggregator__factory(owner).deploy('6', validReserve) | ||
// Set feed and heartbeat on newly-deployed aggregator | ||
await token.setChainReserveFeed(mockV3AggregatorWith6Decimals.address) | ||
await token.setChainReserveHeartbeat(ONE_DAY_SECONDS) | ||
await token.enableProofOfReserve() | ||
expect(await token.chainReserveFeed()).to.equal(mockV3AggregatorWith6Decimals.address) | ||
|
||
// Mint TUSD | ||
const balanceBefore = await token.balanceOf(owner.address) | ||
await expect(token.mint(owner.address, AMOUNT_TO_MINT)).to.be.revertedWith('TrueCurrency: Unexpected decimals of PoR feed') | ||
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore) | ||
}) | ||
|
||
it('should revert mint when feed decimals > TrueCurrency decimals', async () => { | ||
// Re-deploy a mock aggregator with more decimals | ||
const currentTusdSupply = await token.totalSupply() | ||
const validReserve = currentTusdSupply.div(exp(1, 12)).add(AMOUNT_TO_MINT) | ||
|
||
const mockV3AggregatorWith20Decimals = await new MockV3Aggregator__factory(owner).deploy('20', validReserve) | ||
// Set feed and heartbeat on newly-deployed aggregator | ||
await token.setChainReserveFeed(mockV3AggregatorWith20Decimals.address) | ||
await token.setChainReserveHeartbeat(ONE_DAY_SECONDS) | ||
await token.enableProofOfReserve() | ||
expect(await token.chainReserveFeed()).to.equal(mockV3AggregatorWith20Decimals.address) | ||
|
||
// Mint TUSD | ||
const balanceBefore = await token.balanceOf(owner.address) | ||
await expect(token.mint(owner.address, AMOUNT_TO_MINT)).to.be.revertedWith('TrueCurrency: Unexpected decimals of PoR feed') | ||
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore) | ||
}) | ||
|
||
it('should mint successfully when TrueCurrency supply == proof-of-reserves', async () => { | ||
// Mint TUSD | ||
const balanceBefore = await token.balanceOf(owner.address) | ||
await token.mint(owner.address, AMOUNT_TO_MINT) | ||
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore.add(AMOUNT_TO_MINT)) | ||
}) | ||
|
||
it('should revert if TrueCurrency supply > proof-of-reserves', async () => { | ||
const currentTusdSupply = await token.totalSupply() | ||
const notEnoughReserves = currentTusdSupply.sub('1') | ||
await mockV3Aggregator.updateAnswer(notEnoughReserves) | ||
|
||
// Mint TUSD | ||
const balanceBefore = await token.balanceOf(owner.address) | ||
await expect(token.mint(owner.address, AMOUNT_TO_MINT)).to.be.revertedWith( | ||
'TrueCurrency: total supply would exceed reserves after mint', | ||
) | ||
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore) | ||
}) | ||
|
||
it('should revert if the feed is not updated within the heartbeat', async () => { | ||
// Set heartbeat to 1 day | ||
await token.setChainReserveHeartbeat(ONE_DAY_SECONDS) | ||
await token.enableProofOfReserve() | ||
expect(await token.chainReserveHeartbeat()).to.equal(ONE_DAY_SECONDS) | ||
|
||
// Heartbeat is set to 1 day, so fast-forward 2 days | ||
await timeTravel(<unknown> network.provider as providers.JsonRpcProvider, 2 * ONE_DAY_SECONDS) | ||
|
||
// Mint TUSD | ||
const balanceBefore = await token.balanceOf(owner.address) | ||
await expect(token.mint(owner.address, AMOUNT_TO_MINT)).to.be.revertedWith('TrueCurrency: PoR answer too old') | ||
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore) | ||
}) | ||
|
||
it('should revert if feed returns an invalid answer', async () => { | ||
// Update feed with invalid answer | ||
await mockV3Aggregator.updateAnswer(0) | ||
|
||
// Mint TUSD | ||
const balanceBefore = await token.balanceOf(owner.address) | ||
await expect(token.mint(owner.address, AMOUNT_TO_MINT)).to.be.revertedWith('TrueCurrency: Invalid answer from PoR feed') | ||
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore) | ||
}) | ||
|
||
it('should emit NewChainReserveHeartbeatChanged if setChainReserveHeartbeat called successfully', async () => { | ||
const oldChainReserveHeartbeat = await token.chainReserveHeartbeat() | ||
await expect(token.setChainReserveHeartbeat(2 * ONE_DAY_SECONDS)) | ||
.to.emit(token, 'NewChainReserveHeartbeat').withArgs(oldChainReserveHeartbeat, 2 * ONE_DAY_SECONDS) | ||
}) | ||
|
||
it('should emit NewChainReserveFeed if setChainReserveFeed called successfully', async () => { | ||
const oldChainReserveFeed = await token.chainReserveFeed() | ||
await expect(token.setChainReserveFeed(AddressZero)) | ||
.to.emit(token, 'NewChainReserveFeed').withArgs(oldChainReserveFeed, AddressZero) | ||
}) | ||
}) |
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,6 @@ | ||
import { providers } from 'ethers' | ||
|
||
export const timeTravel = async (provider: providers.JsonRpcProvider, time: number) => { | ||
await provider.send('evm_increaseTime', [time]) | ||
await provider.send('evm_mine', []) | ||
} |
Oops, something went wrong.