From 6ebe5e51b9c198155aa11df217dab200c893a637 Mon Sep 17 00:00:00 2001 From: kamescg Date: Tue, 28 Sep 2021 08:18:10 -0400 Subject: [PATCH 1/5] coverage(PrizePool): increase coverage percentage --- contracts/interfaces/IPrizePool.sol | 40 +- contracts/prize-pool/PrizePool.sol | 40 +- contracts/test/PrizePoolHarness.sol | 14 +- test/prize-pool/PrizePool.ordered.test.ts | 450 +++++++++++++++++++ test/prize-pool/PrizePool.test.ts | 499 +++++++++++++--------- 5 files changed, 823 insertions(+), 220 deletions(-) create mode 100644 test/prize-pool/PrizePool.ordered.test.ts diff --git a/contracts/interfaces/IPrizePool.sol b/contracts/interfaces/IPrizePool.sol index 72005cc6..bc0d0680 100644 --- a/contracts/interfaces/IPrizePool.sol +++ b/contracts/interfaces/IPrizePool.sol @@ -88,8 +88,7 @@ interface IPrizePool { function depositTo( address to, uint256 amount - ) - external; + ) external; /// @notice Withdraw assets from the Prize Pool instantly. A fairness fee may be charged for an early exit. /// @param from The address to redeem tokens from. @@ -100,6 +99,12 @@ interface IPrizePool { uint256 amount ) external returns (uint256); + /// @notice Called by the prize strategy to award prizes. + /// @dev The amount awarded must be less than the awardBalance() + /// @param to The address of the winner that receives the award + /// @param amount The amount of assets to be awarded + function award( address to, uint256 amount) external; + /// @notice Returns the balance that is available to award. /// @dev captureAwardBalance() should be called first /// @return The total amount of assets to be awarded for the current prize @@ -119,17 +124,32 @@ interface IPrizePool { /// @return The underlying balance of assets function balance() external returns (uint256); + /** + * @notice Read internal Ticket accounted balance. + */ + function getAccountedBalance() external view returns (uint256); + /** + * @notice Read internal balanceCap variable + */ + function getBalanceCap() external view returns (uint256); + /** + * @notice Read internal liquidityCap variable + */ + function getLiquidityCap() external view returns (uint256); + /** + * @notice Read ticket variable + */ + function getTicket() external view returns (IControlledToken); + /** + * @notice Read prizeStrategy variable + */ + function getPrizeStrategy() external view returns (address); + /// @dev Checks if a specific token is controlled by the Prize Pool /// @param _controlledToken The address of the token to check /// @return True if the token is a controlled token, false otherwise function isControlled(IControlledToken _controlledToken) external view returns (bool); - /// @notice Called by the prize strategy to award prizes. - /// @dev The amount awarded must be less than the awardBalance() - /// @param to The address of the winner that receives the award - /// @param amount The amount of assets to be awarded - function award( address to, uint256 amount) external; - /// @notice Called by the Prize-Strategy to transfer out external ERC20 tokens /// @dev Used to transfer out tokens held by the Prize Pool. Could be liquidated, or anything. /// @param to The address of the winner that receives the award @@ -175,10 +195,6 @@ interface IPrizePool { /// @dev Returns the address of the prize pool ticket. /// @return The address of the prize pool ticket. function ticket() external view returns (IControlledToken); - - /// @dev Returns the address of the prize pool ticket. - /// @return The address of the prize pool ticket. - function getTicket() external view returns (IControlledToken); /// @dev Returns the address of the underlying ERC20 asset /// @return The address of the asset diff --git a/contracts/prize-pool/PrizePool.sol b/contracts/prize-pool/PrizePool.sol index 49d13815..2fd8dc14 100644 --- a/contracts/prize-pool/PrizePool.sol +++ b/contracts/prize-pool/PrizePool.sol @@ -69,11 +69,6 @@ abstract contract PrizePool is IPrizePool, Ownable, ReentrancyGuard, IERC721Rece /* ============ External Functions ============ */ - /// @inheritdoc IPrizePool - function token() external override view returns (address) { - return address(_token()); - } - /// @inheritdoc IPrizePool function balance() external override returns (uint256) { return _balance(); @@ -83,7 +78,10 @@ abstract contract PrizePool is IPrizePool, Ownable, ReentrancyGuard, IERC721Rece function awardBalance() external override view returns (uint256) { return _currentAwardBalance; } - + /// @inheritdoc IPrizePool + function accountedBalance() external override view returns (uint256) { + return _ticketTotalSupply(); + } /// @inheritdoc IPrizePool function canAwardExternal(address _externalToken) external override view returns (bool) { return _canAwardExternal(_externalToken); @@ -93,12 +91,33 @@ abstract contract PrizePool is IPrizePool, Ownable, ReentrancyGuard, IERC721Rece function isControlled(IControlledToken _controlledToken) external override view returns (bool) { return _isControlled(_controlledToken); } - + /// @inheritdoc IPrizePool + function getAccountedBalance() external override view returns (uint256) { + return _ticketTotalSupply(); + } + /// @inheritdoc IPrizePool + function getBalanceCap() external override view returns (uint256) { + return balanceCap; + } + /// @inheritdoc IPrizePool + function getLiquidityCap() external override view returns (uint256) { + return liquidityCap; + } /// @inheritdoc IPrizePool function getTicket() external override view returns (IControlledToken) { return ticket; } + /// @inheritdoc IPrizePool + function getPrizeStrategy() external override view returns (address) { + return prizeStrategy; + } + + /// @inheritdoc IPrizePool + function token() external override view returns (address) { + return address(_token()); + } + /// @inheritdoc IPrizePool function captureAwardBalance() external override nonReentrant returns (uint256) { uint256 ticketTotalSupply = _ticketTotalSupply(); @@ -251,11 +270,6 @@ abstract contract PrizePool is IPrizePool, Ownable, ReentrancyGuard, IERC721Rece _setPrizeStrategy(_prizeStrategy); } - /// @inheritdoc IPrizePool - function accountedBalance() external override view returns (uint256) { - return _ticketTotalSupply(); - } - /// @inheritdoc IPrizePool function compLikeDelegate(ICompLike _compLike, address _to) external override onlyOwner { if (_compLike.balanceOf(address(this)) > 0) { @@ -264,7 +278,7 @@ abstract contract PrizePool is IPrizePool, Ownable, ReentrancyGuard, IERC721Rece } /// @inheritdoc IERC721Receiver - function onERC721Received(address,address,uint256,bytes calldata) external pure override returns (bytes4) { + function onERC721Received(address,address,uint256,bytes calldata) public pure override returns (bytes4) { return IERC721Receiver.onERC721Received.selector; } diff --git a/contracts/test/PrizePoolHarness.sol b/contracts/test/PrizePoolHarness.sol index e529c2b5..f95c0c5d 100644 --- a/contracts/test/PrizePoolHarness.sol +++ b/contracts/test/PrizePoolHarness.sol @@ -35,6 +35,10 @@ contract PrizePoolHarness is PrizePool { function _currentTime() internal override view returns (uint256) { return currentTime; } + + function internalCurrentTime() external view returns (uint256) { + return super._currentTime(); + } function _canAwardExternal(address _externalToken) internal override view returns (bool) { return stubYieldSource.canAwardExternal(_externalToken); @@ -55,4 +59,12 @@ contract PrizePoolHarness is PrizePool { function _redeem(uint256 redeemAmount) internal override returns (uint256) { return stubYieldSource.redeemToken(redeemAmount); } -} + + function mockOnERC721Received(bytes calldata sig) external pure returns (bytes4) { + return super.onERC721Received(address(0), address(0), 0, sig); + } + + function setCurrentAwardBalance(uint256 amount) external { + _currentAwardBalance = amount; + } +} \ No newline at end of file diff --git a/test/prize-pool/PrizePool.ordered.test.ts b/test/prize-pool/PrizePool.ordered.test.ts new file mode 100644 index 00000000..ed7a0c55 --- /dev/null +++ b/test/prize-pool/PrizePool.ordered.test.ts @@ -0,0 +1,450 @@ +import { Signer } from '@ethersproject/abstract-signer'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { deployMockContract, MockContract } from 'ethereum-waffle'; +import { BigNumber, constants, Contract, ContractFactory, utils } from 'ethers'; +import { ethers, artifacts } from 'hardhat'; +import { Artifact } from 'hardhat/types'; + +import { call } from '../helpers/call'; + +const { AddressZero, MaxUint256 } = constants; +const { getContractFactory, getSigners, Wallet } = ethers; +const { parseEther: toWei } = utils; + +const debug = require('debug')('ptv3:PrizePool.test'); + +const NFT_TOKEN_ID = 1; + +describe('PrizePool', function () { + let contractsOwner: SignerWithAddress; + let wallet2: SignerWithAddress; + let prizeStrategyManager: SignerWithAddress; + let ERC20MintableContract: ContractFactory + let ICompLike: Artifact + let ERC721MintableContract: ContractFactory + let PrizePoolHarness: ContractFactory + let Ticket: ContractFactory + let IERC721: Artifact + let YieldSourceStub: Artifact + // Set as `any` cause types are conflicting between the different path for ethers + let prizePool: any; + let prizePool2: any; + let depositToken: Contract; + let erc20Token: Contract; + let erc721Token: Contract; + let ticket: Contract; + let compLike: MockContract; + let erc721tokenMock: MockContract; + let yieldSourceStub: MockContract; + + const depositTokenIntoPrizePool = async ( + walletAddress: string, + amount: BigNumber, + token: Contract = depositToken, + pool: Contract = prizePool, + ) => { + await yieldSourceStub.mock.supplyTokenTo.withArgs(amount, pool.address).returns(); + + await token.approve(pool.address, amount); + await token.mint(walletAddress, amount); + + if (token.address === depositToken.address) { + return await pool.depositTo(walletAddress, amount); + } else { + return await token.transfer(pool.address, amount); + } + }; + + const depositNftIntoPrizePool = async (walletAddress: string) => { + await erc721Token.mint(walletAddress, NFT_TOKEN_ID); + await erc721Token.transferFrom(walletAddress, prizePool.address, NFT_TOKEN_ID); + }; + + before(async () => { + [contractsOwner, wallet2, prizeStrategyManager] = await getSigners(); + debug(`using wallet ${contractsOwner.address}`); + ERC20MintableContract = await getContractFactory('ERC20Mintable', contractsOwner); + ICompLike = await artifacts.readArtifact('ICompLike'); + ERC721MintableContract = await getContractFactory('ERC721Mintable', contractsOwner); + PrizePoolHarness = await getContractFactory('PrizePoolHarness', contractsOwner); + Ticket = await getContractFactory('Ticket'); + IERC721 = await artifacts.readArtifact('IERC721'); + YieldSourceStub = await artifacts.readArtifact('YieldSourceStub'); + compLike = await deployMockContract(contractsOwner as Signer, ICompLike.abi); + }) + + beforeEach(async () => { + debug('mocking tokens...'); + depositToken = await ERC20MintableContract.deploy('Token', 'TOKE'); + erc20Token = await ERC20MintableContract.deploy('Token', 'TOKE'); + erc721Token = await ERC721MintableContract.deploy(); + erc721tokenMock = await deployMockContract(contractsOwner as Signer, IERC721.abi); + yieldSourceStub = await deployMockContract(contractsOwner as Signer, YieldSourceStub.abi); + await yieldSourceStub.mock.depositToken.returns(depositToken.address); + prizePool = await PrizePoolHarness.deploy(contractsOwner.address, yieldSourceStub.address); + ticket = await Ticket.deploy('Ticket', 'TICK', 18, prizePool.address); + + await prizePool.setTicket(ticket.address); + await prizePool.setPrizeStrategy(prizeStrategyManager.address); + }); + + /*============================================ */ + // Constructor Functions --------------------- + /*============================================ */ + describe('constructor()', () => { + it('should fire the events', async () => { + const deployTx = prizePool.deployTransaction; + + await expect(deployTx).to.emit(prizePool, 'LiquidityCapSet').withArgs(MaxUint256); + + await expect(prizePool.setPrizeStrategy(prizeStrategyManager.address)) + .to.emit(prizePool, 'PrizeStrategySet') + .withArgs(prizeStrategyManager.address); + + const setTicketTx = await prizePool.setTicket(ticket.address); + + await expect(setTicketTx) + .to.emit(prizePool, 'TicketSet') + .withArgs(ticket.address); + + await expect(setTicketTx) + .to.emit(prizePool, 'BalanceCapSet') + .withArgs(MaxUint256); + }); + + it('should set all the vars', async () => { + expect(await prizePool.token()).to.equal(depositToken.address); + }); + + it('should reject invalid params', async () => { + const PrizePoolHarness = await getContractFactory('PrizePoolHarness', contractsOwner); + prizePool2 = await PrizePoolHarness.deploy(contractsOwner.address, yieldSourceStub.address); + + await expect(prizePool2.setTicket(AddressZero)).to.be.revertedWith( + 'PrizePool/ticket-not-zero-address', + ); + }); + }); + + /*============================================ */ + // Core Functions ---------------------------- + /*============================================ */ + describe('Core Functions', () => { + describe('depositTo()', () => { + it('should revert when deposit exceeds liquidity cap', async () => { + const amount = toWei('1'); + const liquidityCap = toWei('1000'); + + await depositTokenIntoPrizePool(contractsOwner.address, liquidityCap); + + await prizePool.setLiquidityCap(liquidityCap); + + await expect( + prizePool.depositTo(wallet2.address, amount), + ).to.be.revertedWith('PrizePool/exceeds-liquidity-cap'); + }); + + it('should revert when user deposit exceeds ticket balance cap', async () => { + const amount = toWei('1'); + const balanceCap = toWei('50000'); + + await prizePool.setBalanceCap(balanceCap); + await depositTokenIntoPrizePool(contractsOwner.address, balanceCap); + + await expect(depositTokenIntoPrizePool(contractsOwner.address, amount)).to.be.revertedWith( + 'PrizePool/exceeds-balance-cap', + ); + }); + }); + + describe('captureAwardBalance()', () => { + it('should handle when the balance is less than the collateral', async () => { + await depositTokenIntoPrizePool(contractsOwner.address, toWei('100')); + + await yieldSourceStub.mock.balanceOfToken + .withArgs(prizePool.address) + .returns(toWei('99.9999')); + + expect(await prizePool.awardBalance()).to.equal(toWei('0')); + }); + + it('should handle the situ when the total accrued interest is less than the captured total', async () => { + await depositTokenIntoPrizePool(contractsOwner.address, toWei('100')); + + await yieldSourceStub.mock.balanceOfToken.withArgs(prizePool.address).returns(toWei('110')); + + // first capture the 10 tokens + await prizePool.captureAwardBalance(); + + await yieldSourceStub.mock.balanceOfToken + .withArgs(prizePool.address) + .returns(toWei('109.999')); + + // now try to capture again + await expect(prizePool.captureAwardBalance()).to.not.emit(prizePool, 'AwardCaptured'); + }); + + it('should track the yield less the total token supply', async () => { + await depositTokenIntoPrizePool(contractsOwner.address, toWei('100')); + + await yieldSourceStub.mock.balanceOfToken.withArgs(prizePool.address).returns(toWei('110')); + + await expect(prizePool.captureAwardBalance()) + .to.emit(prizePool, 'AwardCaptured') + .withArgs(toWei('10')); + expect(await prizePool.awardBalance()).to.equal(toWei('10')); + }); + }); + + describe('withdrawFrom()', () => { + it('should allow a user to withdraw instantly', async () => { + let amount = toWei('10'); + + await depositTokenIntoPrizePool(contractsOwner.address, amount); + + await yieldSourceStub.mock.redeemToken.withArgs(amount).returns(amount); + + await expect(prizePool.withdrawFrom(contractsOwner.address, amount)) + .to.emit(prizePool, 'Withdrawal') + .withArgs(contractsOwner.address, contractsOwner.address, ticket.address, amount, amount); + }); + }); + }) + + /*============================================ */ + // Getter Functions -------------------------- + /*============================================ */ + describe('Getter Functions', () => { + describe('balance()', () => { + it('should return zero if no deposits have been made', async () => { + const balance = toWei('11'); + await yieldSourceStub.mock.balanceOfToken.withArgs(prizePool.address).returns(balance); + expect((await call(prizePool, 'balance')).toString()).to.equal(balance); + }); + }); + }) + + /*============================================ */ + // Setter Functions -------------------------- + /*============================================ */ + describe('Setter Functions', () => { + describe('setPrizeStrategy()', () => { + it('should allow the owner to swap the prize strategy', async () => { + const randomWallet = Wallet.createRandom(); + + await expect(prizePool.setPrizeStrategy(randomWallet.address)) + .to.emit(prizePool, 'PrizeStrategySet') + .withArgs(randomWallet.address); + + expect(await prizePool.prizeStrategy()).to.equal(randomWallet.address); + }); + + it('should not allow anyone else to change the prize strategy', async () => { + await expect( + prizePool.connect(wallet2 as Signer).setPrizeStrategy(wallet2.address), + ).to.be.revertedWith('Ownable/caller-not-owner'); + }); + }); + + describe('setBalanceCap', () => { + it('should allow the owner to set the balance cap', async () => { + const balanceCap = toWei('50000'); + + await expect(prizePool.setBalanceCap(balanceCap)) + .to.emit(prizePool, 'BalanceCapSet') + .withArgs(balanceCap); + + expect(await prizePool.balanceCap()).to.equal(balanceCap); + }); + + it('should not allow anyone else to call', async () => { + prizePool2 = prizePool.connect(wallet2 as Signer); + + await expect(prizePool2.setBalanceCap(toWei('50000'))).to.be.revertedWith( + 'Ownable/caller-not-owner', + ); + }); + }); + + describe('setLiquidityCap', () => { + it('should allow the owner to set the liquidity cap', async () => { + const liquidityCap = toWei('1000'); + + await expect(prizePool.setLiquidityCap(liquidityCap)) + .to.emit(prizePool, 'LiquidityCapSet') + .withArgs(liquidityCap); + + expect(await prizePool.liquidityCap()).to.equal(liquidityCap); + }); + + it('should not allow anyone else to call', async () => { + prizePool2 = prizePool.connect(wallet2 as Signer); + + await expect(prizePool2.setLiquidityCap(toWei('1000'))).to.be.revertedWith( + 'Ownable/caller-not-owner', + ); + }); + }); + }) + + /*============================================ */ + // Token Functions --------------------------- + /*============================================ */ + describe('Token Functions', () => { + describe('awardExternalERC20()', () => { + beforeEach(async () => { + await prizePool.setPrizeStrategy(prizeStrategyManager.address); + }); + + it('should exit early when amount = 0', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + + await expect( + prizePool + .connect(prizeStrategyManager) + .awardExternalERC20(contractsOwner.address, erc20Token.address, 0), + ).to.not.emit(prizePool, 'AwardedExternalERC20'); + }); + + it('should only allow the prizeStrategy to award external ERC20s', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + + let prizePool2 = prizePool.connect(wallet2 as Signer); + + await expect( + prizePool2.awardExternalERC20(contractsOwner.address, wallet2.address, toWei('10')), + ).to.be.revertedWith('PrizePool/only-prizeStrategy'); + }); + + it('should allow arbitrary tokens to be transferred', async () => { + const amount = toWei('10'); + + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + + await depositTokenIntoPrizePool(contractsOwner.address, amount, erc20Token); + + await expect( + prizePool + .connect(prizeStrategyManager) + .awardExternalERC20(contractsOwner.address, erc20Token.address, amount), + ) + .to.emit(prizePool, 'AwardedExternalERC20') + .withArgs(contractsOwner.address, erc20Token.address, amount); + }); + }); + + describe('transferExternalERC20()', () => { + beforeEach(async () => { + await prizePool.setPrizeStrategy(prizeStrategyManager.address); + }); + + it('should exit early when amount = 0', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + + await expect( + prizePool + .connect(prizeStrategyManager) + .transferExternalERC20(contractsOwner.address, erc20Token.address, 0), + ).to.not.emit(prizePool, 'TransferredExternalERC20'); + }); + + it('should only allow the prizeStrategy to award external ERC20s', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + + let prizePool2 = prizePool.connect(wallet2 as Signer); + + await expect( + prizePool2.transferExternalERC20(contractsOwner.address, wallet2.address, toWei('10')), + ).to.be.revertedWith('PrizePool/only-prizeStrategy'); + }); + + it('should allow arbitrary tokens to be transferred', async () => { + const amount = toWei('10'); + + await depositTokenIntoPrizePool(contractsOwner.address, amount, erc20Token); + + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + + await expect( + prizePool + .connect(prizeStrategyManager) + .transferExternalERC20(contractsOwner.address, erc20Token.address, amount), + ) + .to.emit(prizePool, 'TransferredExternalERC20') + .withArgs(contractsOwner.address, erc20Token.address, amount); + }); + }); + + describe('awardExternalERC721()', () => { + beforeEach(async () => { + await prizePool.setPrizeStrategy(prizeStrategyManager.address); + }); + + it('should exit early when tokenIds list is empty', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); + + await expect( + prizePool + .connect(prizeStrategyManager) + .awardExternalERC721(contractsOwner.address, erc721Token.address, []), + ).to.not.emit(prizePool, 'AwardedExternalERC721'); + }); + + it('should only allow the prizeStrategy to award external ERC721s', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); + + let prizePool2 = prizePool.connect(wallet2 as Signer); + + await expect( + prizePool2.awardExternalERC721(contractsOwner.address, erc721Token.address, [NFT_TOKEN_ID]), + ).to.be.revertedWith('PrizePool/only-prizeStrategy'); + }); + + it('should allow arbitrary tokens to be transferred', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); + + await depositNftIntoPrizePool(contractsOwner.address); + + await expect( + prizePool + .connect(prizeStrategyManager) + .awardExternalERC721(contractsOwner.address, erc721Token.address, [NFT_TOKEN_ID]), + ) + .to.emit(prizePool, 'AwardedExternalERC721') + .withArgs(contractsOwner.address, erc721Token.address, [NFT_TOKEN_ID]); + }); + + it('should not DoS with faulty ERC721s', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc721tokenMock.address).returns(true); + await erc721tokenMock.mock.transferFrom + .withArgs(prizePool.address, contractsOwner.address, NFT_TOKEN_ID) + .reverts(); + + await expect( + prizePool + .connect(prizeStrategyManager) + .awardExternalERC721(contractsOwner.address, erc721tokenMock.address, [NFT_TOKEN_ID]), + ).to.emit(prizePool, 'ErrorAwardingExternalERC721'); + }); + }); + + describe('onERC721Received()', () => { + it('should receive an ERC721 token when using safeTransferFrom', async () => { + expect(await erc721Token.balanceOf(prizePool.address)).to.equal('0'); + + await depositNftIntoPrizePool(contractsOwner.address); + + expect(await erc721Token.balanceOf(prizePool.address)).to.equal('1'); + }); + }); + }) + + /*============================================ */ + // Internal Functions ------------------------ + /*============================================ */ + describe('Internal Functions', () => { + + }) + +}) \ No newline at end of file diff --git a/test/prize-pool/PrizePool.test.ts b/test/prize-pool/PrizePool.test.ts index 32026c02..51357dc2 100644 --- a/test/prize-pool/PrizePool.test.ts +++ b/test/prize-pool/PrizePool.test.ts @@ -2,38 +2,39 @@ import { Signer } from '@ethersproject/abstract-signer'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { deployMockContract, MockContract } from 'ethereum-waffle'; -import { BigNumber, constants, Contract, utils } from 'ethers'; +import { BigNumber, constants, Contract, ContractFactory, utils } from 'ethers'; import { ethers, artifacts } from 'hardhat'; - +import { Artifact } from 'hardhat/types'; import { call } from '../helpers/call'; - const { AddressZero, MaxUint256 } = constants; const { getContractFactory, getSigners, Wallet } = ethers; const { parseEther: toWei } = utils; - const debug = require('debug')('ptv3:PrizePool.test'); - const NFT_TOKEN_ID = 1; -describe('PrizePool', function () { - let contractsOwner: SignerWithAddress; +describe.only('PrizePool', function () { + let wallet1: SignerWithAddress; let wallet2: SignerWithAddress; let prizeStrategyManager: SignerWithAddress; - + let wallet4: SignerWithAddress; + let wallet5: SignerWithAddress; + let ICompLike: Artifact + let ERC20MintableContract: ContractFactory + let ERC721MintableContract: ContractFactory + let PrizePoolHarness: ContractFactory + let Ticket: ContractFactory + let IERC721: Artifact + let YieldSourceStub: Artifact // Set as `any` cause types are conflicting between the different path for ethers let prizePool: any; let prizePool2: any; - - let yieldSourceStub: MockContract; - let depositToken: Contract; let erc20Token: Contract; let erc721Token: Contract; - let erc721tokenMock: MockContract; - let ticket: Contract; - let compLike: MockContract; + let erc721tokenMock: MockContract; + let yieldSourceStub: MockContract; const depositTokenIntoPrizePool = async ( walletAddress: string, @@ -58,36 +59,42 @@ describe('PrizePool', function () { await erc721Token.transferFrom(walletAddress, prizePool.address, NFT_TOKEN_ID); }; - beforeEach(async () => { - [contractsOwner, wallet2, prizeStrategyManager] = await getSigners(); - debug(`using wallet ${contractsOwner.address}`); + before(async () => { + [wallet1, wallet2, prizeStrategyManager, wallet4, wallet5] = await getSigners(); + debug(`using wallet ${wallet1.address}`); + ERC20MintableContract = await getContractFactory('ERC20Mintable', wallet1); + ICompLike = await artifacts.readArtifact('ICompLike'); + ERC721MintableContract = await getContractFactory('ERC721Mintable', wallet1); + PrizePoolHarness = await getContractFactory('PrizePoolHarness', wallet1); + Ticket = await getContractFactory('Ticket'); + IERC721 = await artifacts.readArtifact('IERC721'); + YieldSourceStub = await artifacts.readArtifact('YieldSourceStub'); + compLike = await deployMockContract(wallet1 as Signer, ICompLike.abi); + }) + beforeEach(async () => { debug('mocking tokens...'); - const ERC20MintableContract = await getContractFactory('ERC20Mintable', contractsOwner); depositToken = await ERC20MintableContract.deploy('Token', 'TOKE'); erc20Token = await ERC20MintableContract.deploy('Token', 'TOKE'); - - const ICompLike = await artifacts.readArtifact('ICompLike'); - compLike = await deployMockContract(contractsOwner as Signer, ICompLike.abi); - - const ERC721MintableContract = await getContractFactory('ERC721Mintable', contractsOwner); erc721Token = await ERC721MintableContract.deploy(); - - const IERC721 = await artifacts.readArtifact('IERC721'); - erc721tokenMock = await deployMockContract(contractsOwner as Signer, IERC721.abi); - - const YieldSourceStub = await artifacts.readArtifact('YieldSourceStub'); - yieldSourceStub = await deployMockContract(contractsOwner as Signer, YieldSourceStub.abi); + erc721tokenMock = await deployMockContract(wallet1 as Signer, IERC721.abi); + yieldSourceStub = await deployMockContract(wallet1 as Signer, YieldSourceStub.abi); await yieldSourceStub.mock.depositToken.returns(depositToken.address); + prizePool = await PrizePoolHarness.deploy(wallet1.address, yieldSourceStub.address); + ticket = await Ticket.deploy('Ticket', 'TICK', 18, prizePool.address); - const PrizePoolHarness = await getContractFactory('PrizePoolHarness', contractsOwner); - prizePool = await PrizePoolHarness.deploy(contractsOwner.address, yieldSourceStub.address); - - const Ticket = await getContractFactory('Ticket'); - ticket = await Ticket.deploy('name', 'SYMBOL', 18, prizePool.address); + await prizePool.setTicket(ticket.address); + await prizePool.setPrizeStrategy(prizeStrategyManager.address); }); + /*============================================ */ + // Constructor Functions --------------------- + /*============================================ */ describe('constructor()', () => { + beforeEach(async () => { + prizePool = await PrizePoolHarness.deploy(wallet1.address, yieldSourceStub.address); + ticket = await Ticket.deploy('Ticket', 'TICK', 18, prizePool.address); + }) it('should fire the events', async () => { const deployTx = prizePool.deployTransaction; @@ -107,35 +114,53 @@ describe('PrizePool', function () { .to.emit(prizePool, 'BalanceCapSet') .withArgs(MaxUint256); }); - }); - describe('with a mocked prize pool', () => { - beforeEach(async () => { - await prizePool.setPrizeStrategy(prizeStrategyManager.address); - await prizePool.setTicket(ticket.address); + it('should set all the vars', async () => { + expect(await prizePool.token()).to.equal(depositToken.address); }); - describe('constructor()', () => { - it('should set all the vars', async () => { - expect(await prizePool.token()).to.equal(depositToken.address); - }); + it('should reject invalid params', async () => { + const PrizePoolHarness = await getContractFactory('PrizePoolHarness', wallet1); + prizePool2 = await PrizePoolHarness.deploy(wallet1.address, yieldSourceStub.address); - it('should reject invalid params', async () => { - const PrizePoolHarness = await getContractFactory('PrizePoolHarness', contractsOwner); - prizePool2 = await PrizePoolHarness.deploy(contractsOwner.address, yieldSourceStub.address); + await expect(prizePool2.setTicket(AddressZero)).to.be.revertedWith( + 'PrizePool/ticket-not-zero-address', + ); + }); + }); - await expect(prizePool2.setTicket(AddressZero)).to.be.revertedWith( - 'PrizePool/ticket-not-zero-address', - ); + /*============================================ */ + // Core Functions ---------------------------- + /*============================================ */ + describe('Core Functions', () => { + describe('award()', () => { + it('should return early if amount is 0', async () => { + await prizePool.setPrizeStrategy(wallet1.address) + await expect(prizePool.award(wallet2.address, toWei('0'))) + .to.not.emit(prizePool, 'Awarded') }); - }); + + it('should fail if amount is GREATER THEN the current award balance', async () => { + await prizePool.setPrizeStrategy(wallet1.address) + await prizePool.setCurrentAwardBalance(toWei('1000')) + await expect(prizePool.award(wallet2.address, toWei('2000'))) + .to.be.revertedWith('PrizePool/award-exceeds-avail') + }); + it('should succeed to award tickets and emit Awarded', async () => { + await prizePool.setPrizeStrategy(wallet1.address) + await prizePool.setCurrentAwardBalance(toWei('2000')) + await expect(prizePool.award(wallet2.address, toWei('1000'))) + .to.emit(prizePool, 'Awarded') + .withArgs(wallet2.address, ticket.address, toWei('1000')) + }); + }) describe('depositTo()', () => { it('should revert when deposit exceeds liquidity cap', async () => { const amount = toWei('1'); const liquidityCap = toWei('1000'); - await depositTokenIntoPrizePool(contractsOwner.address, liquidityCap); + await depositTokenIntoPrizePool(wallet1.address, liquidityCap); await prizePool.setLiquidityCap(liquidityCap); @@ -149,9 +174,9 @@ describe('PrizePool', function () { const balanceCap = toWei('50000'); await prizePool.setBalanceCap(balanceCap); - await depositTokenIntoPrizePool(contractsOwner.address, balanceCap); + await depositTokenIntoPrizePool(wallet1.address, balanceCap); - await expect(depositTokenIntoPrizePool(contractsOwner.address, amount)).to.be.revertedWith( + await expect(depositTokenIntoPrizePool(wallet1.address, amount)).to.be.revertedWith( 'PrizePool/exceeds-balance-cap', ); }); @@ -159,7 +184,7 @@ describe('PrizePool', function () { describe('captureAwardBalance()', () => { it('should handle when the balance is less than the collateral', async () => { - await depositTokenIntoPrizePool(contractsOwner.address, toWei('100')); + await depositTokenIntoPrizePool(wallet1.address, toWei('100')); await yieldSourceStub.mock.balanceOfToken .withArgs(prizePool.address) @@ -169,7 +194,7 @@ describe('PrizePool', function () { }); it('should handle the situ when the total accrued interest is less than the captured total', async () => { - await depositTokenIntoPrizePool(contractsOwner.address, toWei('100')); + await depositTokenIntoPrizePool(wallet1.address, toWei('100')); await yieldSourceStub.mock.balanceOfToken.withArgs(prizePool.address).returns(toWei('110')); @@ -185,7 +210,7 @@ describe('PrizePool', function () { }); it('should track the yield less the total token supply', async () => { - await depositTokenIntoPrizePool(contractsOwner.address, toWei('100')); + await depositTokenIntoPrizePool(wallet1.address, toWei('100')); await yieldSourceStub.mock.balanceOfToken.withArgs(prizePool.address).returns(toWei('110')); @@ -200,26 +225,114 @@ describe('PrizePool', function () { it('should allow a user to withdraw instantly', async () => { let amount = toWei('10'); - await depositTokenIntoPrizePool(contractsOwner.address, amount); + await depositTokenIntoPrizePool(wallet1.address, amount); await yieldSourceStub.mock.redeemToken.withArgs(amount).returns(amount); - await expect(prizePool.withdrawFrom(contractsOwner.address, amount)) + await expect(prizePool.withdrawFrom(wallet1.address, amount)) .to.emit(prizePool, 'Withdrawal') - .withArgs(contractsOwner.address, contractsOwner.address, ticket.address, amount, amount); + .withArgs(wallet1.address, wallet1.address, ticket.address, amount, amount); }); }); + }) + + /*============================================ */ + // Getter Functions -------------------------- + /*============================================ */ + describe('Getter Functions', () => { + it('should getAccountedBalance()', async () => { + expect(await prizePool.accountedBalance()) + .to.equal(0); + expect(await prizePool.getAccountedBalance()) + .to.equal(0); + }); + it('should getBalanceCap()', async () => { + expect(await prizePool.getBalanceCap()) + .to.equal(constants.MaxUint256); + }); + it('should getLiquidityCap()', async () => { + expect(await prizePool.getLiquidityCap()) + .to.equal(constants.MaxUint256); + }); + it('should getTicket()', async () => { + expect(await prizePool.getTicket()) + .to.equal(ticket.address); + }); + it('should getPrizeStrategy()', async () => { + expect(await prizePool.getPrizeStrategy()) + .to.equal(prizeStrategyManager.address); + }); + it('should canAwardExternal()', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(false); + expect(await prizePool.canAwardExternal(erc20Token.address)) + .to.equal(false); + await yieldSourceStub.mock.canAwardExternal.withArgs(ticket.address).returns(true); + expect(await prizePool.canAwardExternal(ticket.address)) + .to.equal(true); + }); describe('balance()', () => { it('should return zero if no deposits have been made', async () => { const balance = toWei('11'); - await yieldSourceStub.mock.balanceOfToken.withArgs(prizePool.address).returns(balance); - expect((await call(prizePool, 'balance')).toString()).to.equal(balance); }); }); + describe('compLikeDelegate()', () => { + it('should fail to delegate tokens', async () => { + await compLike.mock.balanceOf.withArgs(prizePool.address).returns(0) + expect(await prizePool.compLikeDelegate(compLike.address, wallet4.address)) + }); + + it('should succeed to delegate tokens', async () => { + await compLike.mock.balanceOf.withArgs(prizePool.address).returns(100) + await compLike.mock.delegate.withArgs(wallet4.address).returns() + expect(await prizePool.compLikeDelegate(compLike.address, wallet4.address)) + }); + }); + + describe('isControlled()', () => { + it('should validate TRUE with ticket variable', async () => { + expect(await prizePool.isControlled(await prizePool.getTicket())) + .to.equal(true); + }); + + it('should validate FALSE with non-ticket variable', async () => { + expect(await prizePool.isControlled(AddressZero)) + .to.equal(false); + }); + }); + + + }) + + /*============================================ */ + // Setter Functions -------------------------- + /*============================================ */ + describe('Setter Functions', () => { + + beforeEach(async () => { + prizePool = await PrizePoolHarness.deploy(wallet1.address, yieldSourceStub.address); + ticket = await Ticket.deploy('Ticket', 'TICK', 18, prizePool.address); + }) + + describe('setTicket()', () => { + it('should allow the owner to swap the prize strategy', async () => { + await expect(prizePool.setTicket(wallet4.address)) + .to.emit(prizePool, 'TicketSet') + .withArgs(wallet4.address); + + expect(await prizePool.getTicket()).to.equal(wallet4.address); + }); + + it('should not allow anyone else to change the prize strategy', async () => { + await expect( + prizePool.connect(wallet2 as Signer).setTicket(wallet4.address), + ).to.be.revertedWith('Ownable/caller-not-owner'); + }); + }); + describe('setPrizeStrategy()', () => { it('should allow the owner to swap the prize strategy', async () => { const randomWallet = Wallet.createRandom(); @@ -277,174 +390,172 @@ describe('PrizePool', function () { ); }); }); + }) + + /*============================================ */ + // Token Functions --------------------------- + /*============================================ */ + describe('Token Functions', () => { + describe('awardExternalERC20()', () => { + beforeEach(async () => { + await prizePool.setPrizeStrategy(prizeStrategyManager.address); + }); - describe('compLikeDelegate()', () => { - it('should delegate votes', async () => { - await compLike.mock.balanceOf.withArgs(prizePool.address).returns('1'); - await compLike.mock.delegate.withArgs(wallet2.address).revertsWithReason('hello'); + it('should exit early when amount = 0', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); await expect( - prizePool.compLikeDelegate(compLike.address, wallet2.address), - ).to.be.revertedWith('hello'); + prizePool + .connect(prizeStrategyManager) + .awardExternalERC20(wallet1.address, erc20Token.address, 0), + ).to.not.emit(prizePool, 'AwardedExternalERC20'); }); - it('should only allow the owner to delegate', async () => { - await expect( - prizePool.connect(wallet2 as Signer).compLikeDelegate(compLike.address, wallet2.address), - ).to.be.revertedWith('Ownable/caller-not-owner'); - }); + it('should only allow the prizeStrategy to award external ERC20s', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); - it('should not delegate if the balance is zero', async () => { - await compLike.mock.balanceOf.withArgs(prizePool.address).returns('0'); - await prizePool.compLikeDelegate(compLike.address, wallet2.address); - }); - }); - }); + let prizePool2 = prizePool.connect(wallet2 as Signer); - describe('awardExternalERC20()', () => { - beforeEach(async () => { - await prizePool.setPrizeStrategy(prizeStrategyManager.address); - }); + await expect( + prizePool2.awardExternalERC20(wallet1.address, wallet2.address, toWei('10')), + ).to.be.revertedWith('PrizePool/only-prizeStrategy'); + }); - it('should exit early when amount = 0', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + it('should allow arbitrary tokens to be transferred', async () => { + const amount = toWei('10'); - await expect( - prizePool - .connect(prizeStrategyManager) - .awardExternalERC20(contractsOwner.address, erc20Token.address, 0), - ).to.not.emit(prizePool, 'AwardedExternalERC20'); - }); + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); - it('should only allow the prizeStrategy to award external ERC20s', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + await depositTokenIntoPrizePool(wallet1.address, amount, erc20Token); - let prizePool2 = prizePool.connect(wallet2 as Signer); - - await expect( - prizePool2.awardExternalERC20(contractsOwner.address, wallet2.address, toWei('10')), - ).to.be.revertedWith('PrizePool/only-prizeStrategy'); + await expect( + prizePool + .connect(prizeStrategyManager) + .awardExternalERC20(wallet1.address, erc20Token.address, amount), + ) + .to.emit(prizePool, 'AwardedExternalERC20') + .withArgs(wallet1.address, erc20Token.address, amount); + }); }); - it('should allow arbitrary tokens to be transferred', async () => { - const amount = toWei('10'); + describe('transferExternalERC20()', () => { + beforeEach(async () => { + await prizePool.setPrizeStrategy(prizeStrategyManager.address); + }); - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + it('should exit early when amount = 0', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); - await depositTokenIntoPrizePool(contractsOwner.address, amount, erc20Token); + await expect( + prizePool + .connect(prizeStrategyManager) + .transferExternalERC20(wallet1.address, erc20Token.address, 0), + ).to.not.emit(prizePool, 'TransferredExternalERC20'); + }); - await expect( - prizePool - .connect(prizeStrategyManager) - .awardExternalERC20(contractsOwner.address, erc20Token.address, amount), - ) - .to.emit(prizePool, 'AwardedExternalERC20') - .withArgs(contractsOwner.address, erc20Token.address, amount); - }); - }); + it('should only allow the prizeStrategy to award external ERC20s', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); - describe('transferExternalERC20()', () => { - beforeEach(async () => { - await prizePool.setPrizeStrategy(prizeStrategyManager.address); - }); + let prizePool2 = prizePool.connect(wallet2 as Signer); - it('should exit early when amount = 0', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + await expect( + prizePool2.transferExternalERC20(wallet1.address, wallet2.address, toWei('10')), + ).to.be.revertedWith('PrizePool/only-prizeStrategy'); + }); - await expect( - prizePool - .connect(prizeStrategyManager) - .transferExternalERC20(contractsOwner.address, erc20Token.address, 0), - ).to.not.emit(prizePool, 'TransferredExternalERC20'); - }); + it('should allow arbitrary tokens to be transferred', async () => { + const amount = toWei('10'); - it('should only allow the prizeStrategy to award external ERC20s', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + await depositTokenIntoPrizePool(wallet1.address, amount, erc20Token); - let prizePool2 = prizePool.connect(wallet2 as Signer); + await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); - await expect( - prizePool2.transferExternalERC20(contractsOwner.address, wallet2.address, toWei('10')), - ).to.be.revertedWith('PrizePool/only-prizeStrategy'); + await expect( + prizePool + .connect(prizeStrategyManager) + .transferExternalERC20(wallet1.address, erc20Token.address, amount), + ) + .to.emit(prizePool, 'TransferredExternalERC20') + .withArgs(wallet1.address, erc20Token.address, amount); + }); }); - it('should allow arbitrary tokens to be transferred', async () => { - const amount = toWei('10'); - - await depositTokenIntoPrizePool(contractsOwner.address, amount, erc20Token); + describe('awardExternalERC721()', () => { + beforeEach(async () => { + await prizePool.setPrizeStrategy(prizeStrategyManager.address); + }); - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); + it('should exit early when tokenIds list is empty', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); - await expect( - prizePool - .connect(prizeStrategyManager) - .transferExternalERC20(contractsOwner.address, erc20Token.address, amount), - ) - .to.emit(prizePool, 'TransferredExternalERC20') - .withArgs(contractsOwner.address, erc20Token.address, amount); - }); - }); - - describe('awardExternalERC721()', () => { - beforeEach(async () => { - await prizePool.setPrizeStrategy(prizeStrategyManager.address); - }); + await expect( + prizePool + .connect(prizeStrategyManager) + .awardExternalERC721(wallet1.address, erc721Token.address, []), + ).to.not.emit(prizePool, 'AwardedExternalERC721'); + }); - it('should exit early when tokenIds list is empty', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); + it('should only allow the prizeStrategy to award external ERC721s', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); - await expect( - prizePool - .connect(prizeStrategyManager) - .awardExternalERC721(contractsOwner.address, erc721Token.address, []), - ).to.not.emit(prizePool, 'AwardedExternalERC721'); - }); + let prizePool2 = prizePool.connect(wallet2 as Signer); - it('should only allow the prizeStrategy to award external ERC721s', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); - - let prizePool2 = prizePool.connect(wallet2 as Signer); + await expect( + prizePool2.awardExternalERC721(wallet1.address, erc721Token.address, [NFT_TOKEN_ID]), + ).to.be.revertedWith('PrizePool/only-prizeStrategy'); + }); - await expect( - prizePool2.awardExternalERC721(contractsOwner.address, erc721Token.address, [NFT_TOKEN_ID]), - ).to.be.revertedWith('PrizePool/only-prizeStrategy'); - }); + it('should allow arbitrary tokens to be transferred', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); - it('should allow arbitrary tokens to be transferred', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); + await depositNftIntoPrizePool(wallet1.address); - await depositNftIntoPrizePool(contractsOwner.address); + await expect( + prizePool + .connect(prizeStrategyManager) + .awardExternalERC721(wallet1.address, erc721Token.address, [NFT_TOKEN_ID]), + ) + .to.emit(prizePool, 'AwardedExternalERC721') + .withArgs(wallet1.address, erc721Token.address, [NFT_TOKEN_ID]); + }); - await expect( - prizePool - .connect(prizeStrategyManager) - .awardExternalERC721(contractsOwner.address, erc721Token.address, [NFT_TOKEN_ID]), - ) - .to.emit(prizePool, 'AwardedExternalERC721') - .withArgs(contractsOwner.address, erc721Token.address, [NFT_TOKEN_ID]); - }); + it('should not DoS with faulty ERC721s', async () => { + await yieldSourceStub.mock.canAwardExternal.withArgs(erc721tokenMock.address).returns(true); + await erc721tokenMock.mock.transferFrom + .withArgs(prizePool.address, wallet1.address, NFT_TOKEN_ID) + .reverts(); - it('should not DoS with faulty ERC721s', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc721tokenMock.address).returns(true); - await erc721tokenMock.mock.transferFrom - .withArgs(prizePool.address, contractsOwner.address, NFT_TOKEN_ID) - .reverts(); - - await expect( - prizePool - .connect(prizeStrategyManager) - .awardExternalERC721(contractsOwner.address, erc721tokenMock.address, [NFT_TOKEN_ID]), - ).to.emit(prizePool, 'ErrorAwardingExternalERC721'); + await expect( + prizePool + .connect(prizeStrategyManager) + .awardExternalERC721(wallet1.address, erc721tokenMock.address, [NFT_TOKEN_ID]), + ).to.emit(prizePool, 'ErrorAwardingExternalERC721'); + }); }); - }); - - describe('onERC721Received()', () => { - it('should receive an ERC721 token when using safeTransferFrom', async () => { - expect(await erc721Token.balanceOf(prizePool.address)).to.equal('0'); - await depositNftIntoPrizePool(contractsOwner.address); + describe('onERC721Received()', () => { + it('should return the inteface selector', async () => { + expect(await prizePool.mockOnERC721Received('0x150b7a02')) + .to.equal('0x150b7a02'); + }); - expect(await erc721Token.balanceOf(prizePool.address)).to.equal('1'); + it('should receive an ERC721 token when using safeTransferFrom', async () => { + expect(await erc721Token.balanceOf(prizePool.address)).to.equal('0'); + await depositNftIntoPrizePool(wallet1.address); + expect(await erc721Token.balanceOf(prizePool.address)).to.equal('1'); + }); }); - }); -}); + }) + + /*============================================ */ + // Internal Functions ------------------------ + /*============================================ */ + describe('Internal Functions', () => { + it('should get the curren block.timestamp', async () => { + const timenow = (await ethers.provider.getBlock('latest')).timestamp + expect(await prizePool.internalCurrentTime()) + .to.equal(timenow); + }) + }) + +}) \ No newline at end of file From 92a1233f927cef26dfb90c8b68626e3781cb2b82 Mon Sep 17 00:00:00 2001 From: kamescg Date: Tue, 28 Sep 2021 08:18:47 -0400 Subject: [PATCH 2/5] remove only for tests --- test/prize-pool/PrizePool.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/prize-pool/PrizePool.test.ts b/test/prize-pool/PrizePool.test.ts index 51357dc2..21dbf0b9 100644 --- a/test/prize-pool/PrizePool.test.ts +++ b/test/prize-pool/PrizePool.test.ts @@ -12,7 +12,7 @@ const { parseEther: toWei } = utils; const debug = require('debug')('ptv3:PrizePool.test'); const NFT_TOKEN_ID = 1; -describe.only('PrizePool', function () { +describe('PrizePool', function () { let wallet1: SignerWithAddress; let wallet2: SignerWithAddress; let prizeStrategyManager: SignerWithAddress; From 8dee43141082f9006bd55cf3405876ad5a326401 Mon Sep 17 00:00:00 2001 From: kamescg Date: Tue, 28 Sep 2021 08:43:21 -0400 Subject: [PATCH 3/5] rm old file --- test/prize-pool/PrizePool.ordered.test.ts | 450 ---------------------- 1 file changed, 450 deletions(-) delete mode 100644 test/prize-pool/PrizePool.ordered.test.ts diff --git a/test/prize-pool/PrizePool.ordered.test.ts b/test/prize-pool/PrizePool.ordered.test.ts deleted file mode 100644 index ed7a0c55..00000000 --- a/test/prize-pool/PrizePool.ordered.test.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { Signer } from '@ethersproject/abstract-signer'; -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { expect } from 'chai'; -import { deployMockContract, MockContract } from 'ethereum-waffle'; -import { BigNumber, constants, Contract, ContractFactory, utils } from 'ethers'; -import { ethers, artifacts } from 'hardhat'; -import { Artifact } from 'hardhat/types'; - -import { call } from '../helpers/call'; - -const { AddressZero, MaxUint256 } = constants; -const { getContractFactory, getSigners, Wallet } = ethers; -const { parseEther: toWei } = utils; - -const debug = require('debug')('ptv3:PrizePool.test'); - -const NFT_TOKEN_ID = 1; - -describe('PrizePool', function () { - let contractsOwner: SignerWithAddress; - let wallet2: SignerWithAddress; - let prizeStrategyManager: SignerWithAddress; - let ERC20MintableContract: ContractFactory - let ICompLike: Artifact - let ERC721MintableContract: ContractFactory - let PrizePoolHarness: ContractFactory - let Ticket: ContractFactory - let IERC721: Artifact - let YieldSourceStub: Artifact - // Set as `any` cause types are conflicting between the different path for ethers - let prizePool: any; - let prizePool2: any; - let depositToken: Contract; - let erc20Token: Contract; - let erc721Token: Contract; - let ticket: Contract; - let compLike: MockContract; - let erc721tokenMock: MockContract; - let yieldSourceStub: MockContract; - - const depositTokenIntoPrizePool = async ( - walletAddress: string, - amount: BigNumber, - token: Contract = depositToken, - pool: Contract = prizePool, - ) => { - await yieldSourceStub.mock.supplyTokenTo.withArgs(amount, pool.address).returns(); - - await token.approve(pool.address, amount); - await token.mint(walletAddress, amount); - - if (token.address === depositToken.address) { - return await pool.depositTo(walletAddress, amount); - } else { - return await token.transfer(pool.address, amount); - } - }; - - const depositNftIntoPrizePool = async (walletAddress: string) => { - await erc721Token.mint(walletAddress, NFT_TOKEN_ID); - await erc721Token.transferFrom(walletAddress, prizePool.address, NFT_TOKEN_ID); - }; - - before(async () => { - [contractsOwner, wallet2, prizeStrategyManager] = await getSigners(); - debug(`using wallet ${contractsOwner.address}`); - ERC20MintableContract = await getContractFactory('ERC20Mintable', contractsOwner); - ICompLike = await artifacts.readArtifact('ICompLike'); - ERC721MintableContract = await getContractFactory('ERC721Mintable', contractsOwner); - PrizePoolHarness = await getContractFactory('PrizePoolHarness', contractsOwner); - Ticket = await getContractFactory('Ticket'); - IERC721 = await artifacts.readArtifact('IERC721'); - YieldSourceStub = await artifacts.readArtifact('YieldSourceStub'); - compLike = await deployMockContract(contractsOwner as Signer, ICompLike.abi); - }) - - beforeEach(async () => { - debug('mocking tokens...'); - depositToken = await ERC20MintableContract.deploy('Token', 'TOKE'); - erc20Token = await ERC20MintableContract.deploy('Token', 'TOKE'); - erc721Token = await ERC721MintableContract.deploy(); - erc721tokenMock = await deployMockContract(contractsOwner as Signer, IERC721.abi); - yieldSourceStub = await deployMockContract(contractsOwner as Signer, YieldSourceStub.abi); - await yieldSourceStub.mock.depositToken.returns(depositToken.address); - prizePool = await PrizePoolHarness.deploy(contractsOwner.address, yieldSourceStub.address); - ticket = await Ticket.deploy('Ticket', 'TICK', 18, prizePool.address); - - await prizePool.setTicket(ticket.address); - await prizePool.setPrizeStrategy(prizeStrategyManager.address); - }); - - /*============================================ */ - // Constructor Functions --------------------- - /*============================================ */ - describe('constructor()', () => { - it('should fire the events', async () => { - const deployTx = prizePool.deployTransaction; - - await expect(deployTx).to.emit(prizePool, 'LiquidityCapSet').withArgs(MaxUint256); - - await expect(prizePool.setPrizeStrategy(prizeStrategyManager.address)) - .to.emit(prizePool, 'PrizeStrategySet') - .withArgs(prizeStrategyManager.address); - - const setTicketTx = await prizePool.setTicket(ticket.address); - - await expect(setTicketTx) - .to.emit(prizePool, 'TicketSet') - .withArgs(ticket.address); - - await expect(setTicketTx) - .to.emit(prizePool, 'BalanceCapSet') - .withArgs(MaxUint256); - }); - - it('should set all the vars', async () => { - expect(await prizePool.token()).to.equal(depositToken.address); - }); - - it('should reject invalid params', async () => { - const PrizePoolHarness = await getContractFactory('PrizePoolHarness', contractsOwner); - prizePool2 = await PrizePoolHarness.deploy(contractsOwner.address, yieldSourceStub.address); - - await expect(prizePool2.setTicket(AddressZero)).to.be.revertedWith( - 'PrizePool/ticket-not-zero-address', - ); - }); - }); - - /*============================================ */ - // Core Functions ---------------------------- - /*============================================ */ - describe('Core Functions', () => { - describe('depositTo()', () => { - it('should revert when deposit exceeds liquidity cap', async () => { - const amount = toWei('1'); - const liquidityCap = toWei('1000'); - - await depositTokenIntoPrizePool(contractsOwner.address, liquidityCap); - - await prizePool.setLiquidityCap(liquidityCap); - - await expect( - prizePool.depositTo(wallet2.address, amount), - ).to.be.revertedWith('PrizePool/exceeds-liquidity-cap'); - }); - - it('should revert when user deposit exceeds ticket balance cap', async () => { - const amount = toWei('1'); - const balanceCap = toWei('50000'); - - await prizePool.setBalanceCap(balanceCap); - await depositTokenIntoPrizePool(contractsOwner.address, balanceCap); - - await expect(depositTokenIntoPrizePool(contractsOwner.address, amount)).to.be.revertedWith( - 'PrizePool/exceeds-balance-cap', - ); - }); - }); - - describe('captureAwardBalance()', () => { - it('should handle when the balance is less than the collateral', async () => { - await depositTokenIntoPrizePool(contractsOwner.address, toWei('100')); - - await yieldSourceStub.mock.balanceOfToken - .withArgs(prizePool.address) - .returns(toWei('99.9999')); - - expect(await prizePool.awardBalance()).to.equal(toWei('0')); - }); - - it('should handle the situ when the total accrued interest is less than the captured total', async () => { - await depositTokenIntoPrizePool(contractsOwner.address, toWei('100')); - - await yieldSourceStub.mock.balanceOfToken.withArgs(prizePool.address).returns(toWei('110')); - - // first capture the 10 tokens - await prizePool.captureAwardBalance(); - - await yieldSourceStub.mock.balanceOfToken - .withArgs(prizePool.address) - .returns(toWei('109.999')); - - // now try to capture again - await expect(prizePool.captureAwardBalance()).to.not.emit(prizePool, 'AwardCaptured'); - }); - - it('should track the yield less the total token supply', async () => { - await depositTokenIntoPrizePool(contractsOwner.address, toWei('100')); - - await yieldSourceStub.mock.balanceOfToken.withArgs(prizePool.address).returns(toWei('110')); - - await expect(prizePool.captureAwardBalance()) - .to.emit(prizePool, 'AwardCaptured') - .withArgs(toWei('10')); - expect(await prizePool.awardBalance()).to.equal(toWei('10')); - }); - }); - - describe('withdrawFrom()', () => { - it('should allow a user to withdraw instantly', async () => { - let amount = toWei('10'); - - await depositTokenIntoPrizePool(contractsOwner.address, amount); - - await yieldSourceStub.mock.redeemToken.withArgs(amount).returns(amount); - - await expect(prizePool.withdrawFrom(contractsOwner.address, amount)) - .to.emit(prizePool, 'Withdrawal') - .withArgs(contractsOwner.address, contractsOwner.address, ticket.address, amount, amount); - }); - }); - }) - - /*============================================ */ - // Getter Functions -------------------------- - /*============================================ */ - describe('Getter Functions', () => { - describe('balance()', () => { - it('should return zero if no deposits have been made', async () => { - const balance = toWei('11'); - await yieldSourceStub.mock.balanceOfToken.withArgs(prizePool.address).returns(balance); - expect((await call(prizePool, 'balance')).toString()).to.equal(balance); - }); - }); - }) - - /*============================================ */ - // Setter Functions -------------------------- - /*============================================ */ - describe('Setter Functions', () => { - describe('setPrizeStrategy()', () => { - it('should allow the owner to swap the prize strategy', async () => { - const randomWallet = Wallet.createRandom(); - - await expect(prizePool.setPrizeStrategy(randomWallet.address)) - .to.emit(prizePool, 'PrizeStrategySet') - .withArgs(randomWallet.address); - - expect(await prizePool.prizeStrategy()).to.equal(randomWallet.address); - }); - - it('should not allow anyone else to change the prize strategy', async () => { - await expect( - prizePool.connect(wallet2 as Signer).setPrizeStrategy(wallet2.address), - ).to.be.revertedWith('Ownable/caller-not-owner'); - }); - }); - - describe('setBalanceCap', () => { - it('should allow the owner to set the balance cap', async () => { - const balanceCap = toWei('50000'); - - await expect(prizePool.setBalanceCap(balanceCap)) - .to.emit(prizePool, 'BalanceCapSet') - .withArgs(balanceCap); - - expect(await prizePool.balanceCap()).to.equal(balanceCap); - }); - - it('should not allow anyone else to call', async () => { - prizePool2 = prizePool.connect(wallet2 as Signer); - - await expect(prizePool2.setBalanceCap(toWei('50000'))).to.be.revertedWith( - 'Ownable/caller-not-owner', - ); - }); - }); - - describe('setLiquidityCap', () => { - it('should allow the owner to set the liquidity cap', async () => { - const liquidityCap = toWei('1000'); - - await expect(prizePool.setLiquidityCap(liquidityCap)) - .to.emit(prizePool, 'LiquidityCapSet') - .withArgs(liquidityCap); - - expect(await prizePool.liquidityCap()).to.equal(liquidityCap); - }); - - it('should not allow anyone else to call', async () => { - prizePool2 = prizePool.connect(wallet2 as Signer); - - await expect(prizePool2.setLiquidityCap(toWei('1000'))).to.be.revertedWith( - 'Ownable/caller-not-owner', - ); - }); - }); - }) - - /*============================================ */ - // Token Functions --------------------------- - /*============================================ */ - describe('Token Functions', () => { - describe('awardExternalERC20()', () => { - beforeEach(async () => { - await prizePool.setPrizeStrategy(prizeStrategyManager.address); - }); - - it('should exit early when amount = 0', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); - - await expect( - prizePool - .connect(prizeStrategyManager) - .awardExternalERC20(contractsOwner.address, erc20Token.address, 0), - ).to.not.emit(prizePool, 'AwardedExternalERC20'); - }); - - it('should only allow the prizeStrategy to award external ERC20s', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); - - let prizePool2 = prizePool.connect(wallet2 as Signer); - - await expect( - prizePool2.awardExternalERC20(contractsOwner.address, wallet2.address, toWei('10')), - ).to.be.revertedWith('PrizePool/only-prizeStrategy'); - }); - - it('should allow arbitrary tokens to be transferred', async () => { - const amount = toWei('10'); - - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); - - await depositTokenIntoPrizePool(contractsOwner.address, amount, erc20Token); - - await expect( - prizePool - .connect(prizeStrategyManager) - .awardExternalERC20(contractsOwner.address, erc20Token.address, amount), - ) - .to.emit(prizePool, 'AwardedExternalERC20') - .withArgs(contractsOwner.address, erc20Token.address, amount); - }); - }); - - describe('transferExternalERC20()', () => { - beforeEach(async () => { - await prizePool.setPrizeStrategy(prizeStrategyManager.address); - }); - - it('should exit early when amount = 0', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); - - await expect( - prizePool - .connect(prizeStrategyManager) - .transferExternalERC20(contractsOwner.address, erc20Token.address, 0), - ).to.not.emit(prizePool, 'TransferredExternalERC20'); - }); - - it('should only allow the prizeStrategy to award external ERC20s', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); - - let prizePool2 = prizePool.connect(wallet2 as Signer); - - await expect( - prizePool2.transferExternalERC20(contractsOwner.address, wallet2.address, toWei('10')), - ).to.be.revertedWith('PrizePool/only-prizeStrategy'); - }); - - it('should allow arbitrary tokens to be transferred', async () => { - const amount = toWei('10'); - - await depositTokenIntoPrizePool(contractsOwner.address, amount, erc20Token); - - await yieldSourceStub.mock.canAwardExternal.withArgs(erc20Token.address).returns(true); - - await expect( - prizePool - .connect(prizeStrategyManager) - .transferExternalERC20(contractsOwner.address, erc20Token.address, amount), - ) - .to.emit(prizePool, 'TransferredExternalERC20') - .withArgs(contractsOwner.address, erc20Token.address, amount); - }); - }); - - describe('awardExternalERC721()', () => { - beforeEach(async () => { - await prizePool.setPrizeStrategy(prizeStrategyManager.address); - }); - - it('should exit early when tokenIds list is empty', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); - - await expect( - prizePool - .connect(prizeStrategyManager) - .awardExternalERC721(contractsOwner.address, erc721Token.address, []), - ).to.not.emit(prizePool, 'AwardedExternalERC721'); - }); - - it('should only allow the prizeStrategy to award external ERC721s', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); - - let prizePool2 = prizePool.connect(wallet2 as Signer); - - await expect( - prizePool2.awardExternalERC721(contractsOwner.address, erc721Token.address, [NFT_TOKEN_ID]), - ).to.be.revertedWith('PrizePool/only-prizeStrategy'); - }); - - it('should allow arbitrary tokens to be transferred', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc721Token.address).returns(true); - - await depositNftIntoPrizePool(contractsOwner.address); - - await expect( - prizePool - .connect(prizeStrategyManager) - .awardExternalERC721(contractsOwner.address, erc721Token.address, [NFT_TOKEN_ID]), - ) - .to.emit(prizePool, 'AwardedExternalERC721') - .withArgs(contractsOwner.address, erc721Token.address, [NFT_TOKEN_ID]); - }); - - it('should not DoS with faulty ERC721s', async () => { - await yieldSourceStub.mock.canAwardExternal.withArgs(erc721tokenMock.address).returns(true); - await erc721tokenMock.mock.transferFrom - .withArgs(prizePool.address, contractsOwner.address, NFT_TOKEN_ID) - .reverts(); - - await expect( - prizePool - .connect(prizeStrategyManager) - .awardExternalERC721(contractsOwner.address, erc721tokenMock.address, [NFT_TOKEN_ID]), - ).to.emit(prizePool, 'ErrorAwardingExternalERC721'); - }); - }); - - describe('onERC721Received()', () => { - it('should receive an ERC721 token when using safeTransferFrom', async () => { - expect(await erc721Token.balanceOf(prizePool.address)).to.equal('0'); - - await depositNftIntoPrizePool(contractsOwner.address); - - expect(await erc721Token.balanceOf(prizePool.address)).to.equal('1'); - }); - }); - }) - - /*============================================ */ - // Internal Functions ------------------------ - /*============================================ */ - describe('Internal Functions', () => { - - }) - -}) \ No newline at end of file From 4575b3f36558cd3c3eac67e60a19baa21172e81a Mon Sep 17 00:00:00 2001 From: kamescg Date: Wed, 29 Sep 2021 19:39:52 -0400 Subject: [PATCH 4/5] fix(PrizePool): unit tests --- contracts/interfaces/IPrizePool.sol | 5 +---- contracts/prize-pool/PrizePool.sol | 10 +++++----- contracts/test/PrizePoolHarness.sol | 6 +++--- test/prize-pool/PrizePool.test.ts | 4 +--- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/contracts/interfaces/IPrizePool.sol b/contracts/interfaces/IPrizePool.sol index bc0d0680..0cbe1fae 100644 --- a/contracts/interfaces/IPrizePool.sol +++ b/contracts/interfaces/IPrizePool.sol @@ -126,6 +126,7 @@ interface IPrizePool { /** * @notice Read internal Ticket accounted balance. + * @return uint256 accountBalance */ function getAccountedBalance() external view returns (uint256); /** @@ -200,10 +201,6 @@ interface IPrizePool { /// @return The address of the asset function token() external view returns (address); - /// @notice The total of all controlled tokens - /// @return The current total of all tokens - function accountedBalance() external view returns (uint256); - /// @notice Delegate the votes for a Compound COMP-like token held by the prize pool /// @param _compLike The COMP-like token held by the prize pool that should be delegated /// @param _to The address to delegate to diff --git a/contracts/prize-pool/PrizePool.sol b/contracts/prize-pool/PrizePool.sol index 2fd8dc14..c3a61f80 100644 --- a/contracts/prize-pool/PrizePool.sol +++ b/contracts/prize-pool/PrizePool.sol @@ -78,10 +78,10 @@ abstract contract PrizePool is IPrizePool, Ownable, ReentrancyGuard, IERC721Rece function awardBalance() external override view returns (uint256) { return _currentAwardBalance; } - /// @inheritdoc IPrizePool - function accountedBalance() external override view returns (uint256) { - return _ticketTotalSupply(); - } + // /// @inheritdoc IPrizePool + // function accountedBalance() external override view returns (uint256) { + // return _ticketTotalSupply(); + // } /// @inheritdoc IPrizePool function canAwardExternal(address _externalToken) external override view returns (bool) { return _canAwardExternal(_externalToken); @@ -278,7 +278,7 @@ abstract contract PrizePool is IPrizePool, Ownable, ReentrancyGuard, IERC721Rece } /// @inheritdoc IERC721Receiver - function onERC721Received(address,address,uint256,bytes calldata) public pure override returns (bytes4) { + function onERC721Received(address,address,uint256,bytes calldata) external pure override returns (bytes4) { return IERC721Receiver.onERC721Received.selector; } diff --git a/contracts/test/PrizePoolHarness.sol b/contracts/test/PrizePoolHarness.sol index f95c0c5d..43391f49 100644 --- a/contracts/test/PrizePoolHarness.sol +++ b/contracts/test/PrizePoolHarness.sol @@ -60,9 +60,9 @@ contract PrizePoolHarness is PrizePool { return stubYieldSource.redeemToken(redeemAmount); } - function mockOnERC721Received(bytes calldata sig) external pure returns (bytes4) { - return super.onERC721Received(address(0), address(0), 0, sig); - } + // function mockOnERC721Received(bytes calldata sig) external pure returns (bytes4) { + // return super.onERC721Received(address(0), address(0), 0, sig); + // } function setCurrentAwardBalance(uint256 amount) external { _currentAwardBalance = amount; diff --git a/test/prize-pool/PrizePool.test.ts b/test/prize-pool/PrizePool.test.ts index 21dbf0b9..32fa1545 100644 --- a/test/prize-pool/PrizePool.test.ts +++ b/test/prize-pool/PrizePool.test.ts @@ -241,8 +241,6 @@ describe('PrizePool', function () { /*============================================ */ describe('Getter Functions', () => { it('should getAccountedBalance()', async () => { - expect(await prizePool.accountedBalance()) - .to.equal(0); expect(await prizePool.getAccountedBalance()) .to.equal(0); }); @@ -535,7 +533,7 @@ describe('PrizePool', function () { describe('onERC721Received()', () => { it('should return the inteface selector', async () => { - expect(await prizePool.mockOnERC721Received('0x150b7a02')) + expect(await prizePool.onERC721Received(prizePool.address, constants.AddressZero, 0, '0x150b7a02')) .to.equal('0x150b7a02'); }); From 51282972cf09a2ed2a0312323a4202a83d6f3f3e Mon Sep 17 00:00:00 2001 From: Kames Geraghty <3408362+kamescg@users.noreply.github.com> Date: Thu, 30 Sep 2021 11:35:17 -0400 Subject: [PATCH 5/5] Update test/prize-pool/PrizePool.test.ts Co-authored-by: Pierrick Turelier --- test/prize-pool/PrizePool.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/prize-pool/PrizePool.test.ts b/test/prize-pool/PrizePool.test.ts index 32fa1545..ba33cbc8 100644 --- a/test/prize-pool/PrizePool.test.ts +++ b/test/prize-pool/PrizePool.test.ts @@ -549,7 +549,7 @@ describe('PrizePool', function () { // Internal Functions ------------------------ /*============================================ */ describe('Internal Functions', () => { - it('should get the curren block.timestamp', async () => { + it('should get the current block.timestamp', async () => { const timenow = (await ethers.provider.getBlock('latest')).timestamp expect(await prizePool.internalCurrentTime()) .to.equal(timenow);