Skip to content

Commit

Permalink
💿 Add tests for PoR pausing to contracts-por (#1202)
Browse files Browse the repository at this point in the history
* Remove redundant dependencies

* Add tests for PoR from root (with pausing changes)

* Remove redundant gasLimit params
  • Loading branch information
msieczko authored Oct 18, 2022
1 parent 91ae5f9 commit abb1419
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 675 deletions.
2 changes: 1 addition & 1 deletion packages/contracts-por/contracts/TrueCurrencyWithPoR.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ abstract contract TrueCurrencyWithPoR is TrueCurrency, IPoRToken {
}
// Get required info about decimals.
// Decimals of the Proof of Reserve feed must be the same as the token's.
require(decimals() == AggregatorV3Interface(chainReserveFeed).decimals(), "Unexpected decimals of PoR feed");
require(decimals() == AggregatorV3Interface(chainReserveFeed).decimals(), "TrueCurrency: Unexpected decimals of PoR feed");

// Get latest proof-of-reserves from the feed
(, int256 signedReserves, , uint256 updatedAt, ) = AggregatorV3Interface(chainReserveFeed).latestRoundData();
Expand Down
93 changes: 93 additions & 0 deletions packages/contracts-por/contracts/mocks/MockV3Aggregator.sol
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";
}
}
5 changes: 2 additions & 3 deletions packages/contracts-por/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
"private": true,
"scripts": {
"clean": "rm -rf ./build && hardhat clean",
"prebuild": "yarn clean",
"build:hardhat": "hardhat compile",
"build:typechain": "typechain --target ethers-v5 --out-dir build/types 'build/*.json'",
"build": "yarn clean && yarn build:hardhat && yarn build:typechain && mars",
"build": "yarn build:hardhat && yarn build:typechain && mars",
"test": "mocha 'test/**/*.test.ts'"
},
"dependencies": {
Expand All @@ -26,11 +27,9 @@
"@types/mocha": "^9.1.1",
"@types/node": "^17.0.34",
"chai": "^4.3.6",
"ethereum-waffle": "4.0.7",
"ethers": "^5.7.0",
"hardhat": "~2.10.2",
"mocha": "^10.0.0",
"solc": "0.8.16",
"ts-node": "^10.7.0",
"tsconfig-paths": "^4.1.0",
"typechain": "^8.0.0",
Expand Down
177 changes: 177 additions & 0 deletions packages/contracts-por/test/TrueCurrencyWithPoR.test.ts
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)
})
})
6 changes: 6 additions & 0 deletions packages/contracts-por/test/utils/timeTravel.ts
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', [])
}
Loading

0 comments on commit abb1419

Please sign in to comment.