diff --git a/test/plugins/individual-collateral/compoundv3/CFiatV3Wrapper.test.ts b/test/plugins/individual-collateral/compoundv3/CFiatV3Wrapper.test.ts new file mode 100644 index 0000000000..a5f2366ae9 --- /dev/null +++ b/test/plugins/individual-collateral/compoundv3/CFiatV3Wrapper.test.ts @@ -0,0 +1,865 @@ +import { expect } from 'chai' +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import hre, { ethers, network } from 'hardhat' +import { useEnv } from '#/utils/env' +import { whileImpersonating } from '../../../utils/impersonation' +import { advanceTime, advanceBlocks } from '../../../utils/time' +import { allTests, allocateToken, enableRewardsAccrual, mintWcToken } from './helpers' +import { forkNetwork, getForkBlock, COMP, REWARDS, getHolder } from './constants' +import { getResetFork } from '../helpers' +import { + ERC20Mock, + CometInterface, + ICFiatV3Wrapper, + CFiatV3Wrapper__factory, +} from '../../../../typechain' +import { bn } from '../../../../common/numbers' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { MAX_UINT256, ZERO_ADDRESS } from '../../../../common/constants' + +const itL1 = forkNetwork != 'base' && forkNetwork != 'arbitrum' ? it : it.skip + +for (const curr of allTests) { + const describeFork = + useEnv('FORK') && useEnv('FORK_NETWORK') === curr.forkNetwork ? describe : describe.skip + + describeFork(curr.wrapperName, () => { + let bob: SignerWithAddress + let charles: SignerWithAddress + let don: SignerWithAddress + let token: ERC20Mock + let wcTokenV3: ICFiatV3Wrapper + let cTokenV3: CometInterface + + let chainId: number + + before(async () => { + await getResetFork(getForkBlock(curr.tokenName))() + + chainId = await getChainId(hre) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + }) + + beforeEach(async () => { + ;[, bob, charles, don] = await ethers.getSigners() + ;({ token, wcTokenV3, cTokenV3 } = await loadFixture(curr.fix)) + }) + + it('reverts if deployed with a 0 address', async () => { + const CTokenV3WrapperFactory = ( + await ethers.getContractFactory('CFiatV3Wrapper') + ) + + // TODO there is a chai limitation that cannot catch custom errors during deployment + await expect( + CTokenV3WrapperFactory.deploy( + ZERO_ADDRESS, + REWARDS, + COMP, + curr.wrapperName, + curr.wrapperSymbol + ) + ).to.be.reverted + }) + + it('configuration/state', async () => { + expect(await wcTokenV3.symbol()).to.equal(curr.wrapperSymbol) + expect(await wcTokenV3.name()).to.equal(curr.wrapperName) + expect(await wcTokenV3.totalSupply()).to.equal(bn(0)) + + expect(await wcTokenV3.underlyingComet()).to.equal(cTokenV3.address) + expect(await wcTokenV3.rewardERC20()).to.equal(COMP) + }) + + describe('deposit', () => { + const amount = bn('20000e6') + + beforeEach(async () => { + await allocateToken(bob.address, amount, getHolder(await token.symbol()), token.address) + await token.connect(bob).approve(cTokenV3.address, ethers.constants.MaxUint256) + await cTokenV3.connect(bob).supply(token.address, amount) + await cTokenV3.connect(bob).allow(wcTokenV3.address, true) + }) + + it('deposit', async () => { + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + await wcTokenV3.connect(bob).deposit(ethers.constants.MaxUint256) + expect(await cTokenV3.balanceOf(bob.address)).to.equal(0) + expect(await token.balanceOf(bob.address)).to.equal(0) + expect(await wcTokenV3.balanceOf(bob.address)).to.eq(expectedAmount) + }) + + it('deposits to own account', async () => { + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + await wcTokenV3.connect(bob).depositTo(bob.address, ethers.constants.MaxUint256) + expect(await cTokenV3.balanceOf(bob.address)).to.equal(0) + expect(await token.balanceOf(bob.address)).to.equal(0) + expect(await wcTokenV3.balanceOf(bob.address)).to.eq(expectedAmount) + }) + + it('deposits for someone else', async () => { + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + await wcTokenV3.connect(bob).depositTo(don.address, ethers.constants.MaxUint256) + expect(await wcTokenV3.balanceOf(bob.address)).to.eq(0) + expect(await wcTokenV3.balanceOf(don.address)).to.eq(expectedAmount) + }) + + it('checks for correct approval on deposit - regression test', async () => { + await expect( + wcTokenV3 + .connect(don) + .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + ).revertedWithCustomError(wcTokenV3, 'Unauthorized') + + // Provide approval on the wrapper + await wcTokenV3.connect(bob).allow(don.address, true) + + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + + // This should fail even when bob approved wcTokenV3 to spend his tokens, + // because there is no explicit approval of cTokenV3 from bob to don, only + // approval on the wrapper + await expect( + wcTokenV3 + .connect(don) + .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + ).to.be.revertedWithCustomError(cTokenV3, 'Unauthorized') + + // Add explicit approval of cTokenV3 and retry + await cTokenV3.connect(bob).allow(don.address, true) + await wcTokenV3 + .connect(don) + .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + + expect(await wcTokenV3.balanceOf(bob.address)).to.eq(0) + expect(await wcTokenV3.balanceOf(charles.address)).to.eq(expectedAmount) + }) + + it('deposits from a different account', async () => { + expect(await wcTokenV3.balanceOf(charles.address)).to.eq(0) + await expect( + wcTokenV3 + .connect(don) + .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + ).revertedWithCustomError(wcTokenV3, 'Unauthorized') + + // Approval has to be on cTokenV3, not the wrapper + await cTokenV3.connect(bob).allow(don.address, true) + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + await wcTokenV3 + .connect(don) + .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) + + expect(await wcTokenV3.balanceOf(bob.address)).to.eq(0) + expect(await wcTokenV3.balanceOf(charles.address)).to.eq(expectedAmount) + }) + + it('deposits less than available cToken', async () => { + const depositAmount = bn('10000e6') + const expectedAmount = await wcTokenV3.convertDynamicToStatic(depositAmount) + await wcTokenV3.connect(bob).depositTo(bob.address, depositAmount) + expect(await cTokenV3.balanceOf(bob.address)).to.be.closeTo(depositAmount, 100) + expect(await token.balanceOf(bob.address)).to.equal(0) + expect(await wcTokenV3.balanceOf(bob.address)).to.closeTo(expectedAmount, 100) + }) + + it('user that deposits must have same baseTrackingIndex as this Token in Comet', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, amount, bob.address) + expect((await cTokenV3.callStatic.userBasic(wcTokenV3.address)).baseTrackingIndex).to.equal( + await wcTokenV3.baseTrackingIndex(bob.address) + ) + }) + + it('multiple deposits lead to accurate balances', async () => { + let expectedAmount = await wcTokenV3.convertDynamicToStatic(bn('10000e6')) + await wcTokenV3.connect(bob).depositTo(bob.address, bn('10000e6')) + await advanceTime(1000) + expectedAmount = expectedAmount.add(await wcTokenV3.convertDynamicToStatic(bn('10000e6'))) + await wcTokenV3.connect(bob).depositTo(bob.address, bn('10000e6')) + + // The more wcTokenV3 is minted, the higher its value is relative to cTokenV3. + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.be.gt(amount) + expect(await wcTokenV3.balanceOf(bob.address)).to.closeTo(expectedAmount, 100) + + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.be.closeTo( + await cTokenV3.balanceOf(wcTokenV3.address), + 1 + ) + }) + + it('updates the totalSupply', async () => { + const totalSupplyBefore = await wcTokenV3.totalSupply() + const expectedAmount = await wcTokenV3.convertDynamicToStatic( + await cTokenV3.balanceOf(bob.address) + ) + await wcTokenV3.connect(bob).deposit(ethers.constants.MaxUint256) + expect(await wcTokenV3.totalSupply()).to.equal(totalSupplyBefore.add(expectedAmount)) + }) + + it('deposit 0 reverts', async () => { + await expect(wcTokenV3.connect(bob).deposit(0)).to.be.revertedWithCustomError( + wcTokenV3, + 'BadAmount' + ) + }) + + it('depositing 0 balance reverts', async () => { + await cTokenV3.connect(bob).transfer(charles.address, ethers.constants.MaxUint256) + await expect( + wcTokenV3.connect(bob).deposit(ethers.constants.MaxUint256) + ).to.be.revertedWithCustomError(wcTokenV3, 'BadAmount') + }) + + it('desposit to zero address reverts', async () => { + await expect( + wcTokenV3.connect(bob).depositTo(ZERO_ADDRESS, ethers.constants.MaxUint256) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + }) + }) + + describe('withdraw', () => { + const initwtokenAmt = bn('20000e6') + + beforeEach(async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, initwtokenAmt, bob.address) + await mintWcToken(token, cTokenV3, wcTokenV3, charles, initwtokenAmt, charles.address) + }) + + it('withdraws to own account', async () => { + // bob withdraws ALL + const expectedAmountBob = await wcTokenV3.underlyingBalanceOf(bob.address) + await wcTokenV3.connect(bob).withdraw(ethers.constants.MaxUint256) + const bal = await wcTokenV3.balanceOf(bob.address) + expect(bal).to.closeTo(bn('0'), 10) + expect(await cTokenV3.balanceOf(bob.address)).to.closeTo(expectedAmountBob, 80) + }) + + it('withdraws to a different account', async () => { + const expectedAmount = await wcTokenV3.underlyingBalanceOf(bob.address) + await wcTokenV3.connect(bob).withdrawTo(don.address, ethers.constants.MaxUint256) + expect(await cTokenV3.balanceOf(don.address)).to.closeTo(expectedAmount, 100) + expect(await wcTokenV3.balanceOf(bob.address)).to.closeTo(bn('0'), 10) + }) + + it('withdraws from a different account', async () => { + const withdrawAmount = await wcTokenV3.underlyingBalanceOf(bob.address) + await expect( + wcTokenV3.connect(charles).withdrawFrom(bob.address, don.address, withdrawAmount) + ).to.be.revertedWithCustomError(wcTokenV3, 'Unauthorized') + + await wcTokenV3.connect(bob).allow(charles.address, true) + await wcTokenV3.connect(charles).withdrawFrom(bob.address, don.address, withdrawAmount) + + expect(await cTokenV3.balanceOf(don.address)).to.closeTo(withdrawAmount, 100) + expect(await cTokenV3.balanceOf(charles.address)).to.closeTo(bn('0'), 50) + + expect(await wcTokenV3.balanceOf(bob.address)).to.closeTo(bn(0), 150) + }) + + it('withdraws all underlying balance via multiple withdrawals', async () => { + await advanceTime(1000) + const initialBalance = await wcTokenV3.underlyingBalanceOf(bob.address) + const withdrawAmt = bn('10000e6') + await wcTokenV3.connect(bob).withdraw(withdrawAmt) + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.closeTo( + initialBalance.sub(withdrawAmt), + 50 + ) + await advanceTime(1000) + await wcTokenV3.connect(bob).withdraw(ethers.constants.MaxUint256) + expect(await wcTokenV3.balanceOf(bob.address)).to.closeTo(bn('0'), 10) + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.closeTo(bn('0'), 10) + }) + + it('withdrawing 0 reverts', async () => { + const initialBalance = await wcTokenV3.balanceOf(bob.address) + await expect(wcTokenV3.connect(bob).withdraw(0)).to.be.revertedWithCustomError( + wcTokenV3, + 'BadAmount' + ) + expect(await wcTokenV3.balanceOf(bob.address)).to.equal(initialBalance) + }) + + it('withdrawing 0 balance reverts', async () => { + await expect( + wcTokenV3.connect(don).withdraw(ethers.constants.MaxUint256) + ).to.be.revertedWithCustomError(wcTokenV3, 'BadAmount') + }) + + it('handles complex withdrawal sequence', async () => { + let bobWithdrawn = bn('0') + let charlesWithdrawn = bn('0') + let donWithdrawn = bn('0') + + // charles withdraws SOME + const firstWithdrawAmt = bn('15000e6') + charlesWithdrawn = charlesWithdrawn.add(firstWithdrawAmt) + await wcTokenV3.connect(charles).withdraw(firstWithdrawAmt) + const newBalanceCharles = await cTokenV3.balanceOf(charles.address) + expect(newBalanceCharles).to.closeTo(firstWithdrawAmt, 50) + + // don deposits + await mintWcToken(token, cTokenV3, wcTokenV3, don, initwtokenAmt, don.address) + + // bob withdraws SOME + bobWithdrawn = bobWithdrawn.add(bn('12345e6')) + await wcTokenV3.connect(bob).withdraw(bn('12345e6')) + + // don withdraws SOME + donWithdrawn = donWithdrawn.add(bn('123e6')) + await wcTokenV3.connect(don).withdraw(bn('123e6')) + + // charles withdraws ALL + charlesWithdrawn = charlesWithdrawn.add( + await wcTokenV3.underlyingBalanceOf(charles.address) + ) + await wcTokenV3.connect(charles).withdraw(ethers.constants.MaxUint256) + + // don withdraws ALL + donWithdrawn = donWithdrawn.add(await wcTokenV3.underlyingBalanceOf(don.address)) + await wcTokenV3.connect(don).withdraw(ethers.constants.MaxUint256) + + // bob withdraws ALL + bobWithdrawn = bobWithdrawn.add(await wcTokenV3.underlyingBalanceOf(bob.address)) + await wcTokenV3.connect(bob).withdraw(ethers.constants.MaxUint256) + + const bal = await wcTokenV3.balanceOf(bob.address) + + expect(bal).to.closeTo(bn('0'), 10) + expect(await cTokenV3.balanceOf(bob.address)).to.closeTo(bobWithdrawn, 500) + expect(await cTokenV3.balanceOf(charles.address)).to.closeTo(charlesWithdrawn, 500) + expect(await cTokenV3.balanceOf(don.address)).to.closeTo(donWithdrawn, 500) + }) + + it('updates the totalSupply', async () => { + const totalSupplyBefore = await wcTokenV3.totalSupply() + const withdrawAmt = bn('15000e6') + const expectedDiff = await wcTokenV3.convertDynamicToStatic(withdrawAmt) + await wcTokenV3.connect(bob).withdraw(withdrawAmt) + // conservative rounding + expect(await wcTokenV3.totalSupply()).to.be.closeTo(totalSupplyBefore.sub(expectedDiff), 25) + }) + }) + + describe('transfer', () => { + beforeEach(async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + }) + + it('sets max allowance with approval', async () => { + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(bn(0)) + + // set approve + await wcTokenV3.connect(bob).allow(don.address, true) + + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(MAX_UINT256) + + // rollback approve + await wcTokenV3.connect(bob).allow(don.address, false) + + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(bn(0)) + }) + + it('does not transfer without approval', async () => { + await expect( + wcTokenV3.connect(bob).transferFrom(don.address, bob.address, bn('10000e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'Unauthorized') + + // Perform approval + await wcTokenV3.connect(bob).allow(don.address, true) + + await expect( + wcTokenV3.connect(don).transferFrom(bob.address, don.address, bn('10000e6')) + ).to.emit(wcTokenV3, 'Transfer') + }) + + it('transfer from/to zero address revert', async () => { + await expect( + wcTokenV3.connect(bob).transfer(ZERO_ADDRESS, bn('100e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + + await whileImpersonating(ZERO_ADDRESS, async (signer) => { + await expect( + wcTokenV3.connect(signer).transfer(don.address, bn('100e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + }) + }) + + it('performs validation on transfer amount', async () => { + await expect( + wcTokenV3.connect(bob).transfer(don.address, bn('40000e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'ExceedsBalance') + }) + + it('supports IERC20.approve and performs validations', async () => { + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(bn(0)) + expect(await wcTokenV3.hasPermission(bob.address, don.address)).to.equal(false) + + // Cannot set approve to the zero address + await expect( + wcTokenV3.connect(bob).approve(ZERO_ADDRESS, bn('10000e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + + // Can set full allowance with max uint256 + await expect(wcTokenV3.connect(bob).approve(don.address, MAX_UINT256)).to.emit( + wcTokenV3, + 'Approval' + ) + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(MAX_UINT256) + expect(await wcTokenV3.hasPermission(bob.address, don.address)).to.equal(true) + + // Can revert allowance with zero amount + await expect(wcTokenV3.connect(bob).approve(don.address, bn(0))).to.emit( + wcTokenV3, + 'Approval' + ) + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(bn(0)) + expect(await wcTokenV3.hasPermission(bob.address, don.address)).to.equal(false) + + // Any other amount reverts + await expect( + wcTokenV3.connect(bob).approve(don.address, bn('10000e6')) + ).to.be.revertedWithCustomError(wcTokenV3, 'BadAmount') + expect(await wcTokenV3.allowance(bob.address, don.address)).to.equal(bn(0)) + expect(await wcTokenV3.hasPermission(bob.address, don.address)).to.equal(false) + }) + + it('perform validations on allow', async () => { + await expect( + wcTokenV3.connect(bob).allow(ZERO_ADDRESS, true) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + + await whileImpersonating(ZERO_ADDRESS, async (signer) => { + await expect( + wcTokenV3.connect(signer).allow(don.address, true) + ).to.be.revertedWithCustomError(wcTokenV3, 'ZeroAddress') + }) + }) + + it('updates balances and rewards in sender and receiver', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + + await enableRewardsAccrual(cTokenV3) + await advanceTime(1000) + + await wcTokenV3.accrueAccount(don.address) + await wcTokenV3.accrueAccount(bob.address) + + // Don's rewards accrual should be less than Bob's because he deposited later + expect(await wcTokenV3.baseTrackingAccrued(don.address)).to.be.lt( + await wcTokenV3.baseTrackingAccrued(bob.address) + ) + const bobBal1 = await wcTokenV3.balanceOf(bob.address) + const donBal1 = await wcTokenV3.balanceOf(don.address) + await wcTokenV3.connect(bob).transfer(don.address, bn('10000e6')) + const bobBal2 = await wcTokenV3.balanceOf(bob.address) + const donBal2 = await wcTokenV3.balanceOf(don.address) + + expect(bobBal2).equal(bobBal1.sub(bn('10000e6'))) + expect(donBal2).equal(donBal1.add(bn('10000e6'))) + + await advanceTime(1000) + await wcTokenV3.accrueAccount(don.address) + await wcTokenV3.accrueAccount(bob.address) + + expect(await wcTokenV3.baseTrackingAccrued(don.address)).to.be.gt( + await wcTokenV3.baseTrackingAccrued(bob.address) + ) + + const donsBalance = (await wcTokenV3.underlyingBalanceOf(don.address)).toBigInt() + const bobsBalance = (await wcTokenV3.underlyingBalanceOf(bob.address)).toBigInt() + expect(donsBalance).to.be.gt(bobsBalance) + const totalBalances = donsBalance + bobsBalance + + // Rounding in favor of the Wrapped Token is happening here. Amount is negligible + expect(totalBalances).to.be.closeTo(await cTokenV3.balanceOf(wcTokenV3.address), 1) + }) + + it('does not update the total supply', async () => { + const totalSupplyBefore = await wcTokenV3.totalSupply() + await wcTokenV3.connect(bob).transfer(don.address, bn('10000e6')) + expect(totalSupplyBefore).to.equal(await wcTokenV3.totalSupply()) + }) + }) + + describe('accure / accrueAccount', () => { + it('accrues internally for the comet', async () => { + const initAccrueTime = (await cTokenV3.totalsBasic()).lastAccrualTime + await wcTokenV3.accrue() + const endAccrueTime = (await cTokenV3.totalsBasic()).lastAccrualTime + expect(endAccrueTime).gt(initAccrueTime) + }) + + it('accrues rewards over time', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + expect(await wcTokenV3.baseTrackingAccrued(bob.address)).to.eq(0) + await enableRewardsAccrual(cTokenV3) + await advanceTime(1000) + + await wcTokenV3.accrueAccount(bob.address) + expect(await wcTokenV3.baseTrackingAccrued(bob.address)).to.be.gt(0) + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.eq( + await cTokenV3.balanceOf(wcTokenV3.address) + ) + }) + + it('does not accrue when accruals are not enabled in Comet', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + expect(await wcTokenV3.baseTrackingAccrued(bob.address)).to.eq(0) + + await advanceTime(1000) + expect(await wcTokenV3.baseTrackingAccrued(bob.address)).to.eq(0) + }) + }) + + describe('underlying balance', () => { + it('returns the correct amount of decimals', async () => { + const decimals = await wcTokenV3.decimals() + expect(decimals).to.equal(6) + }) + + it('returns underlying balance of user which includes revenue', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + const wrappedBalance = await wcTokenV3.balanceOf(bob.address) + await advanceTime(1000) + expect(wrappedBalance).to.equal(await wcTokenV3.balanceOf(bob.address)) + // Underlying balance increases over time and is greater than the balance in the wrapped token + expect(wrappedBalance).to.be.lt(await wcTokenV3.underlyingBalanceOf(bob.address)) + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.eq( + await cTokenV3.balanceOf(wcTokenV3.address) + ) + + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + await advanceTime(1000) + const totalBalances = (await wcTokenV3.underlyingBalanceOf(don.address)).add( + await wcTokenV3.underlyingBalanceOf(bob.address) + ) + + const contractBalance = await cTokenV3.balanceOf(wcTokenV3.address) + expect(totalBalances).to.closeTo(contractBalance, 10) + expect(totalBalances).to.lte(contractBalance) + }) + + it('returns 0 when user has no balance', async () => { + expect(await wcTokenV3.underlyingBalanceOf(bob.address)).to.equal(0) + }) + + it('also accrues account in Comet to ensure that global indices are updated', async () => { + await enableRewardsAccrual(cTokenV3) + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + const oldTrackingSupplyIndex = (await cTokenV3.totalsBasic()).trackingSupplyIndex + + await advanceTime(1000) + await wcTokenV3.accrueAccount(bob.address) + expect(oldTrackingSupplyIndex).to.be.lessThan( + (await cTokenV3.totalsBasic()).trackingSupplyIndex + ) + }) + + it('matches balance in cTokenV3', async () => { + // mint some ctoken to bob + const amount = bn('20000e6') + await allocateToken(bob.address, amount, getHolder(await token.symbol()), token.address) + await token.connect(bob).approve(cTokenV3.address, ethers.constants.MaxUint256) + await cTokenV3.connect(bob).supply(token.address, amount) + + // mint some wctoken to bob, charles, don + await mintWcToken(token, cTokenV3, wcTokenV3, bob, amount, bob.address) + await mintWcToken(token, cTokenV3, wcTokenV3, charles, amount, charles.address) + await mintWcToken(token, cTokenV3, wcTokenV3, don, amount, don.address) + await advanceTime(100000) + + let totalBalances = (await wcTokenV3.underlyingBalanceOf(don.address)) + .add(await wcTokenV3.underlyingBalanceOf(bob.address)) + .add(await wcTokenV3.underlyingBalanceOf(charles.address)) + let contractBalance = await cTokenV3.balanceOf(wcTokenV3.address) + expect(totalBalances).to.be.closeTo(contractBalance, 10) + expect(totalBalances).to.be.lte(contractBalance) + + const bobBal = await wcTokenV3.balanceOf(bob.address) + await wcTokenV3.connect(bob).withdraw(bobBal) + await wcTokenV3.connect(don).withdraw(bn('10000e6')) + + totalBalances = (await wcTokenV3.underlyingBalanceOf(don.address)) + .add(await wcTokenV3.underlyingBalanceOf(bob.address)) + .add(await wcTokenV3.underlyingBalanceOf(charles.address)) + contractBalance = await cTokenV3.balanceOf(wcTokenV3.address) + expect(totalBalances).to.be.closeTo(contractBalance, 10) + expect(totalBalances).to.be.lte(contractBalance) + }) + }) + + describe('exchange rate', () => { + it('returns the correct exchange rate with 0 balance', async () => { + const totalsBasic = await cTokenV3.totalsBasic() + const baseIndexScale = await cTokenV3.baseIndexScale() + const expectedExchangeRate = totalsBasic.baseSupplyIndex.mul(bn('1e6')).div(baseIndexScale) + expect(await cTokenV3.balanceOf(wcTokenV3.address)).to.equal(0) + expect(await wcTokenV3.exchangeRate()).to.be.closeTo(expectedExchangeRate, 5) + }) + + it('returns the correct exchange rate with a positive balance', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + const totalsBasic = await cTokenV3.totalsBasic() + const baseIndexScale = await cTokenV3.baseIndexScale() + const expectedExchangeRate = totalsBasic.baseSupplyIndex.mul(bn('1e6')).div(baseIndexScale) + expect(await wcTokenV3.exchangeRate()).to.equal(expectedExchangeRate) + }) + + it('current exchange rate is a ratio of total underlying balance and total supply', async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + const totalSupply = await wcTokenV3.totalSupply() + const underlyingBalance = await cTokenV3.balanceOf(wcTokenV3.address) + expect(await wcTokenV3.exchangeRate()).to.equal( + underlyingBalance.mul(bn('1e6')).div(totalSupply) + ) + }) + }) + + describe('claiming rewards', () => { + beforeEach(async () => { + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + }) + + it('does not claim rewards when user has no permission', async () => { + await advanceTime(1000) + await enableRewardsAccrual(cTokenV3) + await expect( + wcTokenV3.connect(don).claimTo(bob.address, bob.address) + ).to.be.revertedWithCustomError(wcTokenV3, 'Unauthorized') + + await wcTokenV3.connect(bob).allow(don.address, true) + expect(await wcTokenV3.isAllowed(bob.address, don.address)).to.eq(true) + await expect(wcTokenV3.connect(don).claimTo(bob.address, bob.address)).to.emit( + wcTokenV3, + 'RewardsClaimed' + ) + }) + + it('regression test: able to claim rewards even when they are big without overflow', async () => { + // Nov 28 2023: uint64 math in CFiatV3Wrapper contract results in overflow when COMP rewards are even moderately large + + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + expect(await compToken.balanceOf(wcTokenV3.address)).to.equal(0) + await advanceTime(1000) + await enableRewardsAccrual(cTokenV3, bn('2e18')) // enough to revert on uint64 implementation + + await expect(wcTokenV3.connect(bob).claimRewards()).to.emit(wcTokenV3, 'RewardsClaimed') + expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) + }) + + it('claims rewards and sends to claimer (claimTo)', async () => { + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + expect(await compToken.balanceOf(wcTokenV3.address)).to.equal(0) + await advanceTime(1000) + await enableRewardsAccrual(cTokenV3) + + await expect(wcTokenV3.connect(bob).claimTo(bob.address, bob.address)).to.emit( + wcTokenV3, + 'RewardsClaimed' + ) + expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) + }) + + it('caps at balance to avoid reverts when claiming rewards (claimTo)', async () => { + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + expect(await compToken.balanceOf(wcTokenV3.address)).to.equal(0) + await advanceTime(1000) + await enableRewardsAccrual(cTokenV3) + + // Accrue multiple times + for (let i = 0; i < 10; i++) { + await advanceTime(1000) + await wcTokenV3.accrue() + } + + // Get rewards from Comet + const cometRewards = await ethers.getContractAt('ICometRewards', REWARDS) + await whileImpersonating(wcTokenV3.address, async (signer) => { + await cometRewards + .connect(signer) + .claimTo(cTokenV3.address, wcTokenV3.address, wcTokenV3.address, true) + }) + + // Accrue individual account + await wcTokenV3.accrueAccount(bob.address) + + // Due to rounding, balance is smaller that owed + const owed = await wcTokenV3.getRewardOwed(bob.address) + const bal = await compToken.balanceOf(wcTokenV3.address) + expect(owed).to.be.greaterThan(bal) + + // Should still be able to claimTo (caps at balance) + const balanceBobPrev = await compToken.balanceOf(bob.address) + await expect(wcTokenV3.connect(bob).claimTo(bob.address, bob.address)).to.emit( + wcTokenV3, + 'RewardsClaimed' + ) + + expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(balanceBobPrev) + }) + + it('claims rewards and sends to claimer (claimRewards)', async () => { + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + expect(await compToken.balanceOf(wcTokenV3.address)).to.equal(0) + await advanceTime(1000) + await enableRewardsAccrual(cTokenV3) + + await expect(wcTokenV3.connect(bob).claimRewards()).to.emit(wcTokenV3, 'RewardsClaimed') + expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) + }) + + it('claims rewards by participation', async () => { + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + + await enableRewardsAccrual(cTokenV3) + await advanceTime(1000) + + expect(await compToken.balanceOf(bob.address)).to.equal(0) + expect(await compToken.balanceOf(don.address)).to.equal(0) + expect(await compToken.balanceOf(wcTokenV3.address)).to.equal(0) + + // claim at the same time + await network.provider.send('evm_setAutomine', [false]) + await wcTokenV3.connect(bob).claimTo(bob.address, bob.address) + await wcTokenV3.connect(don).claimTo(don.address, don.address) + await network.provider.send('evm_setAutomine', [true]) + await advanceBlocks(1) + + expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) + const balanceBob = await compToken.balanceOf(bob.address) + const balanceDon = await compToken.balanceOf(don.address) + expect(balanceDon).lessThanOrEqual(balanceBob) + expect(balanceBob).to.be.closeTo(balanceDon, balanceBob.mul(5).div(1000)) // within 0.5% + }) + + // In this forked block, rewards accrual is not yet enabled in Comet + // Only applies to Mainnet forks (L1) + itL1('claims no rewards when rewards accrual is not enabled', async () => { + const compToken = await ethers.getContractAt('ERC20Mock', COMP) + await advanceTime(1000) + await wcTokenV3.connect(bob).claimTo(bob.address, bob.address) + expect(await compToken.balanceOf(bob.address)).to.equal(0) + }) + + it('returns reward owed after accrual and claims', async () => { + await enableRewardsAccrual(cTokenV3) + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + + await advanceTime(1000) + await advanceBlocks(1) + + const bobsReward = await wcTokenV3.getRewardOwed(bob.address) + const donsReward = await wcTokenV3.getRewardOwed(don.address) + + expect(bobsReward).to.be.greaterThan(donsReward) + + await wcTokenV3.connect(bob).claimTo(bob.address, bob.address) + expect(await wcTokenV3.getRewardOwed(bob.address)).to.equal(0) + + await advanceTime(1000) + expect(await wcTokenV3.getRewardOwed(bob.address)).to.be.greaterThan(0) + }) + + it('accrues the account on deposit and withdraw', async () => { + await enableRewardsAccrual(cTokenV3) + await advanceTime(1200) + await advanceBlocks(100) + const expectedReward = await wcTokenV3.getRewardOwed(bob.address) + await advanceTime(12) + await advanceBlocks(1) + const newExpectedReward = await wcTokenV3.getRewardOwed(bob.address) + // marginal increase in exepected reward due to time passed + expect(newExpectedReward).gt(expectedReward) + + await advanceTime(1200) + await wcTokenV3.connect(bob).withdraw(ethers.constants.MaxUint256) + const nextExpectedReward = await wcTokenV3.getRewardOwed(bob.address) + await advanceTime(1200) + const lastExpectedReward = await wcTokenV3.getRewardOwed(bob.address) + // expected reward stays the same because account is empty + expect(lastExpectedReward).to.eq(nextExpectedReward) + }) + }) + + describe('baseTrackingAccrued', () => { + it('matches baseTrackingAccrued in cTokenV3 over time', async () => { + await enableRewardsAccrual(cTokenV3) + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + let wrappedTokenAccrued = await cTokenV3.baseTrackingAccrued(wcTokenV3.address) + expect(wrappedTokenAccrued).to.equal(await wcTokenV3.baseTrackingAccrued(bob.address)) + + await wcTokenV3.accrueAccount(bob.address) + + wrappedTokenAccrued = await cTokenV3.baseTrackingAccrued(wcTokenV3.address) + expect(wrappedTokenAccrued).to.equal(await wcTokenV3.baseTrackingAccrued(bob.address)) + expect((await cTokenV3.callStatic.userBasic(wcTokenV3.address)).baseTrackingIndex).to.equal( + await wcTokenV3.baseTrackingIndex(bob.address) + ) + + await mintWcToken(token, cTokenV3, wcTokenV3, charles, bn('20000e6'), charles.address) + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + + await advanceTime(1000) + + await network.provider.send('evm_setAutomine', [false]) + await wcTokenV3.accrueAccount(bob.address) + await wcTokenV3.accrueAccount(charles.address) + await wcTokenV3.accrueAccount(don.address) + await advanceBlocks(1) + await network.provider.send('evm_setAutomine', [true]) + + // All users' total accrued rewards in Wrapped cToken should closely match Wrapped cToken's + // accrued rewards in cToken + const bobBTA = await wcTokenV3.baseTrackingAccrued(bob.address) + const charlesBTA = await wcTokenV3.baseTrackingAccrued(charles.address) + const donBTA = await wcTokenV3.baseTrackingAccrued(don.address) + const totalUsersAccrued = bobBTA.add(charlesBTA).add(donBTA) + wrappedTokenAccrued = await cTokenV3.baseTrackingAccrued(wcTokenV3.address) + expect(wrappedTokenAccrued).to.be.closeTo(totalUsersAccrued, 5) + }) + + it('matches baseTrackingAccrued in cTokenV3 after withdrawals', async () => { + await enableRewardsAccrual(cTokenV3) + await mintWcToken(token, cTokenV3, wcTokenV3, bob, bn('20000e6'), bob.address) + await mintWcToken(token, cTokenV3, wcTokenV3, don, bn('20000e6'), don.address) + + await advanceTime(1000) + await wcTokenV3.connect(bob).withdrawTo(bob.address, bn('10000e6')) + + await advanceTime(1000) + + await network.provider.send('evm_setAutomine', [false]) + await wcTokenV3.accrueAccount(bob.address) + await wcTokenV3.accrueAccount(don.address) + await advanceBlocks(1) + await network.provider.send('evm_setAutomine', [true]) + + // All users' total accrued rewards in Wrapped cToken should match Wrapped cToken's accrued rewards in cToken. + const totalUsersAccrued = (await wcTokenV3.baseTrackingAccrued(bob.address)).add( + await wcTokenV3.baseTrackingAccrued(don.address) + ) + const wrappedTokenAccrued = await cTokenV3.baseTrackingAccrued(wcTokenV3.address) + expect(wrappedTokenAccrued).to.closeTo(totalUsersAccrued, 10) + // expect(wrappedTokenAccrued).to.eq(totalUsersAccrued) + }) + }) + }) +} diff --git a/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts b/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts index 78c334c90c..c5c37fea11 100644 --- a/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts +++ b/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts @@ -5,23 +5,23 @@ import { MintCollateralFunc, CollateralStatus, } from '../pluginTestTypes' -import { mintWcUSDC, makewCSUDC, resetFork, enableRewardsAccrual } from './helpers' +import { allTests, CTokenV3Enumeration, mintWcToken, enableRewardsAccrual } from './helpers' import { ethers, network } from 'hardhat' import { ContractFactory, BigNumberish, BigNumber } from 'ethers' import { ERC20Mock, MockV3Aggregator, CometInterface, - ICusdcV3Wrapper, - ICusdcV3WrapperMock, - CusdcV3WrapperMock, - CusdcV3Wrapper__factory, - CusdcV3WrapperMock__factory, + CFiatV3Wrapper__factory, + CFiatV3WrapperMock__factory, MockV3Aggregator__factory, CometMock, CometMock__factory, TestICollateral, + ICFiatV3Wrapper, + CFiatV3WrapperMock, } from '../../../../typechain' +import { getResetFork } from '../helpers' import { pushOracleForward } from '../../../utils/oracles' import { bn, fp } from '../../../../common/numbers' import { MAX_UINT48 } from '../../../../common/constants' @@ -29,19 +29,16 @@ import { expect } from 'chai' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { advanceBlocks, getLatestBlockTimestamp, setNextBlockTimestamp } from '../../../utils/time' import { - forkNetwork, ORACLE_ERROR, ORACLE_TIMEOUT, PRICE_TIMEOUT, COMP, - CUSDC_V3, - USDC_USD_PRICE_FEED, MAX_TRADE_VOL, DEFAULT_THRESHOLD, DELAY_UNTIL_DEFAULT, REWARDS, - USDC, COMET_EXT, + getForkBlock, } from './constants' import { setCode } from '@nomicfoundation/hardhat-network-helpers' @@ -50,16 +47,16 @@ import { setCode } from '@nomicfoundation/hardhat-network-helpers' */ interface CometCollateralFixtureContext extends CollateralFixtureContext { - cusdcV3: CometInterface - wcusdcV3: ICusdcV3Wrapper - usdc: ERC20Mock + cTokenV3: CometInterface + wcTokenV3: ICFiatV3Wrapper + token: ERC20Mock } interface CometCollateralFixtureContextMockComet extends CollateralFixtureContext { - cusdcV3: CometMock - wcusdcV3: ICusdcV3Wrapper - usdc: ERC20Mock - wcusdcV3Mock: CusdcV3WrapperMock + cTokenV3: CometMock + wcTokenV3: ICFiatV3Wrapper + token: ERC20Mock + wcTokenV3Mock: CFiatV3WrapperMock } interface CometCollateralOpts extends CollateralOpts { @@ -72,324 +69,327 @@ interface CometCollateralOpts extends CollateralOpts { const chainlinkDefaultAnswer = bn('1e8') -export const defaultCometCollateralOpts: CometCollateralOpts = { - erc20: CUSDC_V3, - targetName: ethers.utils.formatBytes32String('USD'), - rewardERC20: COMP, - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: USDC_USD_PRICE_FEED, - oracleTimeout: ORACLE_TIMEOUT, - oracleError: ORACLE_ERROR, - maxTradeVolume: MAX_TRADE_VOL, - defaultThreshold: DEFAULT_THRESHOLD, - delayUntilDefault: DELAY_UNTIL_DEFAULT, - revenueHiding: fp('0'), -} +allTests.forEach((curr: CTokenV3Enumeration) => { + const defaultCometCollateralOpts: CometCollateralOpts = { + erc20: curr.cTokenV3, + targetName: ethers.utils.formatBytes32String('USD'), + rewardERC20: COMP, + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: curr.chainlinkFeed, + oracleTimeout: ORACLE_TIMEOUT, + oracleError: ORACLE_ERROR, + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: fp('0'), + } -export const deployCollateral = async ( - opts: CometCollateralOpts = {} -): Promise => { - opts = { ...defaultCometCollateralOpts, ...opts } - - const CTokenV3CollateralFactory: ContractFactory = await ethers.getContractFactory( - 'CTokenV3Collateral' - ) - - const collateral = await CTokenV3CollateralFactory.deploy( - { - erc20: opts.erc20, - targetName: opts.targetName, - priceTimeout: opts.priceTimeout, - chainlinkFeed: opts.chainlinkFeed, - oracleError: opts.oracleError, - oracleTimeout: opts.oracleTimeout, - maxTradeVolume: opts.maxTradeVolume, - defaultThreshold: opts.defaultThreshold, - delayUntilDefault: opts.delayUntilDefault, - }, - opts.revenueHiding, - { gasLimit: 2000000000 } - ) - await collateral.deployed() - - // Push forward chainlink feed - await pushOracleForward(opts.chainlinkFeed!) - - // sometimes we are trying to test a negative test case and we want this to fail silently - // fortunately this syntax fails silently because our tools are terrible - await expect(collateral.refresh()) - - return collateral -} + const deployCollateral = async (opts: CometCollateralOpts = {}): Promise => { + opts = { ...defaultCometCollateralOpts, ...opts } + + const CTokenV3CollateralFactory: ContractFactory = await ethers.getContractFactory( + 'CTokenV3Collateral' + ) + + const collateral = await CTokenV3CollateralFactory.deploy( + { + erc20: opts.erc20, + targetName: opts.targetName, + priceTimeout: opts.priceTimeout, + chainlinkFeed: opts.chainlinkFeed, + oracleError: opts.oracleError, + oracleTimeout: opts.oracleTimeout, + maxTradeVolume: opts.maxTradeVolume, + defaultThreshold: opts.defaultThreshold, + delayUntilDefault: opts.delayUntilDefault, + }, + opts.revenueHiding, + { gasLimit: 2000000000 } + ) + + await collateral.deployed() -type Fixture = () => Promise + // Push forward chainlink feed + await pushOracleForward(opts.chainlinkFeed!) + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return collateral + } + + type Fixture = () => Promise + + const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CometCollateralOpts = {} + ): Fixture => { + const collateralOpts = { ...defaultCometCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const chainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, chainlinkDefaultAnswer) + ) + collateralOpts.chainlinkFeed = chainlinkFeed.address + + const fix = await curr.fix() + const cTokenV3 = fix.cTokenV3 + const { wcTokenV3, token } = fix + + collateralOpts.erc20 = wcTokenV3.address + const collateral = await deployCollateral(collateralOpts) + const rewardToken = await ethers.getContractAt('ERC20Mock', COMP) + + return { + alice, + collateral, + chainlinkFeed, + cTokenV3, + wcTokenV3, + token, + tok: wcTokenV3, + rewardToken, + } + } + + return makeCollateralFixtureContext + } -const makeCollateralFixtureContext = ( - alice: SignerWithAddress, - opts: CometCollateralOpts = {} -): Fixture => { - const collateralOpts = { ...defaultCometCollateralOpts, ...opts } + const deployCollateralCometMockContext = async ( + opts: CometCollateralOpts = {} + ): Promise => { + const collateralOpts = { ...defaultCometCollateralOpts, ...opts } - const makeCollateralFixtureContext = async () => { const MockV3AggregatorFactory = ( await ethers.getContractFactory('MockV3Aggregator') ) + const chainlinkFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + collateralOpts.chainlinkFeed = chainlinkFeed.address + + const CometFactory = await ethers.getContractFactory('CometMock') + const cTokenV3 = await CometFactory.deploy(curr.cTokenV3) - const chainlinkFeed = ( - await MockV3AggregatorFactory.deploy(8, chainlinkDefaultAnswer) + const CTokenV3WrapperFactory = ( + await ethers.getContractFactory('CFiatV3Wrapper') ) - collateralOpts.chainlinkFeed = chainlinkFeed.address - const fix = await makewCSUDC() - const cusdcV3 = fix.cusdcV3 - const { wcusdcV3, usdc } = fix + const wcTokenV3 = ( + ((await CTokenV3WrapperFactory.deploy( + cTokenV3.address, + REWARDS, + COMP, + curr.wrapperName, + curr.wrapperSymbol + )) as unknown as ICFiatV3Wrapper) + ) + const CTokenV3WrapperMockFactory = ( + await ethers.getContractFactory('CFiatV3WrapperMock') + ) + const wcTokenV3Mock = ( + ((await CTokenV3WrapperMockFactory.deploy(wcTokenV3.address)) as unknown) + ) - collateralOpts.erc20 = wcusdcV3.address + collateralOpts.erc20 = wcTokenV3Mock.address + const token = await ethers.getContractAt('ERC20Mock', curr.token) const collateral = await deployCollateral(collateralOpts) const rewardToken = await ethers.getContractAt('ERC20Mock', COMP) return { - alice, collateral, chainlinkFeed, - cusdcV3, - wcusdcV3, - usdc, - tok: wcusdcV3, + cTokenV3, + wcTokenV3: wcTokenV3Mock, + wcTokenV3Mock: wcTokenV3Mock as unknown as CFiatV3WrapperMock, + token, + tok: wcTokenV3, rewardToken, } } - return makeCollateralFixtureContext -} - -const deployCollateralCometMockContext = async ( - opts: CometCollateralOpts = {} -): Promise => { - const collateralOpts = { ...defaultCometCollateralOpts, ...opts } - - const MockV3AggregatorFactory = ( - await ethers.getContractFactory('MockV3Aggregator') - ) - const chainlinkFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - collateralOpts.chainlinkFeed = chainlinkFeed.address - - const CometFactory = await ethers.getContractFactory('CometMock') - const cusdcV3 = await CometFactory.deploy(CUSDC_V3) - - const CusdcV3WrapperFactory = ( - await ethers.getContractFactory('CusdcV3Wrapper') - ) - - const wcusdcV3 = ( - ((await CusdcV3WrapperFactory.deploy( - cusdcV3.address, - REWARDS, - COMP - )) as unknown as ICusdcV3Wrapper) - ) - const CusdcV3WrapperMockFactory = ( - await ethers.getContractFactory('CusdcV3WrapperMock') - ) - const wcusdcV3Mock = ( - ((await CusdcV3WrapperMockFactory.deploy(wcusdcV3.address)) as unknown) - ) - - collateralOpts.erc20 = wcusdcV3Mock.address - const usdc = await ethers.getContractAt('ERC20Mock', USDC) - const collateral = await deployCollateral(collateralOpts) - const rewardToken = await ethers.getContractAt('ERC20Mock', COMP) - - return { - collateral, - chainlinkFeed, - cusdcV3, - wcusdcV3: wcusdcV3Mock, - wcusdcV3Mock: wcusdcV3Mock as unknown as CusdcV3WrapperMock, - usdc, - tok: wcusdcV3, - rewardToken, + /* + Define helper functions + */ + + const mintCollateralTo: MintCollateralFunc = async ( + ctx: CometCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string + ) => { + await mintWcToken( + ctx.token, + ctx.cTokenV3, + ctx.tok as unknown as ICFiatV3Wrapper, + user, + amount, + recipient + ) } -} -/* - Define helper functions -*/ - -const mintCollateralTo: MintCollateralFunc = async ( - ctx: CometCollateralFixtureContext, - amount: BigNumberish, - user: SignerWithAddress, - recipient: string -) => { - await mintWcUSDC( - ctx.usdc, - ctx.cusdcV3, - ctx.tok as unknown as ICusdcV3Wrapper, - user, - amount, - recipient - ) -} - -const reduceTargetPerRef = async ( - ctx: CometCollateralFixtureContext, - pctDecrease: BigNumberish -) => { - const lastRound = await ctx.chainlinkFeed.latestRoundData() - const nextAnswer = lastRound.answer.sub(lastRound.answer.mul(pctDecrease).div(100)) - await ctx.chainlinkFeed.updateAnswer(nextAnswer) -} + const reduceTargetPerRef = async ( + ctx: CometCollateralFixtureContext, + pctDecrease: BigNumberish + ) => { + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.sub(lastRound.answer.mul(pctDecrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) + } -const increaseTargetPerRef = async ( - ctx: CometCollateralFixtureContext, - pctIncrease: BigNumberish -) => { - const lastRound = await ctx.chainlinkFeed.latestRoundData() - const nextAnswer = lastRound.answer.add(lastRound.answer.mul(pctIncrease).div(100)) - await ctx.chainlinkFeed.updateAnswer(nextAnswer) -} + const increaseTargetPerRef = async ( + ctx: CometCollateralFixtureContext, + pctIncrease: BigNumberish + ) => { + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.add(lastRound.answer.mul(pctIncrease).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) + } -const reduceRefPerTok = async (ctx: CometCollateralFixtureContext, pctDecrease: BigNumberish) => { - const totalsBasic = await ctx.cusdcV3.totalsBasic() - const bsi = totalsBasic.baseSupplyIndex + const reduceRefPerTok = async (ctx: CometCollateralFixtureContext, pctDecrease: BigNumberish) => { + const totalsBasic = await ctx.cTokenV3.totalsBasic() + const bsi = totalsBasic.baseSupplyIndex - // save old bytecode - const oldBytecode = await network.provider.send('eth_getCode', [COMET_EXT]) + // save old bytecode + const oldBytecode = await network.provider.send('eth_getCode', [COMET_EXT]) - const mockFactory = await ethers.getContractFactory('CometExtMock') - const mock = await mockFactory.deploy() - const bytecode = await network.provider.send('eth_getCode', [mock.address]) - await setCode(COMET_EXT, bytecode) + const mockFactory = await ethers.getContractFactory('CometExtMock') + const mock = await mockFactory.deploy() + const bytecode = await network.provider.send('eth_getCode', [mock.address]) + await setCode(COMET_EXT, bytecode) - const cometAsMock = await ethers.getContractAt('CometExtMock', ctx.cusdcV3.address) - await cometAsMock.setBaseSupplyIndex(bsi.sub(bsi.mul(pctDecrease).div(100))) + const cometAsMock = await ethers.getContractAt('CometExtMock', ctx.cTokenV3.address) + await cometAsMock.setBaseSupplyIndex(bsi.sub(bsi.mul(pctDecrease).div(100))) - await setCode(COMET_EXT, oldBytecode) -} + await setCode(COMET_EXT, oldBytecode) + } -const increaseRefPerTok = async () => { - await advanceBlocks(1000) - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) -} + const increaseRefPerTok = async () => { + await advanceBlocks(1000) + await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) + } -const getExpectedPrice = async (ctx: CometCollateralFixtureContext): Promise => { - const initRefPerTok = await ctx.collateral.refPerTok() + const getExpectedPrice = async (ctx: CometCollateralFixtureContext): Promise => { + const initRefPerTok = await ctx.collateral.refPerTok() - const decimals = await ctx.chainlinkFeed.decimals() + const decimals = await ctx.chainlinkFeed.decimals() - const initData = await ctx.chainlinkFeed.latestRoundData() - return initData.answer - .mul(bn(10).pow(18 - decimals)) - .mul(initRefPerTok) - .div(fp('1')) -} + const initData = await ctx.chainlinkFeed.latestRoundData() + return initData.answer + .mul(bn(10).pow(18 - decimals)) + .mul(initRefPerTok) + .div(fp('1')) + } -/* - Define collateral-specific tests -*/ + /* + Define collateral-specific tests + */ -const collateralSpecificConstructorTests = () => { - return -} + const collateralSpecificConstructorTests = () => { + return + } -const collateralSpecificStatusTests = () => { - it('does revenue hiding correctly', async () => { - const { collateral, wcusdcV3Mock } = await deployCollateralCometMockContext({ - revenueHiding: fp('0.01'), + const collateralSpecificStatusTests = () => { + it('does revenue hiding correctly', async () => { + const { collateral, wcTokenV3Mock } = await deployCollateralCometMockContext({ + revenueHiding: fp('0.01'), + }) + + // Should remain SOUND after a 1% decrease + let refPerTok = await collateral.refPerTok() + let currentExchangeRate = await wcTokenV3Mock.exchangeRate() + await wcTokenV3Mock.setMockExchangeRate( + true, + currentExchangeRate.sub(currentExchangeRate.mul(1).div(100)) + ) + await collateral.refresh() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + // refPerTok should be unchanged + expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand + + // Should become DISABLED if drops more than that + refPerTok = await collateral.refPerTok() + currentExchangeRate = await wcTokenV3Mock.exchangeRate() + await wcTokenV3Mock.setMockExchangeRate( + true, + currentExchangeRate.sub(currentExchangeRate.mul(1).div(100)) + ) + await collateral.refresh() + expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) + + // refPerTok should have fallen 1% + refPerTok = refPerTok.sub(refPerTok.div(100)) + expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand }) - // Should remain SOUND after a 1% decrease - let refPerTok = await collateral.refPerTok() - let currentExchangeRate = await wcusdcV3Mock.exchangeRate() - await wcusdcV3Mock.setMockExchangeRate( - true, - currentExchangeRate.sub(currentExchangeRate.mul(1).div(100)) - ) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - - // refPerTok should be unchanged - expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand - - // Should become DISABLED if drops more than that - refPerTok = await collateral.refPerTok() - currentExchangeRate = await wcusdcV3Mock.exchangeRate() - await wcusdcV3Mock.setMockExchangeRate( - true, - currentExchangeRate.sub(currentExchangeRate.mul(1).div(100)) - ) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - - // refPerTok should have fallen 1% - refPerTok = refPerTok.sub(refPerTok.div(100)) - expect(await collateral.refPerTok()).to.be.closeTo(refPerTok, refPerTok.div(bn('1e3'))) // within 1-part-in-1-thousand - }) - - it('enters DISABLED state when refPerTok() decreases', async () => { - // Context: Usually this is left to generic suite, but we were having issues with the comet extensions - // on arbitrum as compared to ethereum mainnet, and this was the easiest way around it. - - const { collateral, wcusdcV3Mock } = await deployCollateralCometMockContext({}) - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - - // Should default instantly after 5% drop - const currentExchangeRate = await wcusdcV3Mock.exchangeRate() - await wcusdcV3Mock.setMockExchangeRate( - true, - currentExchangeRate.sub(currentExchangeRate.mul(5).div(100)) - ) - await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) - }) - - it('should not brick refPerTok() even if _underlyingRefPerTok() reverts', async () => { - const { collateral, wcusdcV3Mock } = await deployCollateralCometMockContext({}) - await wcusdcV3Mock.setRevertExchangeRate(true) - await expect(collateral.refresh()).not.to.be.reverted - await expect(collateral.refPerTok()).not.to.be.reverted - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - }) -} + it('enters DISABLED state when refPerTok() decreases', async () => { + // Context: Usually this is left to generic suite, but we were having issues with the comet extensions + // on arbitrum as compared to ethereum mainnet, and this was the easiest way around it. + + const { collateral, wcTokenV3Mock } = await deployCollateralCometMockContext({}) + + // Check initial state + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await collateral.whenDefault()).to.equal(MAX_UINT48) + await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') + + // Should default instantly after 5% drop + const currentExchangeRate = await wcTokenV3Mock.exchangeRate() + await wcTokenV3Mock.setMockExchangeRate( + true, + currentExchangeRate.sub(currentExchangeRate.mul(5).div(100)) + ) + await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') + expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) + expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) + }) -const beforeEachRewardsTest = async (ctx: CometCollateralFixtureContext) => { - await enableRewardsAccrual(ctx.cusdcV3) -} + it('should not brick refPerTok() even if _underlyingRefPerTok() reverts', async () => { + const { collateral, wcTokenV3Mock } = await deployCollateralCometMockContext({}) + await wcTokenV3Mock.setRevertExchangeRate(true) + await expect(collateral.refresh()).not.to.be.reverted + await expect(collateral.refPerTok()).not.to.be.reverted + expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) + }) + } -/* - Run the test suite -*/ + const beforeEachRewardsTest = async (ctx: CometCollateralFixtureContext) => { + await enableRewardsAccrual(ctx.cTokenV3) + } -const opts = { - deployCollateral, - collateralSpecificConstructorTests, - collateralSpecificStatusTests, - beforeEachRewardsTest, - makeCollateralFixtureContext, - mintCollateralTo, - reduceTargetPerRef, - increaseTargetPerRef, - reduceRefPerTok, - increaseRefPerTok, - getExpectedPrice, - itClaimsRewards: it, - itChecksTargetPerRefDefault: it, - itChecksTargetPerRefDefaultUp: it, - itChecksRefPerTokDefault: it.skip, // implemented in this file - itChecksPriceChanges: it, - itChecksNonZeroDefaultThreshold: it, - itHasRevenueHiding: it.skip, // implemented in this file - itIsPricedByPeg: true, - resetFork, - collateralName: 'CompoundV3USDC', - chainlinkDefaultAnswer, - targetNetwork: forkNetwork, -} + /* + Run the test suite + */ + + const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + beforeEachRewardsTest, + makeCollateralFixtureContext, + mintCollateralTo, + reduceTargetPerRef, + increaseTargetPerRef, + reduceRefPerTok, + increaseRefPerTok, + getExpectedPrice, + itClaimsRewards: it, + itChecksTargetPerRefDefault: it, + itChecksTargetPerRefDefaultUp: it, + itChecksRefPerTokDefault: it.skip, // implemented in this file + itChecksPriceChanges: it, + itChecksNonZeroDefaultThreshold: it, + itHasRevenueHiding: it.skip, // implemented in this file + itIsPricedByPeg: true, + resetFork: getResetFork(getForkBlock(curr.tokenName)), + collateralName: curr.testName, + chainlinkDefaultAnswer, + targetNetwork: curr.forkNetwork, + } -collateralTests(opts) + collateralTests(opts) +}) diff --git a/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts b/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts deleted file mode 100644 index d8749820d9..0000000000 --- a/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts +++ /dev/null @@ -1,842 +0,0 @@ -import { expect } from 'chai' -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' -import hre, { ethers, network } from 'hardhat' -import { useEnv } from '#/utils/env' -import { whileImpersonating } from '../../../utils/impersonation' -import { advanceTime, advanceBlocks } from '../../../utils/time' -import { allocateUSDC, enableRewardsAccrual, mintWcUSDC, makewCSUDC, resetFork } from './helpers' -import { forkNetwork, COMP, REWARDS } from './constants' -import { - ERC20Mock, - CometInterface, - ICusdcV3Wrapper, - CusdcV3Wrapper__factory, -} from '../../../../typechain' -import { bn } from '../../../../common/numbers' -import { getChainId } from '../../../../common/blockchain-utils' -import { networkConfig } from '../../../../common/configuration' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { MAX_UINT256, ZERO_ADDRESS } from '../../../../common/constants' - -const describeFork = useEnv('FORK') ? describe : describe.skip - -const itL1 = forkNetwork != 'base' && forkNetwork != 'arbitrum' ? it : it.skip - -describeFork('Wrapped CUSDCv3', () => { - let bob: SignerWithAddress - let charles: SignerWithAddress - let don: SignerWithAddress - let usdc: ERC20Mock - let wcusdcV3: ICusdcV3Wrapper - let cusdcV3: CometInterface - - let chainId: number - - before(async () => { - await resetFork() - - chainId = await getChainId(hre) - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - }) - - beforeEach(async () => { - ;[, bob, charles, don] = await ethers.getSigners() - ;({ usdc, wcusdcV3, cusdcV3 } = await loadFixture(makewCSUDC)) - }) - - it('reverts if deployed with a 0 address', async () => { - const CusdcV3WrapperFactory = ( - await ethers.getContractFactory('CusdcV3Wrapper') - ) - - // TODO there is a chai limitation that cannot catch custom errors during deployment - await expect(CusdcV3WrapperFactory.deploy(ZERO_ADDRESS, REWARDS, COMP)).to.be.reverted - }) - - it('configuration/state', async () => { - expect(await wcusdcV3.symbol()).to.equal('wcUSDCv3') - expect(await wcusdcV3.name()).to.equal('Wrapped cUSDCv3') - expect(await wcusdcV3.totalSupply()).to.equal(bn(0)) - - expect(await wcusdcV3.underlyingComet()).to.equal(cusdcV3.address) - expect(await wcusdcV3.rewardERC20()).to.equal(COMP) - }) - - describe('deposit', () => { - const amount = bn('20000e6') - - beforeEach(async () => { - await allocateUSDC(bob.address, amount) - await usdc.connect(bob).approve(cusdcV3.address, ethers.constants.MaxUint256) - await cusdcV3.connect(bob).supply(usdc.address, amount) - await cusdcV3.connect(bob).allow(wcusdcV3.address, true) - }) - - it('deposit', async () => { - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - await wcusdcV3.connect(bob).deposit(ethers.constants.MaxUint256) - expect(await cusdcV3.balanceOf(bob.address)).to.equal(0) - expect(await usdc.balanceOf(bob.address)).to.equal(0) - expect(await wcusdcV3.balanceOf(bob.address)).to.eq(expectedAmount) - }) - - it('deposits to own account', async () => { - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - await wcusdcV3.connect(bob).depositTo(bob.address, ethers.constants.MaxUint256) - expect(await cusdcV3.balanceOf(bob.address)).to.equal(0) - expect(await usdc.balanceOf(bob.address)).to.equal(0) - expect(await wcusdcV3.balanceOf(bob.address)).to.eq(expectedAmount) - }) - - it('deposits for someone else', async () => { - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - await wcusdcV3.connect(bob).depositTo(don.address, ethers.constants.MaxUint256) - expect(await wcusdcV3.balanceOf(bob.address)).to.eq(0) - expect(await wcusdcV3.balanceOf(don.address)).to.eq(expectedAmount) - }) - - it('checks for correct approval on deposit - regression test', async () => { - await expect( - wcusdcV3.connect(don).depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) - ).revertedWithCustomError(wcusdcV3, 'Unauthorized') - - // Provide approval on the wrapper - await wcusdcV3.connect(bob).allow(don.address, true) - - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - - // This should fail even when bob approved wcusdcv3 to spend his tokens, - // because there is no explicit approval of cUSDCv3 from bob to don, only - // approval on the wrapper - await expect( - wcusdcV3.connect(don).depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) - ).to.be.revertedWithCustomError(cusdcV3, 'Unauthorized') - - // Add explicit approval of cUSDCv3 and retry - await cusdcV3.connect(bob).allow(don.address, true) - await wcusdcV3 - .connect(don) - .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) - - expect(await wcusdcV3.balanceOf(bob.address)).to.eq(0) - expect(await wcusdcV3.balanceOf(charles.address)).to.eq(expectedAmount) - }) - - it('deposits from a different account', async () => { - expect(await wcusdcV3.balanceOf(charles.address)).to.eq(0) - await expect( - wcusdcV3.connect(don).depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) - ).revertedWithCustomError(wcusdcV3, 'Unauthorized') - - // Approval has to be on cUsdcV3, not the wrapper - await cusdcV3.connect(bob).allow(don.address, true) - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - await wcusdcV3 - .connect(don) - .depositFrom(bob.address, charles.address, ethers.constants.MaxUint256) - - expect(await wcusdcV3.balanceOf(bob.address)).to.eq(0) - expect(await wcusdcV3.balanceOf(charles.address)).to.eq(expectedAmount) - }) - - it('deposits less than available cusdc', async () => { - const depositAmount = bn('10000e6') - const expectedAmount = await wcusdcV3.convertDynamicToStatic(depositAmount) - await wcusdcV3.connect(bob).depositTo(bob.address, depositAmount) - expect(await cusdcV3.balanceOf(bob.address)).to.be.closeTo(depositAmount, 100) - expect(await usdc.balanceOf(bob.address)).to.equal(0) - expect(await wcusdcV3.balanceOf(bob.address)).to.closeTo(expectedAmount, 100) - }) - - it('user that deposits must have same baseTrackingIndex as this Token in Comet', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, amount, bob.address) - expect((await cusdcV3.callStatic.userBasic(wcusdcV3.address)).baseTrackingIndex).to.equal( - await wcusdcV3.baseTrackingIndex(bob.address) - ) - }) - - it('multiple deposits lead to accurate balances', async () => { - let expectedAmount = await wcusdcV3.convertDynamicToStatic(bn('10000e6')) - await wcusdcV3.connect(bob).depositTo(bob.address, bn('10000e6')) - await advanceTime(1000) - expectedAmount = expectedAmount.add(await wcusdcV3.convertDynamicToStatic(bn('10000e6'))) - await wcusdcV3.connect(bob).depositTo(bob.address, bn('10000e6')) - - // The more wcUSDCv3 is minted, the higher its value is relative to cUSDCv3. - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.be.gt(amount) - expect(await wcusdcV3.balanceOf(bob.address)).to.closeTo(expectedAmount, 100) - - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.be.closeTo( - await cusdcV3.balanceOf(wcusdcV3.address), - 1 - ) - }) - - it('updates the totalSupply', async () => { - const totalSupplyBefore = await wcusdcV3.totalSupply() - const expectedAmount = await wcusdcV3.convertDynamicToStatic( - await cusdcV3.balanceOf(bob.address) - ) - await wcusdcV3.connect(bob).deposit(ethers.constants.MaxUint256) - expect(await wcusdcV3.totalSupply()).to.equal(totalSupplyBefore.add(expectedAmount)) - }) - - it('deposit 0 reverts', async () => { - await expect(wcusdcV3.connect(bob).deposit(0)).to.be.revertedWithCustomError( - wcusdcV3, - 'BadAmount' - ) - }) - - it('depositing 0 balance reverts', async () => { - await cusdcV3.connect(bob).transfer(charles.address, ethers.constants.MaxUint256) - await expect( - wcusdcV3.connect(bob).deposit(ethers.constants.MaxUint256) - ).to.be.revertedWithCustomError(wcusdcV3, 'BadAmount') - }) - - it('desposit to zero address reverts', async () => { - await expect( - wcusdcV3.connect(bob).depositTo(ZERO_ADDRESS, ethers.constants.MaxUint256) - ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') - }) - }) - - describe('withdraw', () => { - const initwusdcAmt = bn('20000e6') - - beforeEach(async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, initwusdcAmt, bob.address) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, charles, initwusdcAmt, charles.address) - }) - - it('withdraws to own account', async () => { - // bob withdraws ALL - const expectedAmountBob = await wcusdcV3.underlyingBalanceOf(bob.address) - await wcusdcV3.connect(bob).withdraw(ethers.constants.MaxUint256) - const bal = await wcusdcV3.balanceOf(bob.address) - expect(bal).to.closeTo(bn('0'), 10) - expect(await cusdcV3.balanceOf(bob.address)).to.closeTo(expectedAmountBob, 50) - }) - - it('withdraws to a different account', async () => { - const expectedAmount = await wcusdcV3.underlyingBalanceOf(bob.address) - await wcusdcV3.connect(bob).withdrawTo(don.address, ethers.constants.MaxUint256) - expect(await cusdcV3.balanceOf(don.address)).to.closeTo(expectedAmount, 100) - expect(await wcusdcV3.balanceOf(bob.address)).to.closeTo(bn('0'), 10) - }) - - it('withdraws from a different account', async () => { - const withdrawAmount = await wcusdcV3.underlyingBalanceOf(bob.address) - await expect( - wcusdcV3.connect(charles).withdrawFrom(bob.address, don.address, withdrawAmount) - ).to.be.revertedWithCustomError(wcusdcV3, 'Unauthorized') - - await wcusdcV3.connect(bob).allow(charles.address, true) - await wcusdcV3.connect(charles).withdrawFrom(bob.address, don.address, withdrawAmount) - - expect(await cusdcV3.balanceOf(don.address)).to.closeTo(withdrawAmount, 100) - expect(await cusdcV3.balanceOf(charles.address)).to.closeTo(bn('0'), 30) - - expect(await wcusdcV3.balanceOf(bob.address)).to.closeTo(bn(0), 100) - }) - - it('withdraws all underlying balance via multiple withdrawals', async () => { - await advanceTime(1000) - const initialBalance = await wcusdcV3.underlyingBalanceOf(bob.address) - const withdrawAmt = bn('10000e6') - await wcusdcV3.connect(bob).withdraw(withdrawAmt) - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.closeTo( - initialBalance.sub(withdrawAmt), - 50 - ) - await advanceTime(1000) - await wcusdcV3.connect(bob).withdraw(ethers.constants.MaxUint256) - expect(await wcusdcV3.balanceOf(bob.address)).to.closeTo(bn('0'), 10) - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.closeTo(bn('0'), 10) - }) - - it('withdrawing 0 reverts', async () => { - const initialBalance = await wcusdcV3.balanceOf(bob.address) - await expect(wcusdcV3.connect(bob).withdraw(0)).to.be.revertedWithCustomError( - wcusdcV3, - 'BadAmount' - ) - expect(await wcusdcV3.balanceOf(bob.address)).to.equal(initialBalance) - }) - - it('withdrawing 0 balance reverts', async () => { - await expect( - wcusdcV3.connect(don).withdraw(ethers.constants.MaxUint256) - ).to.be.revertedWithCustomError(wcusdcV3, 'BadAmount') - }) - - it('handles complex withdrawal sequence', async () => { - let bobWithdrawn = bn('0') - let charlesWithdrawn = bn('0') - let donWithdrawn = bn('0') - - // charles withdraws SOME - const firstWithdrawAmt = bn('15000e6') - charlesWithdrawn = charlesWithdrawn.add(firstWithdrawAmt) - await wcusdcV3.connect(charles).withdraw(firstWithdrawAmt) - const newBalanceCharles = await cusdcV3.balanceOf(charles.address) - expect(newBalanceCharles).to.closeTo(firstWithdrawAmt, 50) - - // don deposits - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, initwusdcAmt, don.address) - - // bob withdraws SOME - bobWithdrawn = bobWithdrawn.add(bn('12345e6')) - await wcusdcV3.connect(bob).withdraw(bn('12345e6')) - - // don withdraws SOME - donWithdrawn = donWithdrawn.add(bn('123e6')) - await wcusdcV3.connect(don).withdraw(bn('123e6')) - - // charles withdraws ALL - charlesWithdrawn = charlesWithdrawn.add(await wcusdcV3.underlyingBalanceOf(charles.address)) - await wcusdcV3.connect(charles).withdraw(ethers.constants.MaxUint256) - - // don withdraws ALL - donWithdrawn = donWithdrawn.add(await wcusdcV3.underlyingBalanceOf(don.address)) - await wcusdcV3.connect(don).withdraw(ethers.constants.MaxUint256) - - // bob withdraws ALL - bobWithdrawn = bobWithdrawn.add(await wcusdcV3.underlyingBalanceOf(bob.address)) - await wcusdcV3.connect(bob).withdraw(ethers.constants.MaxUint256) - - const bal = await wcusdcV3.balanceOf(bob.address) - - expect(bal).to.closeTo(bn('0'), 10) - expect(await cusdcV3.balanceOf(bob.address)).to.closeTo(bobWithdrawn, 200) - expect(await cusdcV3.balanceOf(charles.address)).to.closeTo(charlesWithdrawn, 200) - expect(await cusdcV3.balanceOf(don.address)).to.closeTo(donWithdrawn, 200) - }) - - it('updates the totalSupply', async () => { - const totalSupplyBefore = await wcusdcV3.totalSupply() - const withdrawAmt = bn('15000e6') - const expectedDiff = await wcusdcV3.convertDynamicToStatic(withdrawAmt) - await wcusdcV3.connect(bob).withdraw(withdrawAmt) - // conservative rounding - expect(await wcusdcV3.totalSupply()).to.be.closeTo(totalSupplyBefore.sub(expectedDiff), 25) - }) - }) - - describe('transfer', () => { - beforeEach(async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - }) - - it('sets max allowance with approval', async () => { - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) - - // set approve - await wcusdcV3.connect(bob).allow(don.address, true) - - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(MAX_UINT256) - - // rollback approve - await wcusdcV3.connect(bob).allow(don.address, false) - - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) - }) - - it('does not transfer without approval', async () => { - await expect( - wcusdcV3.connect(bob).transferFrom(don.address, bob.address, bn('10000e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'Unauthorized') - - // Perform approval - await wcusdcV3.connect(bob).allow(don.address, true) - - await expect( - wcusdcV3.connect(don).transferFrom(bob.address, don.address, bn('10000e6')) - ).to.emit(wcusdcV3, 'Transfer') - }) - - it('transfer from/to zero address revert', async () => { - await expect( - wcusdcV3.connect(bob).transfer(ZERO_ADDRESS, bn('100e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') - - await whileImpersonating(ZERO_ADDRESS, async (signer) => { - await expect( - wcusdcV3.connect(signer).transfer(don.address, bn('100e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') - }) - }) - - it('performs validation on transfer amount', async () => { - await expect( - wcusdcV3.connect(bob).transfer(don.address, bn('40000e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'ExceedsBalance') - }) - - it('supports IERC20.approve and performs validations', async () => { - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) - expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(false) - - // Cannot set approve to the zero address - await expect( - wcusdcV3.connect(bob).approve(ZERO_ADDRESS, bn('10000e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') - - // Can set full allowance with max uint256 - await expect(wcusdcV3.connect(bob).approve(don.address, MAX_UINT256)).to.emit( - wcusdcV3, - 'Approval' - ) - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(MAX_UINT256) - expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(true) - - // Can revert allowance with zero amount - await expect(wcusdcV3.connect(bob).approve(don.address, bn(0))).to.emit(wcusdcV3, 'Approval') - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) - expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(false) - - // Any other amount reverts - await expect( - wcusdcV3.connect(bob).approve(don.address, bn('10000e6')) - ).to.be.revertedWithCustomError(wcusdcV3, 'BadAmount') - expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) - expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(false) - }) - - it('perform validations on allow', async () => { - await expect(wcusdcV3.connect(bob).allow(ZERO_ADDRESS, true)).to.be.revertedWithCustomError( - wcusdcV3, - 'ZeroAddress' - ) - - await whileImpersonating(ZERO_ADDRESS, async (signer) => { - await expect( - wcusdcV3.connect(signer).allow(don.address, true) - ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') - }) - }) - - it('updates balances and rewards in sender and receiver', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - - await enableRewardsAccrual(cusdcV3) - await advanceTime(1000) - - await wcusdcV3.accrueAccount(don.address) - await wcusdcV3.accrueAccount(bob.address) - - // Don's rewards accrual should be less than Bob's because he deposited later - expect(await wcusdcV3.baseTrackingAccrued(don.address)).to.be.lt( - await wcusdcV3.baseTrackingAccrued(bob.address) - ) - const bobBal1 = await wcusdcV3.balanceOf(bob.address) - const donBal1 = await wcusdcV3.balanceOf(don.address) - await wcusdcV3.connect(bob).transfer(don.address, bn('10000e6')) - const bobBal2 = await wcusdcV3.balanceOf(bob.address) - const donBal2 = await wcusdcV3.balanceOf(don.address) - - expect(bobBal2).equal(bobBal1.sub(bn('10000e6'))) - expect(donBal2).equal(donBal1.add(bn('10000e6'))) - - await advanceTime(1000) - await wcusdcV3.accrueAccount(don.address) - await wcusdcV3.accrueAccount(bob.address) - - expect(await wcusdcV3.baseTrackingAccrued(don.address)).to.be.gt( - await wcusdcV3.baseTrackingAccrued(bob.address) - ) - - const donsBalance = (await wcusdcV3.underlyingBalanceOf(don.address)).toBigInt() - const bobsBalance = (await wcusdcV3.underlyingBalanceOf(bob.address)).toBigInt() - expect(donsBalance).to.be.gt(bobsBalance) - const totalBalances = donsBalance + bobsBalance - - // Rounding in favor of the Wrapped Token is happening here. Amount is negligible - expect(totalBalances).to.be.closeTo(await cusdcV3.balanceOf(wcusdcV3.address), 1) - }) - - it('does not update the total supply', async () => { - const totalSupplyBefore = await wcusdcV3.totalSupply() - await wcusdcV3.connect(bob).transfer(don.address, bn('10000e6')) - expect(totalSupplyBefore).to.equal(await wcusdcV3.totalSupply()) - }) - }) - - describe('accure / accrueAccount', () => { - it('accrues internally for the comet', async () => { - const initAccrueTime = (await cusdcV3.totalsBasic()).lastAccrualTime - await wcusdcV3.accrue() - const endAccrueTime = (await cusdcV3.totalsBasic()).lastAccrualTime - expect(endAccrueTime).gt(initAccrueTime) - }) - - it('accrues rewards over time', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - expect(await wcusdcV3.baseTrackingAccrued(bob.address)).to.eq(0) - await enableRewardsAccrual(cusdcV3) - await advanceTime(1000) - - await wcusdcV3.accrueAccount(bob.address) - expect(await wcusdcV3.baseTrackingAccrued(bob.address)).to.be.gt(0) - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.eq( - await cusdcV3.balanceOf(wcusdcV3.address) - ) - }) - - it('does not accrue when accruals are not enabled in Comet', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - expect(await wcusdcV3.baseTrackingAccrued(bob.address)).to.eq(0) - - await advanceTime(1000) - expect(await wcusdcV3.baseTrackingAccrued(bob.address)).to.eq(0) - }) - }) - - describe('underlying balance', () => { - it('returns the correct amount of decimals', async () => { - const decimals = await wcusdcV3.decimals() - expect(decimals).to.equal(6) - }) - - it('returns underlying balance of user which includes revenue', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - const wrappedBalance = await wcusdcV3.balanceOf(bob.address) - await advanceTime(1000) - expect(wrappedBalance).to.equal(await wcusdcV3.balanceOf(bob.address)) - // Underlying balance increases over time and is greater than the balance in the wrapped token - expect(wrappedBalance).to.be.lt(await wcusdcV3.underlyingBalanceOf(bob.address)) - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.eq( - await cusdcV3.balanceOf(wcusdcV3.address) - ) - - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - await advanceTime(1000) - const totalBalances = (await wcusdcV3.underlyingBalanceOf(don.address)).add( - await wcusdcV3.underlyingBalanceOf(bob.address) - ) - - const contractBalance = await cusdcV3.balanceOf(wcusdcV3.address) - expect(totalBalances).to.closeTo(contractBalance, 10) - expect(totalBalances).to.lte(contractBalance) - }) - - it('returns 0 when user has no balance', async () => { - expect(await wcusdcV3.underlyingBalanceOf(bob.address)).to.equal(0) - }) - - it('also accrues account in Comet to ensure that global indices are updated', async () => { - await enableRewardsAccrual(cusdcV3) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - const oldTrackingSupplyIndex = (await cusdcV3.totalsBasic()).trackingSupplyIndex - - await advanceTime(1000) - await wcusdcV3.accrueAccount(bob.address) - expect(oldTrackingSupplyIndex).to.be.lessThan( - (await cusdcV3.totalsBasic()).trackingSupplyIndex - ) - }) - - it('matches balance in cUSDCv3', async () => { - // mint some cusdc to bob - const amount = bn('20000e6') - await allocateUSDC(bob.address, amount) - await usdc.connect(bob).approve(cusdcV3.address, ethers.constants.MaxUint256) - await cusdcV3.connect(bob).supply(usdc.address, amount) - - // mint some wcusdc to bob, charles, don - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, amount, bob.address) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, charles, amount, charles.address) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, amount, don.address) - await advanceTime(100000) - - let totalBalances = (await wcusdcV3.underlyingBalanceOf(don.address)) - .add(await wcusdcV3.underlyingBalanceOf(bob.address)) - .add(await wcusdcV3.underlyingBalanceOf(charles.address)) - let contractBalance = await cusdcV3.balanceOf(wcusdcV3.address) - expect(totalBalances).to.be.closeTo(contractBalance, 10) - expect(totalBalances).to.be.lte(contractBalance) - - const bobBal = await wcusdcV3.balanceOf(bob.address) - await wcusdcV3.connect(bob).withdraw(bobBal) - await wcusdcV3.connect(don).withdraw(bn('10000e6')) - - totalBalances = (await wcusdcV3.underlyingBalanceOf(don.address)) - .add(await wcusdcV3.underlyingBalanceOf(bob.address)) - .add(await wcusdcV3.underlyingBalanceOf(charles.address)) - contractBalance = await cusdcV3.balanceOf(wcusdcV3.address) - expect(totalBalances).to.be.closeTo(contractBalance, 10) - expect(totalBalances).to.be.lte(contractBalance) - }) - }) - - describe('exchange rate', () => { - it('returns the correct exchange rate with 0 balance', async () => { - const totalsBasic = await cusdcV3.totalsBasic() - const baseIndexScale = await cusdcV3.baseIndexScale() - const expectedExchangeRate = totalsBasic.baseSupplyIndex.mul(bn('1e6')).div(baseIndexScale) - expect(await cusdcV3.balanceOf(wcusdcV3.address)).to.equal(0) - expect(await wcusdcV3.exchangeRate()).to.be.closeTo(expectedExchangeRate, 5) - }) - - it('returns the correct exchange rate with a positive balance', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - const totalsBasic = await cusdcV3.totalsBasic() - const baseIndexScale = await cusdcV3.baseIndexScale() - const expectedExchangeRate = totalsBasic.baseSupplyIndex.mul(bn('1e6')).div(baseIndexScale) - expect(await wcusdcV3.exchangeRate()).to.equal(expectedExchangeRate) - }) - - it('current exchange rate is a ratio of total underlying balance and total supply', async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - const totalSupply = await wcusdcV3.totalSupply() - const underlyingBalance = await cusdcV3.balanceOf(wcusdcV3.address) - expect(await wcusdcV3.exchangeRate()).to.equal( - underlyingBalance.mul(bn('1e6')).div(totalSupply) - ) - }) - }) - - describe('claiming rewards', () => { - beforeEach(async () => { - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - }) - - it('does not claim rewards when user has no permission', async () => { - await advanceTime(1000) - await enableRewardsAccrual(cusdcV3) - await expect( - wcusdcV3.connect(don).claimTo(bob.address, bob.address) - ).to.be.revertedWithCustomError(wcusdcV3, 'Unauthorized') - - await wcusdcV3.connect(bob).allow(don.address, true) - expect(await wcusdcV3.isAllowed(bob.address, don.address)).to.eq(true) - await expect(wcusdcV3.connect(don).claimTo(bob.address, bob.address)).to.emit( - wcusdcV3, - 'RewardsClaimed' - ) - }) - - it('regression test: able to claim rewards even when they are big without overflow', async () => { - // Nov 28 2023: uint64 math in CusdcV3Wrapper contract results in overflow when COMP rewards are even moderately large - - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) - await advanceTime(1000) - await enableRewardsAccrual(cusdcV3, bn('2e18')) // enough to revert on uint64 implementation - - await expect(wcusdcV3.connect(bob).claimRewards()).to.emit(wcusdcV3, 'RewardsClaimed') - expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) - }) - - it('claims rewards and sends to claimer (claimTo)', async () => { - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) - await advanceTime(1000) - await enableRewardsAccrual(cusdcV3) - - await expect(wcusdcV3.connect(bob).claimTo(bob.address, bob.address)).to.emit( - wcusdcV3, - 'RewardsClaimed' - ) - expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) - }) - - it('caps at balance to avoid reverts when claiming rewards (claimTo)', async () => { - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) - await advanceTime(1000) - await enableRewardsAccrual(cusdcV3) - - // Accrue multiple times - for (let i = 0; i < 10; i++) { - await advanceTime(1000) - await wcusdcV3.accrue() - } - - // Get rewards from Comet - const cometRewards = await ethers.getContractAt('ICometRewards', REWARDS) - await whileImpersonating(wcusdcV3.address, async (signer) => { - await cometRewards - .connect(signer) - .claimTo(cusdcV3.address, wcusdcV3.address, wcusdcV3.address, true) - }) - - // Accrue individual account - await wcusdcV3.accrueAccount(bob.address) - - // Due to rounding, balance is smaller that owed - const owed = await wcusdcV3.getRewardOwed(bob.address) - const bal = await compToken.balanceOf(wcusdcV3.address) - expect(owed).to.be.greaterThan(bal) - - // Should still be able to claimTo (caps at balance) - const balanceBobPrev = await compToken.balanceOf(bob.address) - await expect(wcusdcV3.connect(bob).claimTo(bob.address, bob.address)).to.emit( - wcusdcV3, - 'RewardsClaimed' - ) - - expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(balanceBobPrev) - }) - - it('claims rewards and sends to claimer (claimRewards)', async () => { - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) - await advanceTime(1000) - await enableRewardsAccrual(cusdcV3) - - await expect(wcusdcV3.connect(bob).claimRewards()).to.emit(wcusdcV3, 'RewardsClaimed') - expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) - }) - - it('claims rewards by participation', async () => { - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - - await enableRewardsAccrual(cusdcV3) - await advanceTime(1000) - - expect(await compToken.balanceOf(bob.address)).to.equal(0) - expect(await compToken.balanceOf(don.address)).to.equal(0) - expect(await compToken.balanceOf(wcusdcV3.address)).to.equal(0) - - // claim at the same time - await network.provider.send('evm_setAutomine', [false]) - await wcusdcV3.connect(bob).claimTo(bob.address, bob.address) - await wcusdcV3.connect(don).claimTo(don.address, don.address) - await network.provider.send('evm_setAutomine', [true]) - await advanceBlocks(1) - - expect(await compToken.balanceOf(bob.address)).to.be.greaterThan(0) - const balanceBob = await compToken.balanceOf(bob.address) - const balanceDon = await compToken.balanceOf(don.address) - expect(balanceDon).lessThanOrEqual(balanceBob) - expect(balanceBob).to.be.closeTo(balanceDon, balanceBob.mul(5).div(1000)) // within 0.5% - }) - - // In this forked block, rewards accrual is not yet enabled in Comet - // Only applies to Mainnet forks (L1) - itL1('claims no rewards when rewards accrual is not enabled', async () => { - const compToken = await ethers.getContractAt('ERC20Mock', COMP) - await advanceTime(1000) - await wcusdcV3.connect(bob).claimTo(bob.address, bob.address) - expect(await compToken.balanceOf(bob.address)).to.equal(0) - }) - - it('returns reward owed after accrual and claims', async () => { - await enableRewardsAccrual(cusdcV3) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - - await advanceTime(1000) - await advanceBlocks(1) - - const bobsReward = await wcusdcV3.getRewardOwed(bob.address) - const donsReward = await wcusdcV3.getRewardOwed(don.address) - - expect(bobsReward).to.be.greaterThan(donsReward) - - await wcusdcV3.connect(bob).claimTo(bob.address, bob.address) - expect(await wcusdcV3.getRewardOwed(bob.address)).to.equal(0) - - await advanceTime(1000) - expect(await wcusdcV3.getRewardOwed(bob.address)).to.be.greaterThan(0) - }) - - it('accrues the account on deposit and withdraw', async () => { - await enableRewardsAccrual(cusdcV3) - await advanceTime(1200) - await advanceBlocks(100) - const expectedReward = await wcusdcV3.getRewardOwed(bob.address) - await advanceTime(12) - await advanceBlocks(1) - const newExpectedReward = await wcusdcV3.getRewardOwed(bob.address) - // marginal increase in exepected reward due to time passed - expect(newExpectedReward).gt(expectedReward) - - await advanceTime(1200) - await wcusdcV3.connect(bob).withdraw(ethers.constants.MaxUint256) - const nextExpectedReward = await wcusdcV3.getRewardOwed(bob.address) - await advanceTime(1200) - const lastExpectedReward = await wcusdcV3.getRewardOwed(bob.address) - // expected reward stays the same because account is empty - expect(lastExpectedReward).to.eq(nextExpectedReward) - }) - }) - - describe('baseTrackingAccrued', () => { - it('matches baseTrackingAccrued in cUSDCv3 over time', async () => { - await enableRewardsAccrual(cusdcV3) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - let wrappedTokenAccrued = await cusdcV3.baseTrackingAccrued(wcusdcV3.address) - expect(wrappedTokenAccrued).to.equal(await wcusdcV3.baseTrackingAccrued(bob.address)) - - await wcusdcV3.accrueAccount(bob.address) - - wrappedTokenAccrued = await cusdcV3.baseTrackingAccrued(wcusdcV3.address) - expect(wrappedTokenAccrued).to.equal(await wcusdcV3.baseTrackingAccrued(bob.address)) - expect((await cusdcV3.callStatic.userBasic(wcusdcV3.address)).baseTrackingIndex).to.equal( - await wcusdcV3.baseTrackingIndex(bob.address) - ) - - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, charles, bn('20000e6'), charles.address) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - - await advanceTime(1000) - - await network.provider.send('evm_setAutomine', [false]) - await wcusdcV3.accrueAccount(bob.address) - await wcusdcV3.accrueAccount(charles.address) - await wcusdcV3.accrueAccount(don.address) - await advanceBlocks(1) - await network.provider.send('evm_setAutomine', [true]) - - // All users' total accrued rewards in Wrapped cUSDC should closely match Wrapped cUSDC's - // accrued rewards in cUSDC. - const bobBTA = await wcusdcV3.baseTrackingAccrued(bob.address) - const charlesBTA = await wcusdcV3.baseTrackingAccrued(charles.address) - const donBTA = await wcusdcV3.baseTrackingAccrued(don.address) - const totalUsersAccrued = bobBTA.add(charlesBTA).add(donBTA) - wrappedTokenAccrued = await cusdcV3.baseTrackingAccrued(wcusdcV3.address) - expect(wrappedTokenAccrued).to.be.closeTo(totalUsersAccrued, 5) - }) - - it('matches baseTrackingAccrued in cUSDCv3 after withdrawals', async () => { - await enableRewardsAccrual(cusdcV3) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) - await mintWcUSDC(usdc, cusdcV3, wcusdcV3, don, bn('20000e6'), don.address) - - await advanceTime(1000) - await wcusdcV3.connect(bob).withdrawTo(bob.address, bn('10000e6')) - - await advanceTime(1000) - - await network.provider.send('evm_setAutomine', [false]) - await wcusdcV3.accrueAccount(bob.address) - await wcusdcV3.accrueAccount(don.address) - await advanceBlocks(1) - await network.provider.send('evm_setAutomine', [true]) - - // All users' total accrued rewards in Wrapped cUSDC should match Wrapped cUSDC's accrued rewards in cUSDC. - const totalUsersAccrued = (await wcusdcV3.baseTrackingAccrued(bob.address)).add( - await wcusdcV3.baseTrackingAccrued(don.address) - ) - const wrappedTokenAccrued = await cusdcV3.baseTrackingAccrued(wcusdcV3.address) - expect(wrappedTokenAccrued).to.closeTo(totalUsersAccrued, 10) - // expect(wrappedTokenAccrued).to.eq(totalUsersAccrued) - }) - }) -}) diff --git a/test/plugins/individual-collateral/compoundv3/constants.ts b/test/plugins/individual-collateral/compoundv3/constants.ts index 1b884a5473..99af24181b 100644 --- a/test/plugins/individual-collateral/compoundv3/constants.ts +++ b/test/plugins/individual-collateral/compoundv3/constants.ts @@ -3,7 +3,7 @@ import { networkConfig } from '../../../../common/configuration' import { useEnv } from '#/utils/env' export const forkNetwork = useEnv('FORK_NETWORK') ?? 'mainnet' -let chainId +let chainId: string switch (forkNetwork) { case 'mainnet': @@ -22,25 +22,62 @@ switch (forkNetwork) { const USDC_NAME = 'USDC' const CUSDC_NAME = 'cUSDCv3' + const USDC_HOLDERS: { [key: string]: string } = { '1': '0x0a59649758aa4d66e25f08dd01271e891fe52199', '8453': '0xcdac0d6c6c59727a65f871236188350531885c43', '42161': '0x2df1c51e09aecf9cacb7bc98cb1742757f163df7', } -const FORK_BLOCKS: { [key: string]: number } = { +const USDT_HOLDERS: { [key: string]: string } = { + '1': '0xF977814e90dA44bFA03b6295A0616a897441aceC', + '8453': '0x0000000000000000000000000000000000000000', + '42161': '0xF977814e90dA44bFA03b6295A0616a897441aceC', +} + +export const HOLDERS: { [key: string]: { [chainId: string]: string } } = { + USDC: USDC_HOLDERS, + USDT: USDT_HOLDERS, +} + +export const getHolder = (tokenName: string): string => { + return HOLDERS[tokenName][chainId] +} + +const USDC_FORK_BLOCKS: { [key: string]: number } = { '1': 15850930, '8453': 12292893, '42161': 193157126, } +const USDT_FORK_BLOCKS: { [key: string]: number } = { + '1': 20528446, + '8453': 12292893, // not used + '42161': 227293528, +} + +export const FORK_BLOCKS: { [key: string]: { [chainId: string]: number } } = { + USDC: USDC_FORK_BLOCKS, + USDT: USDT_FORK_BLOCKS, +} + +export const getForkBlock = (tokenName: string): number => { + return FORK_BLOCKS[tokenName][chainId] +} + // Mainnet Addresses export const RSR = networkConfig[chainId].tokens.RSR as string export const USDC_USD_PRICE_FEED = networkConfig[chainId].chainlinkFeeds.USDC as string export const CUSDC_V3 = networkConfig[chainId].tokens[CUSDC_NAME]! +export const USDC = networkConfig[chainId].tokens[USDC_NAME]! +export const USDC_DECIMALS = bn(6) + +export const USDT_USD_PRICE_FEED = networkConfig[chainId].chainlinkFeeds.USDT as string +export const CUSDT_V3 = networkConfig[chainId].tokens.cUSDTv3 as string +export const USDT = networkConfig[chainId].tokens.USDT as string +export const USDT_DECIMALS = bn(6) + export const COMP = networkConfig[chainId].tokens.COMP as string export const REWARDS = networkConfig[chainId].COMET_REWARDS! -export const USDC = networkConfig[chainId].tokens[USDC_NAME]! -export const USDC_HOLDER = USDC_HOLDERS[chainId] export const COMET_CONFIGURATOR = networkConfig[chainId].COMET_CONFIGURATOR! export const COMET_PROXY_ADMIN = networkConfig[chainId].COMET_PROXY_ADMIN! export const COMET_EXT = networkConfig[chainId].COMET_EXT! @@ -51,6 +88,3 @@ export const ORACLE_ERROR = fp('0.005') export const DEFAULT_THRESHOLD = bn(5).mul(bn(10).pow(16)) // 0.05 export const DELAY_UNTIL_DEFAULT = bn(86400) export const MAX_TRADE_VOL = bn(1000000) -export const USDC_DECIMALS = bn(6) - -export const FORK_BLOCK = FORK_BLOCKS[chainId] diff --git a/test/plugins/individual-collateral/compoundv3/helpers.ts b/test/plugins/individual-collateral/compoundv3/helpers.ts index f08566f823..dc682311b7 100644 --- a/test/plugins/individual-collateral/compoundv3/helpers.ts +++ b/test/plugins/individual-collateral/compoundv3/helpers.ts @@ -5,23 +5,25 @@ import { CometInterface, ICometConfigurator, ICometProxyAdmin, - ICusdcV3Wrapper, - CusdcV3Wrapper__factory, + ICFiatV3Wrapper, + CFiatV3Wrapper__factory, } from '../../../../typechain' import { whileImpersonating } from '../../../utils/impersonation' import { bn } from '../../../../common/numbers' import { BigNumberish } from 'ethers' import { - USDC_HOLDER, USDC, + USDT, + USDC_USD_PRICE_FEED, + USDT_USD_PRICE_FEED, COMET_CONFIGURATOR, COMET_PROXY_ADMIN, CUSDC_V3, + CUSDT_V3, REWARDS, COMP, - FORK_BLOCK, + getHolder, } from './constants' -import { getResetFork } from '../helpers' export const enableRewardsAccrual = async ( cusdcV3: CometInterface, @@ -49,60 +51,126 @@ const allocateERC20 = async (token: ERC20Mock, from: string, to: string, balance }) } -export const allocateUSDC = async ( +export const allocateToken = async ( to: string, balance: BigNumberish, - from: string = USDC_HOLDER, - token: string = USDC + from: string, + token: string ) => { - const usdc = await ethers.getContractAt('ERC20Mock', token) - await allocateERC20(usdc, from, to, balance) + const erc20 = await ethers.getContractAt('ERC20Mock', token) + await allocateERC20(erc20, from, to, balance) } -interface WrappedcUSDCFixture { - cusdcV3: CometInterface - wcusdcV3: ICusdcV3Wrapper - usdc: ERC20Mock +export interface WrappedCTokenFixture { + cTokenV3: CometInterface + wcTokenV3: ICFiatV3Wrapper + token: ERC20Mock } -export const mintWcUSDC = async ( - usdc: ERC20Mock, - cusdc: CometInterface, - wcusdc: ICusdcV3Wrapper, +export const mintWcToken = async ( + token: ERC20Mock, + cTokenV3: CometInterface, + wcTokenV3: ICFiatV3Wrapper, account: SignerWithAddress, amount: BigNumberish, recipient: string ) => { - const initBal = await cusdc.balanceOf(account.address) + const initBal = await cTokenV3.balanceOf(account.address) // do these actions together to move rate as little as possible await hre.network.provider.send('evm_setAutomine', [false]) - const usdcAmount = await wcusdc.convertStaticToDynamic(amount) - await allocateUSDC(account.address, usdcAmount) - await usdc.connect(account).approve(cusdc.address, ethers.constants.MaxUint256) - await cusdc.connect(account).allow(wcusdc.address, true) + const tokenAmount = await wcTokenV3.convertStaticToDynamic(amount) + await allocateToken(account.address, tokenAmount, getHolder(await token.symbol()), token.address) + await token.connect(account).approve(cTokenV3.address, ethers.constants.MaxUint256) + await cTokenV3.connect(account).allow(wcTokenV3.address, true) await hre.network.provider.send('evm_setAutomine', [true]) - await cusdc.connect(account).supply(usdc.address, usdcAmount) - const nowBal = await cusdc.balanceOf(account.address) + await cTokenV3.connect(account).supply(token.address, tokenAmount) + const nowBal = await cTokenV3.balanceOf(account.address) if (account.address == recipient) { - await wcusdc.connect(account).deposit(nowBal.sub(initBal)) + await wcTokenV3.connect(account).deposit(nowBal.sub(initBal)) } else { - await wcusdc.connect(account).depositTo(recipient, nowBal.sub(initBal)) + await wcTokenV3.connect(account).depositTo(recipient, nowBal.sub(initBal)) } } -export const makewCSUDC = async (): Promise => { +export const makewCSUDC = async (): Promise => { const cusdcV3 = await ethers.getContractAt('CometInterface', CUSDC_V3) - const CusdcV3WrapperFactory = ( - await ethers.getContractFactory('CusdcV3Wrapper') + const CTokenV3WrapperFactory = ( + await ethers.getContractFactory('CFiatV3Wrapper') ) - const wcusdcV3 = ( - await CusdcV3WrapperFactory.deploy(cusdcV3.address, REWARDS, COMP) + const wcusdcV3 = ( + await CTokenV3WrapperFactory.deploy( + cusdcV3.address, + REWARDS, + COMP, + 'Wrapped cUSDCv3', + 'wcUSDCv3' + ) ) const usdc = await ethers.getContractAt('ERC20Mock', USDC) - return { cusdcV3, wcusdcV3, usdc } + return { cTokenV3: cusdcV3, wcTokenV3: wcusdcV3, token: usdc } +} + +export const makewCSUDT = async (): Promise => { + const cusdtV3 = await ethers.getContractAt('CometInterface', CUSDT_V3) + const CTokenV3WrapperFactory = ( + await ethers.getContractFactory('CFiatV3Wrapper') + ) + const wcusdtV3 = ( + await CTokenV3WrapperFactory.deploy( + cusdtV3.address, + REWARDS, + COMP, + 'Wrapped cUSDTv3', + 'wcUSDTv3' + ) + ) + const usdt = await ethers.getContractAt('ERC20Mock', USDT) + + return { cTokenV3: cusdtV3, wcTokenV3: wcusdtV3, token: usdt } +} + +// Test configuration +export interface CTokenV3Enumeration { + testName: string + forkNetwork: string + wrapperName: string + wrapperSymbol: string + cTokenV3: string + token: string + tokenName: string + chainlinkFeed: string + fix: typeof makewCSUDC +} + +const cUSDCv3 = { + testName: 'CompoundV3USDC', + wrapperName: 'Wrapped cUSDCv3', + wrapperSymbol: 'wcUSDCv3', + cTokenV3: CUSDC_V3, + token: USDC, + tokenName: 'USDC', + chainlinkFeed: USDC_USD_PRICE_FEED, + fix: makewCSUDC, +} + +const cUSDTv3 = { + testName: 'CompoundV3USDT', + wrapperName: 'Wrapped cUSDTv3', + wrapperSymbol: 'wcUSDTv3', + cTokenV3: CUSDT_V3, + token: USDT, + tokenName: 'USDT', + chainlinkFeed: USDT_USD_PRICE_FEED, + fix: makewCSUDT, } -export const resetFork = getResetFork(FORK_BLOCK) +export const allTests = [ + { ...cUSDCv3, forkNetwork: 'mainnet' }, + { ...cUSDCv3, forkNetwork: 'base' }, + { ...cUSDCv3, forkNetwork: 'arbitrum' }, + { ...cUSDTv3, forkNetwork: 'mainnet' }, + { ...cUSDTv3, forkNetwork: 'arbitrum' }, +]