|  | 
|  | 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 | +}) | 
0 commit comments