Skip to content

Commit abb1419

Browse files
author
Michał Sieczkowski
authored
💿 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
1 parent 91ae5f9 commit abb1419

File tree

6 files changed

+313
-675
lines changed

6 files changed

+313
-675
lines changed

packages/contracts-por/contracts/TrueCurrencyWithPoR.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ abstract contract TrueCurrencyWithPoR is TrueCurrency, IPoRToken {
3131
}
3232
// Get required info about decimals.
3333
// Decimals of the Proof of Reserve feed must be the same as the token's.
34-
require(decimals() == AggregatorV3Interface(chainReserveFeed).decimals(), "Unexpected decimals of PoR feed");
34+
require(decimals() == AggregatorV3Interface(chainReserveFeed).decimals(), "TrueCurrency: Unexpected decimals of PoR feed");
3535

3636
// Get latest proof-of-reserves from the feed
3737
(, int256 signedReserves, , uint256 updatedAt, ) = AggregatorV3Interface(chainReserveFeed).latestRoundData();
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.6.10;
3+
4+
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol";
5+
6+
/**
7+
* @title MockV3Aggregator
8+
* @notice Based on the FluxAggregator contract
9+
* @notice Use this contract when you need to test
10+
* other contract's ability to read data from an
11+
* aggregator contract, but how the aggregator got
12+
* its answer is unimportant
13+
*/
14+
contract MockV3Aggregator is AggregatorV3Interface {
15+
uint256 public constant override version = 0;
16+
17+
uint8 public override decimals;
18+
int256 public latestAnswer;
19+
uint256 public latestTimestamp;
20+
uint256 public latestRound;
21+
22+
mapping(uint256 => int256) public getAnswer;
23+
mapping(uint256 => uint256) public getTimestamp;
24+
mapping(uint256 => uint256) private getStartedAt;
25+
26+
constructor(uint8 _decimals, int256 _initialAnswer) public {
27+
decimals = _decimals;
28+
updateAnswer(_initialAnswer);
29+
}
30+
31+
function updateAnswer(int256 _answer) public {
32+
latestAnswer = _answer;
33+
latestTimestamp = block.timestamp;
34+
latestRound++;
35+
getAnswer[latestRound] = _answer;
36+
getTimestamp[latestRound] = block.timestamp;
37+
getStartedAt[latestRound] = block.timestamp;
38+
}
39+
40+
function updateRoundData(
41+
uint80 _roundId,
42+
int256 _answer,
43+
uint256 _timestamp,
44+
uint256 _startedAt
45+
) public {
46+
latestRound = _roundId;
47+
latestAnswer = _answer;
48+
latestTimestamp = _timestamp;
49+
getAnswer[latestRound] = _answer;
50+
getTimestamp[latestRound] = _timestamp;
51+
getStartedAt[latestRound] = _startedAt;
52+
}
53+
54+
function getRoundData(uint80 _roundId)
55+
external
56+
view
57+
override
58+
returns (
59+
uint80 roundId,
60+
int256 answer,
61+
uint256 startedAt,
62+
uint256 updatedAt,
63+
uint80 answeredInRound
64+
)
65+
{
66+
return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId);
67+
}
68+
69+
function latestRoundData()
70+
external
71+
view
72+
override
73+
returns (
74+
uint80 roundId,
75+
int256 answer,
76+
uint256 startedAt,
77+
uint256 updatedAt,
78+
uint80 answeredInRound
79+
)
80+
{
81+
return (
82+
uint80(latestRound),
83+
getAnswer[latestRound],
84+
getStartedAt[latestRound],
85+
getTimestamp[latestRound],
86+
uint80(latestRound)
87+
);
88+
}
89+
90+
function description() external view override returns (string memory) {
91+
return "MockV3Aggregator.sol";
92+
}
93+
}

