From a7d711e40c86b9bd7069e88a5af5a716ecd42b65 Mon Sep 17 00:00:00 2001 From: Rob Secord Date: Mon, 22 Jun 2020 18:41:59 -0400 Subject: [PATCH] Add unit-tests for redeemTicketsInstantly --- .../periodic-prize-pool/PeriodicPrizePool.sol | 40 +++++++++----- .../test/CompoundPeriodicPrizePoolHarness.sol | 9 ++- test/Integration.test.js | 21 +++---- test/PeriodicPrizePool.test.js | 55 ++++++++++++++++++- test/features/support/PoolEnv.js | 5 +- 5 files changed, 102 insertions(+), 28 deletions(-) diff --git a/contracts/periodic-prize-pool/PeriodicPrizePool.sol b/contracts/periodic-prize-pool/PeriodicPrizePool.sol index 5b284866..1040d0b3 100644 --- a/contracts/periodic-prize-pool/PeriodicPrizePool.sol +++ b/contracts/periodic-prize-pool/PeriodicPrizePool.sol @@ -323,11 +323,11 @@ abstract contract PeriodicPrizePool is Timelock, BaseRelayRecipient, ReentrancyG // Ticket Minting/Redeeming // - function mintTickets(address to, uint256 amount, bytes calldata data) external nonReentrant { + function mintTickets(address to, uint256 amount, bytes calldata data, bytes calldata operatorData) external nonReentrant { console.log("PeriodicPrizePool mint tickets: %s", amount); _token().transferFrom(_msgSender(), address(this), amount); _supply(amount); - _mintTickets(to, amount, data, ""); + _mintTickets(to, amount, data, operatorData); _mintedTickets(amount); } @@ -362,7 +362,7 @@ abstract contract PeriodicPrizePool is Timelock, BaseRelayRecipient, ReentrancyG // burn the tickets _burnTickets(from, tickets, data, operatorData); // burn the interestTracker - _redeemTicketInterestShares(from, tickets, userInterestRatioMantissa); + _redeemTicketInterestShares(from, tickets, userInterestRatioMantissa, data, operatorData); // redeem the tickets less the fee uint256 amount = tickets.sub(exitFee); @@ -394,39 +394,53 @@ abstract contract PeriodicPrizePool is Timelock, BaseRelayRecipient, ReentrancyG return FixedPoint.calculateMantissa(_balanceOfTicketInterest(user, tickets), tickets); } - function redeemTicketsInstantly(uint256 tickets, bytes calldata data) external nonReentrant returns (uint256) { + function redeemTicketsInstantly( + uint256 tickets, + bytes calldata data, + bytes calldata operatorData + ) + external nonReentrant returns (uint256) + { address sender = _msgSender(); + require(__ticket.balanceOf(sender) >= tickets, "Insufficient balance"); uint256 userInterestRatioMantissa = _ticketInterestRatioMantissa(sender); - uint256 exitFee = calculateExitFee( tickets, userInterestRatioMantissa ); - + // burn the tickets - _burnTickets(sender, tickets, data, ""); + _burnTickets(sender, tickets, data, operatorData); // now calculate how much interest needs to be redeemed to maintain the interest ratio - _redeemTicketInterestShares(sender, tickets, userInterestRatioMantissa); + _redeemTicketInterestShares(sender, tickets, userInterestRatioMantissa, data, operatorData); uint256 ticketsLessFee = tickets.sub(exitFee); - + // redeem the interestTracker less the fee _redeem(ticketsLessFee); _token().transfer(sender, ticketsLessFee); - emit TicketsRedeemedInstantly(sender, sender, tickets, exitFee, data, ""); + emit TicketsRedeemedInstantly(sender, sender, tickets, exitFee, data, operatorData); // return the exit fee return exitFee; } - function _redeemTicketInterestShares(address sender, uint256 tickets, uint256 userInterestRatioMantissa) internal { + function _redeemTicketInterestShares( + address sender, + uint256 tickets, + uint256 userInterestRatioMantissa, + bytes memory data, + bytes memory operatorData + ) + internal + { uint256 ticketInterest = FixedPoint.multiplyUintByMantissa(tickets, userInterestRatioMantissa); uint256 burnedShares = redeemCollateral(tickets.add(ticketInterest)); ticketInterestShares[sender] = ticketInterestShares[sender].sub(burnedShares); - ticketCredit.controllerMint(sender, ticketInterest, "", ""); + ticketCredit.controllerMint(sender, ticketInterest, data, operatorData); } function operatorRedeemTicketsWithTimelock( @@ -697,7 +711,7 @@ abstract contract PeriodicPrizePool is Timelock, BaseRelayRecipient, ReentrancyG // otherwise we need to transfer the collateral from one user to the other // the from's collateralization will increase, so credit them uint256 fromTicketInterestRatio = _ticketInterestRatioMantissa(from); - _redeemTicketInterestShares(from, amount, fromTicketInterestRatio); + _redeemTicketInterestShares(from, amount, fromTicketInterestRatio, "", ""); _mintTicketInterestShares(to, amount); } diff --git a/contracts/test/CompoundPeriodicPrizePoolHarness.sol b/contracts/test/CompoundPeriodicPrizePoolHarness.sol index fed3b594..4a2bc3ef 100644 --- a/contracts/test/CompoundPeriodicPrizePoolHarness.sol +++ b/contracts/test/CompoundPeriodicPrizePoolHarness.sol @@ -15,6 +15,10 @@ contract CompoundPeriodicPrizePoolHarness is CompoundPeriodicPrizePool { previousPrize = _previousPrize; } + function setPrizeAverageTickets(uint256 _prizeAverageTickets) external { + prizeAverageTickets = _prizeAverageTickets; + } + function setCurrentTime(uint256 _time) external { time = _time; } @@ -26,8 +30,9 @@ contract CompoundPeriodicPrizePoolHarness is CompoundPeriodicPrizePool { return time; } - function setInterestSharesForTest(address user, uint256 amount) external { - ticketInterestShares[user] = amount; + function setInterestSharesForTest(address user, uint256 _collateral) external { + uint256 shares = FixedPoint.divideUintByMantissa(_collateral, _exchangeRateMantissa()); + ticketInterestShares[user] = shares; } function setSponsorshipInterestSharesForTest(address user, uint256 amount) external { diff --git a/test/Integration.test.js b/test/Integration.test.js index 835dfd06..f1aa5fee 100644 --- a/test/Integration.test.js +++ b/test/Integration.test.js @@ -13,6 +13,7 @@ const { const toWei = ethers.utils.parseEther const fromWei = ethers.utils.formatEther +const EMPTY_STR = [] const debug = require('debug')('ptv3:Integration.test') @@ -82,7 +83,7 @@ describe('Integration Test', () => { let tx, receipt - await prizePool.mintTickets(wallet._address, toWei('100'), [], overrides) + await prizePool.mintTickets(wallet._address, toWei('100'), EMPTY_STR, EMPTY_STR, overrides) debug('Accrue custom...') @@ -98,7 +99,7 @@ describe('Integration Test', () => { debug('completing award...') - await prizeStrategy.completeAward(prizePool.address, []) + await prizeStrategy.completeAward(prizePool.address, EMPTY_STR) debug('completed award') @@ -107,13 +108,13 @@ describe('Integration Test', () => { debug('Redeem tickets with timelock...') - await prizePool.redeemTicketsWithTimelock(toWei('122'), []) + await prizePool.redeemTicketsWithTimelock(toWei('122'), EMPTY_STR) debug('Second award...') await increaseTime(prizePeriodSeconds * 2) await prizeStrategy.startAward(prizePool.address) - await prizeStrategy.completeAward(prizePool.address, []) + await prizeStrategy.completeAward(prizePool.address, EMPTY_STR) debug('Sweep timelocked funds...') @@ -127,7 +128,7 @@ describe('Integration Test', () => { it('should support instant redemption', async () => { debug('Minting tickets...') await token.approve(prizePool.address, toWei('100')) - await prizePool.mintTickets(wallet._address, toWei('100'), [], overrides) + await prizePool.mintTickets(wallet._address, toWei('100'), EMPTY_STR, EMPTY_STR, overrides) debug('accruing...') @@ -139,7 +140,7 @@ describe('Integration Test', () => { debug('redeeming tickets...') - await prizePool.redeemTicketsInstantly(toWei('100'), []) + await prizePool.redeemTicketsInstantly(toWei('100'), EMPTY_STR, EMPTY_STR) debug('checking balance...') @@ -155,7 +156,7 @@ describe('Integration Test', () => { debug('1.2') - await prizePool2.mintTickets(wallet2._address, toWei('100'), [], overrides) + await prizePool2.mintTickets(wallet2._address, toWei('100'), EMPTY_STR, EMPTY_STR, overrides) debug('1.5') @@ -167,19 +168,19 @@ describe('Integration Test', () => { // second user has not collateralized await token.approve(prizePool.address, toWei('100')) - await prizePool.mintTickets(wallet._address, toWei('100'), [], overrides) + await prizePool.mintTickets(wallet._address, toWei('100'), EMPTY_STR, EMPTY_STR, overrides) debug('3') await prizeStrategy.startAward(prizePool.address) - await prizeStrategy.completeAward(prizePool.address, []) + await prizeStrategy.completeAward(prizePool.address, EMPTY_STR) debug('4') // when second user withdraws, they must pay a fee let balanceBeforeWithdrawal = await token.balanceOf(wallet._address) - await prizePool.redeemTicketsInstantly(toWei('100'), []) + await prizePool.redeemTicketsInstantly(toWei('100'), EMPTY_STR, EMPTY_STR) debug('5') diff --git a/test/PeriodicPrizePool.test.js b/test/PeriodicPrizePool.test.js index b35911d7..e2630d3e 100644 --- a/test/PeriodicPrizePool.test.js +++ b/test/PeriodicPrizePool.test.js @@ -29,7 +29,7 @@ describe('PeriodicPrizePool contract', function() { let ticket, ticketCredit, sponsorship, sponsorshipCredit - let prizePeriodSeconds = toWei('1000') + let prizePeriodSeconds = 1000 beforeEach(async () => { [wallet, wallet2] = await buidler.ethers.getSigners() @@ -99,6 +99,55 @@ describe('PeriodicPrizePool contract', function() { }) }) + // + // Ticket Minting/Redeeming + // + + describe('redeemTicketsInstantly()', () => { + it('should not allow a user to redeem more tickets than they hold', async () => { + const amount = toWei('10') + + // Pre-fund + await prizePool.setInterestSharesForTest(wallet._address, amount) + + // Mocks + await cToken.mock.balanceOfUnderlying.withArgs(prizePool.address).returns(amount) + await ticket.mock.balanceOf.withArgs(wallet._address).returns(amount) + + // Test revert + await expect(prizePool.redeemTicketsInstantly(amount.mul(2), EMPTY_STR, EMPTY_STR)) + .to.be.revertedWith('Insufficient balance') + }) + + it('should allow a user to redeem their tickets', async () => { + const averagePrize = toWei('100') + const amount = toWei('10') + + // Pre-fund + await cToken.mock.balanceOfUnderlying.withArgs(prizePool.address).returns(toWei('0')) + await prizePool.supplyCollateralForTest(amount) + await prizePool.setInterestSharesForTest(wallet._address, amount) + await prizePool.setPrizeAverageTickets(averagePrize) + + // Mocks + await cToken.mock.balanceOfUnderlying.withArgs(prizePool.address).returns(amount) + await ticket.mock.balanceOf.withArgs(wallet._address).returns(amount) + await cToken.mock.redeemUnderlying.withArgs(amount).returns(amount) + await token.mock.transfer.withArgs(wallet._address, amount).returns(true) + await ticket.mock.controllerBurn.withArgs(wallet._address, amount, EMPTY_STR, EMPTY_STR).returns() + await ticketCredit.mock.controllerMint.withArgs(wallet._address, toWei('0'), EMPTY_STR, EMPTY_STR).returns() + + // Test redeemTicketsInstantly + await expect(prizePool.redeemTicketsInstantly(amount, EMPTY_STR, EMPTY_STR)) + .to.emit(prizePool, 'TicketsRedeemedInstantly') + .withArgs(wallet._address, wallet._address, amount, toWei('0'), EMPTY_STR, EMPTY_STR) + }) + }) + + // + // Sponsorship Minting/Redeeming + // + describe('supplySponsorship()', () => { it('should mint sponsorship tokens', async () => { const supplyAmount = toWei('10') @@ -202,6 +251,10 @@ describe('PeriodicPrizePool contract', function() { }) }) + // + // Sponsorship Sweep + // + describe('sweepSponsorship()', () => { it('should allow anyone to sweep sponsorship for a list of users', async () => { const amounts = [toWei('10'), toWei('98765'), toWei('100'), toWei('100000000'), toWei('10101101')] diff --git a/test/features/support/PoolEnv.js b/test/features/support/PoolEnv.js index aeaddcd0..7d3b2443 100644 --- a/test/features/support/PoolEnv.js +++ b/test/features/support/PoolEnv.js @@ -10,6 +10,7 @@ const debug = require('debug')('ptv3:cucumber:world') const toWei = (val) => ethers.utils.parseEther('' + val) const fromWei = (val) => ethers.utils.formatEther('' + val) +const EMPTY_STR = [] function PoolEnv() { @@ -61,7 +62,7 @@ function PoolEnv() { } await token.approve(prizePool.address, amount, this.overrides) - await prizePool.mintTickets(wallet._address, amount, [], this.overrides) + await prizePool.mintTickets(wallet._address, amount, EMPTY_STR, EMPTY_STR, this.overrides) debug(`Bought tickets`) } @@ -127,7 +128,7 @@ function PoolEnv() { await this.env.rng.setRandomNumber(randomNumber, this.overrides) debug(`Completing award...`) - await this.env.prizeStrategy.completeAward(this._prizePool.address, [], this.overrides) + await this.env.prizeStrategy.completeAward(this._prizePool.address, EMPTY_STR, this.overrides) debug('award completed')