packages/contracts-por/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
"private": true,
66
"scripts": {
77
"clean": "rm -rf ./build && hardhat clean",
8+
"prebuild": "yarn clean",
89
"build:hardhat": "hardhat compile",
910
"build:typechain": "typechain --target ethers-v5 --out-dir build/types 'build/*.json'",
10-
"build": "yarn clean && yarn build:hardhat && yarn build:typechain && mars",
11+
"build": "yarn build:hardhat && yarn build:typechain && mars",
1112
"test": "mocha 'test/**/*.test.ts'"
1213
},
1314
"dependencies": {
@@ -26,11 +27,9 @@
2627
"@types/mocha": "^9.1.1",
2728
"@types/node": "^17.0.34",
2829
"chai": "^4.3.6",
29-
"ethereum-waffle": "4.0.7",
3030
"ethers": "^5.7.0",
3131
"hardhat": "~2.10.2",
3232
"mocha": "^10.0.0",
33-
"solc": "0.8.16",
3433
"ts-node": "^10.7.0",
3534
"tsconfig-paths": "^4.1.0",
3635
"typechain": "^8.0.0",
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { BigNumber, BigNumberish, Wallet, utils, providers } from 'ethers'
2+
import { AddressZero } from '@ethersproject/constants'
3+
import { expect, use } from 'chai'
4+
import { waffle, network } from 'hardhat'
5+
6+
import { timeTravel } from 'utils/timeTravel'
7+
import {
8+
MockV3Aggregator,
9+
MockV3Aggregator__factory,
10+
TrueCurrencyWithPoR,
11+
TrueUSDWithPoR__factory,
12+
} from 'contracts'
13+
14+
use(waffle.solidity)
15+
16+
// = base * 10^{exponent}
17+
const exp = (base: BigNumberish, exponent: BigNumberish): BigNumber => {
18+
return BigNumber.from(base).mul(BigNumber.from(10).pow(exponent))
19+
}
20+
21+
describe('TrueCurrency with Proof-of-reserves check', () => {
22+
const ONE_DAY_SECONDS = 24 * 60 * 60 // seconds in a day
23+
const TUSD_FEED_INITIAL_ANSWER = exp(1_000_000, 18).toString() // "1M TUSD in reserves"
24+
const AMOUNT_TO_MINT = utils.parseEther('1000000')
25+
let token: TrueCurrencyWithPoR
26+
let mockV3Aggregator: MockV3Aggregator
27+
let owner: Wallet
28+
29+
before(async () => {
30+
const provider = waffle.provider;
31+
[owner] = provider.getWallets()
32+
33+
token = (await new TrueUSDWithPoR__factory(owner).deploy()) as TrueCurrencyWithPoR
34+
35+
// Deploy a mock aggregator to mock Proof of Reserve feed answers
36+
mockV3Aggregator = await new MockV3Aggregator__factory(owner).deploy(
37+
'18',
38+
TUSD_FEED_INITIAL_ANSWER,
39+
)
40+
})
41+
42+
beforeEach(async () => {
43+
// Reset pool Proof Of Reserve feed defaults
44+
const currentFeed = await token.chainReserveFeed()
45+
if (currentFeed.toLowerCase() !== mockV3Aggregator.address.toLowerCase()) {
46+
await token.setChainReserveFeed(mockV3Aggregator.address)
47+
await token.setChainReserveHeartbeat(ONE_DAY_SECONDS)
48+
await token.enableProofOfReserve()
49+
}
50+
51+
// Set fresh, valid answer on mock Proof of Reserve feed
52+
const tusdSupply = await token.totalSupply()
53+
await mockV3Aggregator.updateAnswer(tusdSupply.add(AMOUNT_TO_MINT))
54+
})
55+
56+
it('should mint successfully when feed is unset', async () => {
57+
// Make sure feed is unset
58+
await token.setChainReserveFeed(AddressZero)
59+
expect(await token.chainReserveFeed()).to.equal(AddressZero)
60+
61+
// Mint TUSD
62+
const balanceBefore = await token.balanceOf(owner.address)
63+
await token.mint(owner.address, AMOUNT_TO_MINT)
64+
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore.add(AMOUNT_TO_MINT))
65+
})
66+
67+
it('should mint successfully when feed is set, but heartbeat is default', async () => {
68+
// Mint TUSD
69+
const balanceBefore = await token.balanceOf(owner.address)
70+
await token.mint(owner.address, AMOUNT_TO_MINT)
71+
expect(await token.balanceOf(owner.address)).to.equal(AMOUNT_TO_MINT.add(balanceBefore))
72+
})
73+
74+
it('should mint successfully when both feed and heartbeat are set', async () => {
75+
// Set heartbeat to 1 day
76+
await token.setChainReserveHeartbeat(ONE_DAY_SECONDS)
77+
expect(await token.chainReserveHeartbeat()).to.equal(ONE_DAY_SECONDS)
78+
79+
// Mint TUSD
80+
const balanceBefore = await token.balanceOf(owner.address)
81+
await token.mint(owner.address, AMOUNT_TO_MINT)
82+
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore.add(AMOUNT_TO_MINT))
83+
})
84+
85+
it('should revert mint when feed decimals < TrueCurrency decimals', async () => {
86+
const currentTusdSupply = await token.totalSupply()
87+
const validReserve = currentTusdSupply.div(exp(1, 12)).add(AMOUNT_TO_MINT)
88+
89+
// Re-deploy a mock aggregator with fewer decimals
90+
const mockV3AggregatorWith6Decimals = await new MockV3Aggregator__factory(owner).deploy('6', validReserve)
91+
// Set feed and heartbeat on newly-deployed aggregator
92+
await token.setChainReserveFeed(mockV3AggregatorWith6Decimals.address)
93+
await token.setChainReserveHeartbeat(ONE_DAY_SECONDS)
94+
await token.enableProofOfReserve()
95+
expect(await token.chainReserveFeed()).to.equal(mockV3AggregatorWith6Decimals.address)
96+
97+
// Mint TUSD
98+
const balanceBefore = await token.balanceOf(owner.address)
99+
await expect(token.mint(owner.address, AMOUNT_TO_MINT)).to.be.revertedWith('TrueCurrency: Unexpected decimals of PoR feed')
100+
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore)
101+
})
102+
103+
it('should revert mint when feed decimals > TrueCurrency decimals', async () => {
104+
// Re-deploy a mock aggregator with more decimals
105+
const currentTusdSupply = await token.totalSupply()
106+
const validReserve = currentTusdSupply.div(exp(1, 12)).add(AMOUNT_TO_MINT)
107+
108+
const mockV3AggregatorWith20Decimals = await new MockV3Aggregator__factory(owner).deploy('20', validReserve)
109+
// Set feed and heartbeat on newly-deployed aggregator
110+
await token.setChainReserveFeed(mockV3AggregatorWith20Decimals.address)
111+
await token.setChainReserveHeartbeat(ONE_DAY_SECONDS)
112+
await token.enableProofOfReserve()
113+
expect(await token.chainReserveFeed()).to.equal(mockV3AggregatorWith20Decimals.address)
114+
115+
// Mint TUSD
116+
const balanceBefore = await token.balanceOf(owner.address)
117+
await expect(token.mint(owner.address, AMOUNT_TO_MINT)).to.be.revertedWith('TrueCurrency: Unexpected decimals of PoR feed')
118+
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore)
119+
})
120+
121+
it('should mint successfully when TrueCurrency supply == proof-of-reserves', async () => {
122+
// Mint TUSD
123+
const balanceBefore = await token.balanceOf(owner.address)
124+
await token.mint(owner.address, AMOUNT_TO_MINT)
125+
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore.add(AMOUNT_TO_MINT))
126+
})
127+
128+
it('should revert if TrueCurrency supply > proof-of-reserves', async () => {
129+
const currentTusdSupply = await token.totalSupply()
130+
const notEnoughReserves = currentTusdSupply.sub('1')
131+
await mockV3Aggregator.updateAnswer(notEnoughReserves)
132+
133+
// Mint TUSD
134+
const balanceBefore = await token.balanceOf(owner.address)
135+
await expect(token.mint(owner.address, AMOUNT_TO_MINT)).to.be.revertedWith(
136+
'TrueCurrency: total supply would exceed reserves after mint',
137+
)
138+
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore)
139+
})
140+
141+
it('should revert if the feed is not updated within the heartbeat', async () => {
142+
// Set heartbeat to 1 day
143+
await token.setChainReserveHeartbeat(ONE_DAY_SECONDS)
144+
await token.enableProofOfReserve()
145+
expect(await token.chainReserveHeartbeat()).to.equal(ONE_DAY_SECONDS)
146+
147+
// Heartbeat is set to 1 day, so fast-forward 2 days
148+
await timeTravel(<unknown> network.provider as providers.JsonRpcProvider, 2 * ONE_DAY_SECONDS)
149+
150+
// Mint TUSD
151+
const balanceBefore = await token.balanceOf(owner.address)
152+
await expect(token.mint(owner.address, AMOUNT_TO_MINT)).to.be.revertedWith('TrueCurrency: PoR answer too old')
153+
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore)
154+
})
155+
156+
it('should revert if feed returns an invalid answer', async () => {
157+
// Update feed with invalid answer
158+
await mockV3Aggregator.updateAnswer(0)
159+
160+
// Mint TUSD
161+
const balanceBefore = await token.balanceOf(owner.address)
162+
await expect(token.mint(owner.address, AMOUNT_TO_MINT)).to.be.revertedWith('TrueCurrency: Invalid answer from PoR feed')
163+
expect(await token.balanceOf(owner.address)).to.equal(balanceBefore)
164+
})
165+
166+
it('should emit NewChainReserveHeartbeatChanged if setChainReserveHeartbeat called successfully', async () => {
167+
const oldChainReserveHeartbeat = await token.chainReserveHeartbeat()
168+
await expect(token.setChainReserveHeartbeat(2 * ONE_DAY_SECONDS))
169+
.to.emit(token, 'NewChainReserveHeartbeat').withArgs(oldChainReserveHeartbeat, 2 * ONE_DAY_SECONDS)
170+
})
171+
172+
it('should emit NewChainReserveFeed if setChainReserveFeed called successfully', async () => {
173+
const oldChainReserveFeed = await token.chainReserveFeed()
174+
await expect(token.setChainReserveFeed(AddressZero))
175+
.to.emit(token, 'NewChainReserveFeed').withArgs(oldChainReserveFeed, AddressZero)
176+
})
177+
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { providers } from 'ethers'
2+
3+
export const timeTravel = async (provider: providers.JsonRpcProvider, time: number) => {
4+
await provider.send('evm_increaseTime', [time])
5+
await provider.send('evm_mine', [])
6+
}

0 commit comments

Comments
 (0)