From ec29e0fcfec45ed6941f3094932bd2cb63db8e72 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 9 Dec 2021 16:09:52 -0600 Subject: [PATCH 01/55] fix(TwabRewards): check for inexistent promotion --- contracts/TwabRewards.sol | 4 +++- test/TwabRewards.test.ts | 42 +++++++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 62e5f3c..d036a58 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -264,7 +264,9 @@ contract TwabRewards is ITwabRewards { @return Promotion settings */ function _getPromotion(uint256 _promotionId) internal view returns (Promotion memory) { - return _promotions[_promotionId]; + Promotion memory _promotion = _promotions[_promotionId]; + require(_promotion.creator != address(0), "TwabRewards/invalid-promotion"); + return _promotion; } /** diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 1a0bd7a..8b370ca 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -189,6 +189,12 @@ describe('TwabRewards', () => { ); }); + it('should fail to cancel an inexistent promotion', async () => { + await expect(twabRewards.cancelPromotion(1, wallet1.address)).to.be.revertedWith( + 'TwabRewards/invalid-promotion', + ); + }); + it('should fail to cancel promotion if recipient is address zero', async () => { await createPromotion(ticket.address); @@ -235,10 +241,8 @@ describe('TwabRewards', () => { }); it('should fail to extend an inexistent promotion', async () => { - await createPromotion(ticket.address); - - await expect(twabRewards.extendPromotion(2, 6)).to.be.revertedWith( - 'TwabRewards/promotion-not-active', + await expect(twabRewards.extendPromotion(1, 6)).to.be.revertedWith( + 'TwabRewards/invalid-promotion', ); }); @@ -263,6 +267,12 @@ describe('TwabRewards', () => { expect(promotion.epochDuration).to.equal(epochDuration); expect(promotion.numberOfEpochs).to.equal(numberOfEpochs); }); + + it('should revert if promotion id does not exist', async () => { + await expect(twabRewards.callStatic.getPromotion(1)).to.be.revertedWith( + 'TwabRewards/invalid-promotion', + ); + }); }); describe('getRemainingRewards()', async () => { @@ -283,6 +293,12 @@ describe('TwabRewards', () => { ); } }); + + it('should revert if promotion id passed is inexistent', async () => { + await expect(twabRewards.callStatic.getPromotion(1)).to.be.revertedWith( + 'TwabRewards/invalid-promotion', + ); + }); }); describe('getCurrentEpochId()', async () => { @@ -292,6 +308,12 @@ describe('TwabRewards', () => { expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(3); }); + + it('should revert if promotion id passed is inexistent', async () => { + await expect(twabRewards.callStatic.getCurrentEpochId(1)).to.be.revertedWith( + 'TwabRewards/invalid-promotion', + ); + }); }); describe('getRewardsAmount()', async () => { @@ -431,6 +453,12 @@ describe('TwabRewards', () => { twabRewards.callStatic.getRewardsAmount(wallet2.address, 1, ['1', '2', '3']), ).to.be.revertedWith('TwabRewards/epoch-not-over'); }); + + it('should revert if promotion id passed is inexistent', async () => { + await expect( + twabRewards.callStatic.getRewardsAmount(wallet2.address, 1, ['0', '1', '2']), + ).to.be.revertedWith('TwabRewards/invalid-promotion'); + }); }); describe('claimRewards()', async () => { @@ -561,6 +589,12 @@ describe('TwabRewards', () => { .withArgs(promotionId, epochIds, wallet2.address, zeroAmount); }); + it('should fail to claim rewards for an inexistent promotion', async () => { + await expect( + twabRewards.claimRewards(wallet2.address, 1, ['0', '1', '2']), + ).to.be.revertedWith('TwabRewards/invalid-promotion'); + }); + it('should fail to claim rewards if one or more epochs are not over yet', async () => { const wallet2Amount = toWei('750'); const wallet3Amount = toWei('250'); From b2f94fada2b913e57913aa061fb4cce2b4ac4fb7 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 9 Dec 2021 18:14:22 -0600 Subject: [PATCH 02/55] fix(TwabRewards): check timestamp in createPromotion --- contracts/TwabRewards.sol | 1 + contracts/interfaces/ITwabRewards.sol | 3 +++ test/TwabRewards.test.ts | 18 ++++++++++++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index d036a58..12bd3a9 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -94,6 +94,7 @@ contract TwabRewards is ITwabRewards { uint8 _numberOfEpochs ) external override returns (uint256) { _requireTicket(_ticket); + require(_startTimestamp >= block.timestamp, "TwabRewards/past-start-timestamp"); uint256 _nextPromotionId = _latestPromotionId + 1; _latestPromotionId = _nextPromotionId; diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index f46eaf0..89b523f 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -35,6 +35,9 @@ interface ITwabRewards { @dev For sake of simplicity, `msg.sender` will be the creator of the promotion. @dev `_latestPromotionId` starts at 0 and is incremented by 1 for each new promotion. So the first promotion will have id 1, the second 2, etc. + @dev Ideally, `_startTimestamp` should be set to a value far in the future. + So the transaction is minted in a block way ahead of the actual start of the promotion. + The transaction will revert if mined in a block with a timestamp lower to start timestamp. @param _ticket Prize Pool ticket address for which the promotion is created @param _token Address of the token to be distributed @param _tokensPerEpoch Number of tokens to be distributed per epoch diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 8b370ca..5319413 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -7,7 +7,8 @@ import { ethers } from 'hardhat'; import { increaseTime as increaseTimeUtil } from './utils/increaseTime'; -const increaseTime = (time: number) => increaseTimeUtil(provider, time); +// We add 1 cause promotion starts a latest timestamp + 1 +const increaseTime = (time: number) => increaseTimeUtil(provider, time + 1); const { constants, getContractFactory, getSigners, provider, utils, Wallet } = ethers; const { parseEther: toWei } = utils; @@ -56,11 +57,16 @@ describe('TwabRewards', () => { const createPromotion = async ( ticketAddress: string, epochsNumber: number = numberOfEpochs, + startTimestamp?: number, ) => { await rewardToken.mint(wallet1.address, promotionAmount); await rewardToken.approve(twabRewards.address, promotionAmount); - createPromotionTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + if (startTimestamp) { + createPromotionTimestamp = startTimestamp; + } else { + createPromotionTimestamp = (await ethers.provider.getBlock('latest')).timestamp + 1; + } return await twabRewards.createPromotion( ticketAddress, @@ -138,6 +144,14 @@ describe('TwabRewards', () => { ); }); + it('should fail to create a new promotion if start timestamp is before block timestamp', async () => { + const startTimestamp = (await ethers.provider.getBlock('latest')).timestamp - 1; + + await expect( + createPromotion(ticket.address, numberOfEpochs, startTimestamp), + ).to.be.revertedWith('TwabRewards/past-start-timestamp'); + }); + it('should fail to create a new promotion if number of epochs exceeds limit', async () => { await expect(createPromotion(ticket.address, 256)).to.be.reverted; }); From cff6b548be9da4d50c3caa8cf84f6573ae6974bc Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 9 Dec 2021 16:09:52 -0600 Subject: [PATCH 03/55] chore(TwabRewards): add back TwabRewards --- contracts/TwabRewards.sol | 380 +++++++++++++++ contracts/test/TwabRewardsHarness.sol | 19 + test/TwabRewards.test.ts | 666 ++++++++++++++++++++++++++ 3 files changed, 1065 insertions(+) create mode 100644 contracts/TwabRewards.sol create mode 100644 contracts/test/TwabRewardsHarness.sol create mode 100644 test/TwabRewards.test.ts diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol new file mode 100644 index 0000000..d036a58 --- /dev/null +++ b/contracts/TwabRewards.sol @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.6; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@pooltogether/v4-core/contracts/interfaces/ITicket.sol"; + +import "./interfaces/ITwabRewards.sol"; + +/** + * @title PoolTogether V4 TwabRewards + * @author PoolTogether Inc Team + * @notice Contract to distribute rewards to depositors in a pool. + * This contract supports the creation of several promotions that can run simultaneously. + * In order to calculate user rewards, we use the TWAB (Time-Weighted Average Balance) from the Ticket contract. + * This way, users simply need to hold their tickets to be eligible to claim rewards. + * Rewards are calculated based on the average amount of tickets they hold during the epoch duration. + */ +contract TwabRewards is ITwabRewards { + using SafeERC20 for IERC20; + + /* ============ Global Variables ============ */ + + /// @notice Settings of each promotion. + mapping(uint256 => Promotion) internal _promotions; + + /// @notice Latest recorded promotion id. + /// @dev Starts at 0 and is incremented by 1 for each new promotion. So the first promotion will have id 1, the second 2, etc. + uint256 internal _latestPromotionId; + + /// @notice Keeps track of claimed rewards per user. + /// @dev _claimedEpochs[promotionId][user] => claimedEpochs + /// @dev We pack epochs claimed by a user into a uint256. So we can't store more than 255 epochs. + mapping(uint256 => mapping(address => uint256)) internal _claimedEpochs; + + /* ============ Events ============ */ + + /** + @notice Emitted when a promotion is created. + @param promotionId Id of the newly created promotion + */ + event PromotionCreated(uint256 indexed promotionId); + + /** + @notice Emitted when a promotion is cancelled. + @param promotionId Id of the promotion being cancelled + @param amount Amount of tokens transferred to the promotion creator + */ + event PromotionCancelled(uint256 indexed promotionId, uint256 amount); + + /** + @notice Emitted when a promotion is extended. + @param promotionId Id of the promotion being extended + @param numberOfEpochs Number of epochs the promotion has been extended by + */ + event PromotionExtended(uint256 indexed promotionId, uint256 numberOfEpochs); + + /** + @notice Emitted when rewards have been claimed. + @param promotionId Id of the promotion for which epoch rewards were claimed + @param epochIds Ids of the epochs being claimed + @param user Address of the user for which the rewards were claimed + @param amount Amount of tokens transferred to the recipient address + */ + event RewardsClaimed( + uint256 indexed promotionId, + uint256[] epochIds, + address indexed user, + uint256 amount + ); + + /* ============ Modifiers ============ */ + + /// @dev Ensure that the caller is the creator of the promotion. + /// @param _promotionId Id of the promotion to check + modifier onlyPromotionCreator(uint256 _promotionId) { + require( + msg.sender == _getPromotion(_promotionId).creator, + "TwabRewards/only-promotion-creator" + ); + _; + } + + /* ============ External Functions ============ */ + + /// @inheritdoc ITwabRewards + function createPromotion( + address _ticket, + IERC20 _token, + uint216 _tokensPerEpoch, + uint32 _startTimestamp, + uint32 _epochDuration, + uint8 _numberOfEpochs + ) external override returns (uint256) { + _requireTicket(_ticket); + + uint256 _nextPromotionId = _latestPromotionId + 1; + _latestPromotionId = _nextPromotionId; + + _promotions[_nextPromotionId] = Promotion( + msg.sender, + _ticket, + _token, + _tokensPerEpoch, + _startTimestamp, + _epochDuration, + _numberOfEpochs + ); + + _token.safeTransferFrom(msg.sender, address(this), _tokensPerEpoch * _numberOfEpochs); + + emit PromotionCreated(_nextPromotionId); + + return _nextPromotionId; + } + + /// @inheritdoc ITwabRewards + function cancelPromotion(uint256 _promotionId, address _to) + external + override + onlyPromotionCreator(_promotionId) + returns (bool) + { + Promotion memory _promotion = _getPromotion(_promotionId); + + _requirePromotionActive(_promotion); + require(_to != address(0), "TwabRewards/recipient-not-zero-address"); + + uint256 _remainingRewards = _getRemainingRewards(_promotion); + + delete _promotions[_promotionId]; + _promotion.token.safeTransfer(_to, _remainingRewards); + + emit PromotionCancelled(_promotionId, _remainingRewards); + + return true; + } + + /// @inheritdoc ITwabRewards + function extendPromotion(uint256 _promotionId, uint8 _numberOfEpochs) + external + override + returns (bool) + { + Promotion memory _promotion = _getPromotion(_promotionId); + + _requirePromotionActive(_promotion); + + uint8 _extendedNumberOfEpochs = _promotion.numberOfEpochs + _numberOfEpochs; + _promotions[_promotionId].numberOfEpochs = _extendedNumberOfEpochs; + + uint256 _amount = _numberOfEpochs * _promotion.tokensPerEpoch; + _promotion.token.safeTransferFrom(msg.sender, address(this), _amount); + + emit PromotionExtended(_promotionId, _numberOfEpochs); + + return true; + } + + /// @inheritdoc ITwabRewards + function claimRewards( + address _user, + uint256 _promotionId, + uint256[] calldata _epochIds + ) external override returns (uint256) { + Promotion memory _promotion = _getPromotion(_promotionId); + + uint256 _rewardsAmount; + uint256 _userClaimedEpochs = _claimedEpochs[_promotionId][_user]; + + for (uint256 index = 0; index < _epochIds.length; index++) { + uint256 _epochId = _epochIds[index]; + + require( + !_isClaimedEpoch(_userClaimedEpochs, _epochId), + "TwabRewards/rewards-already-claimed" + ); + + _rewardsAmount += _calculateRewardAmount(_user, _promotion, _epochId); + _userClaimedEpochs = _updateClaimedEpoch(_userClaimedEpochs, _epochId); + } + + _claimedEpochs[_promotionId][_user] = _userClaimedEpochs; + + _promotion.token.safeTransfer(_user, _rewardsAmount); + + emit RewardsClaimed(_promotionId, _epochIds, _user, _rewardsAmount); + + return _rewardsAmount; + } + + /// @inheritdoc ITwabRewards + function getPromotion(uint256 _promotionId) external view override returns (Promotion memory) { + return _getPromotion(_promotionId); + } + + /// @inheritdoc ITwabRewards + function getCurrentEpochId(uint256 _promotionId) external view override returns (uint256) { + return _getCurrentEpochId(_getPromotion(_promotionId)); + } + + /// @inheritdoc ITwabRewards + function getRemainingRewards(uint256 _promotionId) external view override returns (uint256) { + return _getRemainingRewards(_getPromotion(_promotionId)); + } + + /// @inheritdoc ITwabRewards + function getRewardsAmount( + address _user, + uint256 _promotionId, + uint256[] calldata _epochIds + ) external view override returns (uint256[] memory) { + Promotion memory _promotion = _getPromotion(_promotionId); + uint256[] memory _rewardsAmount = new uint256[](_epochIds.length); + + for (uint256 index = 0; index < _epochIds.length; index++) { + _rewardsAmount[index] = _calculateRewardAmount(_user, _promotion, _epochIds[index]); + } + + return _rewardsAmount; + } + + /* ============ Internal Functions ============ */ + + /** + @notice Determine if address passed is actually a ticket. + @param _ticket Address to check + */ + function _requireTicket(address _ticket) internal view { + require(_ticket != address(0), "TwabRewards/ticket-not-zero-address"); + + (bool succeeded, bytes memory data) = address(_ticket).staticcall( + abi.encodePacked(ITicket(_ticket).controller.selector) + ); + + address controllerAddress; + + if (data.length > 0) { + controllerAddress = abi.decode(data, (address)); + } + + require(succeeded && controllerAddress != address(0), "TwabRewards/invalid-ticket"); + } + + /** + @notice Determine if a promotion is active. + @param _promotion Promotion to check + */ + function _requirePromotionActive(Promotion memory _promotion) internal view { + uint256 _promotionEndTimestamp = _promotion.startTimestamp + + (_promotion.epochDuration * _promotion.numberOfEpochs); + + require( + _promotionEndTimestamp > 0 && _promotionEndTimestamp >= block.timestamp, + "TwabRewards/promotion-not-active" + ); + } + + /** + @notice Get settings for a specific promotion. + @dev Will revert if the promotion does not exist. + @param _promotionId Promotion id to get settings for + @return Promotion settings + */ + function _getPromotion(uint256 _promotionId) internal view returns (Promotion memory) { + Promotion memory _promotion = _promotions[_promotionId]; + require(_promotion.creator != address(0), "TwabRewards/invalid-promotion"); + return _promotion; + } + + /** + @notice Get the current epoch id of a promotion. + @dev Epoch ids and their boolean values are tightly packed and stored in a uint256, so epoch id starts at 0. + @param _promotion Promotion to get current epoch for + @return Epoch id + */ + function _getCurrentEpochId(Promotion memory _promotion) internal view returns (uint256) { + // elapsedTimestamp / epochDurationTimestamp + return (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; + } + + /** + @notice Get reward amount for a specific user. + @dev Rewards can only be claimed once the epoch is over. + @param _user User to get reward amount for + @param _promotion Promotion from which the epoch is + @param _epochId Epoch id to get reward amount for + @return Reward amount + */ + function _calculateRewardAmount( + address _user, + Promotion memory _promotion, + uint256 _epochId + ) internal view returns (uint256) { + uint256 _epochDuration = _promotion.epochDuration; + uint256 _epochStartTimestamp = _promotion.startTimestamp + (_epochDuration * _epochId); + uint256 _epochEndTimestamp = _epochStartTimestamp + _epochDuration; + + require(block.timestamp > _epochEndTimestamp, "TwabRewards/epoch-not-over"); + + ITicket _ticket = ITicket(_promotion.ticket); + + uint256 _averageBalance = _ticket.getAverageBalanceBetween( + _user, + uint64(_epochStartTimestamp), + uint64(_epochEndTimestamp) + ); + + uint64[] memory _epochStartTimestamps = new uint64[](1); + _epochStartTimestamps[0] = uint64(_epochStartTimestamp); + + uint64[] memory _epochEndTimestamps = new uint64[](1); + _epochEndTimestamps[0] = uint64(_epochEndTimestamp); + + uint256[] memory _averageTotalSupplies = _ticket.getAverageTotalSuppliesBetween( + _epochStartTimestamps, + _epochEndTimestamps + ); + + if (_averageTotalSupplies[0] > 0) { + return (_promotion.tokensPerEpoch * _averageBalance) / _averageTotalSupplies[0]; + } + + return 0; + } + + /** + @notice Get the total amount of tokens left to be rewarded. + @param _promotion Promotion to get the total amount of tokens left to be rewarded for + @return Amount of tokens left to be rewarded + */ + function _getRemainingRewards(Promotion memory _promotion) internal view returns (uint256) { + // _tokensPerEpoch * _numberOfEpochsLeft + return + _promotion.tokensPerEpoch * + (_promotion.numberOfEpochs - _getCurrentEpochId(_promotion)); + } + + /** + @notice Set boolean value for a specific epoch. + @dev Bits are stored in a uint256 from right to left. + Let's take the example of the following 8 bits word. 0110 0011 + To set the boolean value to 1 for the epoch id 2, we need to create a mask by shifting 1 to the left by 2 bits. + We get: 0000 0001 << 2 = 0000 0100 + We then OR the mask with the word to set the value. + We get: 0110 0011 | 0000 0100 = 0110 0111 + @param _userClaimedEpochs Tightly packed epoch ids with their boolean values + @param _epochId Id of the epoch to set the boolean for + @return Tightly packed epoch ids with the newly boolean value set + */ + function _updateClaimedEpoch(uint256 _userClaimedEpochs, uint256 _epochId) + internal + pure + returns (uint256) + { + return _userClaimedEpochs | (uint256(1) << _epochId); + } + + /** + @notice Check if rewards of an epoch for a given promotion have already been claimed by the user. + @dev Bits are stored in a uint256 from right to left. + Let's take the example of the following 8 bits word. 0110 0111 + To retrieve the boolean value for the epoch id 2, we need to shift the word to the right by 2 bits. + We get: 0110 0111 >> 2 = 0001 1001 + We then get the value of the last bit by masking with 1. + We get: 0001 1001 & 0000 0001 = 0000 0001 = 1 + We then return the boolean value true since the last bit is 1. + @param _userClaimedEpochs Record of epochs already claimed by the user + @param _epochId Epoch id to check + @return true if the rewards have already been claimed for the given epoch, false otherwise + */ + function _isClaimedEpoch(uint256 _userClaimedEpochs, uint256 _epochId) + internal + pure + returns (bool) + { + return (_userClaimedEpochs >> _epochId) & uint256(1) == 1; + } +} diff --git a/contracts/test/TwabRewardsHarness.sol b/contracts/test/TwabRewardsHarness.sol new file mode 100644 index 0000000..a2d07b5 --- /dev/null +++ b/contracts/test/TwabRewardsHarness.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.6; + +import "../TwabRewards.sol"; + +contract TwabRewardsHarness is TwabRewards { + function requireTicket(address _ticket) external view { + return _requireTicket(_ticket); + } + + function isClaimedEpoch(uint256 _userClaimedEpochs, uint8 _epochId) + external + pure + returns (bool) + { + return _isClaimedEpoch(_userClaimedEpochs, _epochId); + } +} diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts new file mode 100644 index 0000000..8b370ca --- /dev/null +++ b/test/TwabRewards.test.ts @@ -0,0 +1,666 @@ +import TicketInterface from '@pooltogether/v4-core/abis/ITicket.json'; +import { deployMockContract, MockContract } from '@ethereum-waffle/mock-contract'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { Contract, ContractFactory, Signer } from 'ethers'; +import { ethers } from 'hardhat'; + +import { increaseTime as increaseTimeUtil } from './utils/increaseTime'; + +const increaseTime = (time: number) => increaseTimeUtil(provider, time); + +const { constants, getContractFactory, getSigners, provider, utils, Wallet } = ethers; +const { parseEther: toWei } = utils; +const { AddressZero } = constants; + +describe('TwabRewards', () => { + let wallet1: SignerWithAddress; + let wallet2: SignerWithAddress; + let wallet3: SignerWithAddress; + + let erc20MintableFactory: ContractFactory; + let ticketFactory: ContractFactory; + let twabRewardsFactory: ContractFactory; + + let rewardToken: Contract; + let ticket: Contract; + let twabRewards: Contract; + + let mockTicket: MockContract; + + let createPromotionTimestamp: number; + + before(async () => { + [wallet1, wallet2, wallet3] = await getSigners(); + + erc20MintableFactory = await getContractFactory('ERC20Mintable'); + ticketFactory = await getContractFactory('TicketHarness'); + twabRewardsFactory = await getContractFactory('TwabRewardsHarness'); + }); + + beforeEach(async () => { + rewardToken = await erc20MintableFactory.deploy('Reward', 'REWA'); + twabRewards = await twabRewardsFactory.deploy(); + ticket = await erc20MintableFactory.deploy('Ticket', 'TICK'); + + ticket = await ticketFactory.deploy('Ticket', 'TICK', 18, wallet1.address); + + mockTicket = await deployMockContract(wallet1, TicketInterface); + }); + + const tokensPerEpoch = toWei('10000'); + const epochDuration = 604800; // 1 week in seconds + const numberOfEpochs = 12; // 3 months since 1 epoch runs for 1 week + const promotionAmount = tokensPerEpoch.mul(numberOfEpochs); + + const createPromotion = async ( + ticketAddress: string, + epochsNumber: number = numberOfEpochs, + ) => { + await rewardToken.mint(wallet1.address, promotionAmount); + await rewardToken.approve(twabRewards.address, promotionAmount); + + createPromotionTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + return await twabRewards.createPromotion( + ticketAddress, + rewardToken.address, + tokensPerEpoch, + createPromotionTimestamp, + epochDuration, + epochsNumber, + ); + }; + + describe('createPromotion()', async () => { + it('should create a new promotion', async () => { + const promotionId = 1; + + await expect(createPromotion(ticket.address)) + .to.emit(twabRewards, 'PromotionCreated') + .withArgs(promotionId); + + const promotion = await twabRewards.callStatic.getPromotion(promotionId); + + expect(promotion.creator).to.equal(wallet1.address); + expect(promotion.ticket).to.equal(ticket.address); + expect(promotion.token).to.equal(rewardToken.address); + expect(promotion.tokensPerEpoch).to.equal(tokensPerEpoch); + expect(promotion.startTimestamp).to.equal(createPromotionTimestamp); + expect(promotion.epochDuration).to.equal(epochDuration); + expect(promotion.numberOfEpochs).to.equal(numberOfEpochs); + }); + + it('should create a second promotion and handle allowance properly', async () => { + const promotionIdOne = 1; + const promotionIdTwo = 2; + + await expect(createPromotion(ticket.address)) + .to.emit(twabRewards, 'PromotionCreated') + .withArgs(promotionIdOne); + + const firstPromotion = await twabRewards.callStatic.getPromotion(promotionIdOne); + + expect(firstPromotion.creator).to.equal(wallet1.address); + expect(firstPromotion.ticket).to.equal(ticket.address); + expect(firstPromotion.token).to.equal(rewardToken.address); + expect(firstPromotion.tokensPerEpoch).to.equal(tokensPerEpoch); + expect(firstPromotion.startTimestamp).to.equal(createPromotionTimestamp); + expect(firstPromotion.epochDuration).to.equal(epochDuration); + expect(firstPromotion.numberOfEpochs).to.equal(numberOfEpochs); + + await expect(createPromotion(ticket.address)) + .to.emit(twabRewards, 'PromotionCreated') + .withArgs(promotionIdTwo); + + const secondPromotion = await twabRewards.callStatic.getPromotion(promotionIdTwo); + + expect(secondPromotion.creator).to.equal(wallet1.address); + expect(secondPromotion.ticket).to.equal(ticket.address); + expect(secondPromotion.token).to.equal(rewardToken.address); + expect(secondPromotion.tokensPerEpoch).to.equal(tokensPerEpoch); + expect(secondPromotion.startTimestamp).to.equal(createPromotionTimestamp); + expect(secondPromotion.epochDuration).to.equal(epochDuration); + expect(secondPromotion.numberOfEpochs).to.equal(numberOfEpochs); + }); + + it('should fail to create a new promotion if ticket is address zero', async () => { + await expect(createPromotion(AddressZero)).to.be.revertedWith( + 'TwabRewards/ticket-not-zero-address', + ); + }); + + it('should fail to create a new promotion if ticket is not an actual ticket', async () => { + const randomWallet = Wallet.createRandom(); + + await expect(createPromotion(randomWallet.address)).to.be.revertedWith( + 'TwabRewards/invalid-ticket', + ); + }); + + it('should fail to create a new promotion if number of epochs exceeds limit', async () => { + await expect(createPromotion(ticket.address, 256)).to.be.reverted; + }); + }); + + describe('cancelPromotion()', async () => { + it('should cancel a promotion and transfer the correct amount of reward tokens', async () => { + for (let index = 0; index < numberOfEpochs; index++) { + let promotionId = index + 1; + + await createPromotion(ticket.address); + + const { epochDuration, numberOfEpochs, tokensPerEpoch } = + await twabRewards.callStatic.getPromotion(promotionId); + + if (index > 0) { + await increaseTime(epochDuration * index); + } + + const transferredAmount = tokensPerEpoch + .mul(numberOfEpochs) + .sub(tokensPerEpoch.mul(index)); + + await expect(twabRewards.cancelPromotion(promotionId, wallet1.address)) + .to.emit(twabRewards, 'PromotionCancelled') + .withArgs(promotionId, transferredAmount); + + expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); + + // We burn tokens from wallet1 to reset balance + await rewardToken.burn(wallet1.address, transferredAmount); + } + }); + + it('should fail to cancel promotion if not owner', async () => { + await createPromotion(ticket.address); + + await expect( + twabRewards.connect(wallet2).cancelPromotion(1, AddressZero), + ).to.be.revertedWith('TwabRewards/only-promotion-creator'); + }); + + it('should fail to cancel an inactive promotion', async () => { + await createPromotion(ticket.address); + await increaseTime(epochDuration * 13); + + await expect(twabRewards.cancelPromotion(1, wallet1.address)).to.be.revertedWith( + 'TwabRewards/promotion-not-active', + ); + }); + + it('should fail to cancel an inexistent promotion', async () => { + await expect(twabRewards.cancelPromotion(1, wallet1.address)).to.be.revertedWith( + 'TwabRewards/invalid-promotion', + ); + }); + + it('should fail to cancel promotion if recipient is address zero', async () => { + await createPromotion(ticket.address); + + await expect(twabRewards.cancelPromotion(1, AddressZero)).to.be.revertedWith( + 'TwabRewards/recipient-not-zero-address', + ); + }); + }); + + describe('extendPromotion()', async () => { + it('should extend a promotion', async () => { + await createPromotion(ticket.address); + + const numberOfEpochsAdded = 6; + const extendedPromotionAmount = tokensPerEpoch.mul(numberOfEpochsAdded); + const extendedPromotionEpochs = numberOfEpochs + numberOfEpochsAdded; + + await rewardToken.mint(wallet1.address, extendedPromotionAmount); + await rewardToken.approve(twabRewards.address, extendedPromotionAmount); + + const promotionId = 1; + + await expect(twabRewards.extendPromotion(promotionId, numberOfEpochsAdded)) + .to.emit(twabRewards, 'PromotionExtended') + .withArgs(promotionId, numberOfEpochsAdded); + + expect( + (await twabRewards.callStatic.getPromotion(promotionId)).numberOfEpochs, + ).to.equal(extendedPromotionEpochs); + + expect(await rewardToken.balanceOf(wallet1.address)).to.equal(0); + expect(await rewardToken.balanceOf(twabRewards.address)).to.equal( + promotionAmount.add(extendedPromotionAmount), + ); + }); + + it('should fail to extend an inactive promotion', async () => { + await createPromotion(ticket.address); + await increaseTime(epochDuration * 13); + + await expect(twabRewards.extendPromotion(1, 6)).to.be.revertedWith( + 'TwabRewards/promotion-not-active', + ); + }); + + it('should fail to extend an inexistent promotion', async () => { + await expect(twabRewards.extendPromotion(1, 6)).to.be.revertedWith( + 'TwabRewards/invalid-promotion', + ); + }); + + it('should fail to extend a promotion over the epochs limit', async () => { + await createPromotion(ticket.address); + + await expect(twabRewards.extendPromotion(1, 244)).to.be.reverted; + }); + }); + + describe('getPromotion()', async () => { + it('should get promotion by id', async () => { + await createPromotion(ticket.address); + + const promotion = await twabRewards.callStatic.getPromotion(1); + + expect(promotion.creator).to.equal(wallet1.address); + expect(promotion.ticket).to.equal(ticket.address); + expect(promotion.token).to.equal(rewardToken.address); + expect(promotion.tokensPerEpoch).to.equal(tokensPerEpoch); + expect(promotion.startTimestamp).to.equal(createPromotionTimestamp); + expect(promotion.epochDuration).to.equal(epochDuration); + expect(promotion.numberOfEpochs).to.equal(numberOfEpochs); + }); + + it('should revert if promotion id does not exist', async () => { + await expect(twabRewards.callStatic.getPromotion(1)).to.be.revertedWith( + 'TwabRewards/invalid-promotion', + ); + }); + }); + + describe('getRemainingRewards()', async () => { + it('should return the correct amount of reward tokens left', async () => { + await createPromotion(ticket.address); + + const promotionId = 1; + const { epochDuration, numberOfEpochs, tokensPerEpoch } = + await twabRewards.callStatic.getPromotion(promotionId); + + for (let index = 0; index < numberOfEpochs; index++) { + if (index > 0) { + await increaseTime(epochDuration); + } + + expect(await twabRewards.getRemainingRewards(promotionId)).to.equal( + tokensPerEpoch.mul(numberOfEpochs).sub(tokensPerEpoch.mul(index)), + ); + } + }); + + it('should revert if promotion id passed is inexistent', async () => { + await expect(twabRewards.callStatic.getPromotion(1)).to.be.revertedWith( + 'TwabRewards/invalid-promotion', + ); + }); + }); + + describe('getCurrentEpochId()', async () => { + it('should get the current epoch id of a promotion', async () => { + await createPromotion(ticket.address); + await increaseTime(epochDuration * 3); + + expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(3); + }); + + it('should revert if promotion id passed is inexistent', async () => { + await expect(twabRewards.callStatic.getCurrentEpochId(1)).to.be.revertedWith( + 'TwabRewards/invalid-promotion', + ); + }); + }); + + describe('getRewardsAmount()', async () => { + it('should get rewards amount for one or more epochs', async () => { + const promotionId = 1; + const epochIds = ['0', '1', '2']; + + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + const totalAmount = wallet2Amount.add(wallet3Amount); + + const wallet2ShareOfTickets = wallet2Amount.mul(100).div(totalAmount); + const wallet2RewardAmount = wallet2ShareOfTickets.mul(tokensPerEpoch).div(100); + + const wallet3ShareOfTickets = wallet3Amount.mul(100).div(totalAmount); + const wallet3RewardAmount = wallet3ShareOfTickets.mul(tokensPerEpoch).div(100); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.connect(wallet2).delegate(wallet2.address); + await ticket.mint(wallet3.address, wallet3Amount); + await ticket.connect(wallet3).delegate(wallet3.address); + + await createPromotion(ticket.address); + await increaseTime(epochDuration * 3); + + expect( + await twabRewards.callStatic.getRewardsAmount( + wallet2.address, + promotionId, + epochIds, + ), + ).to.deep.equal([wallet2RewardAmount, wallet2RewardAmount, wallet2RewardAmount]); + + expect( + await twabRewards.callStatic.getRewardsAmount( + wallet3.address, + promotionId, + epochIds, + ), + ).to.deep.equal([wallet3RewardAmount, wallet3RewardAmount, wallet3RewardAmount]); + }); + + it('should decrease rewards amount if user delegate in the middle of an epoch', async () => { + const promotionId = 1; + const epochIds = ['0', '1', '2']; + const halfEpoch = epochDuration / 2; + + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + const totalAmount = wallet2Amount.add(wallet3Amount); + + const wallet2ShareOfTickets = wallet2Amount.mul(100).div(totalAmount); + const wallet2RewardAmount = wallet2ShareOfTickets.mul(tokensPerEpoch).div(100); + + const wallet3ShareOfTickets = wallet3Amount.mul(100).div(totalAmount); + const wallet3RewardAmount = wallet3ShareOfTickets.mul(tokensPerEpoch).div(100); + const wallet3HalfRewardAmount = wallet3RewardAmount.div(2); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.connect(wallet2).delegate(wallet2.address); + await ticket.mint(wallet3.address, wallet3Amount); + await ticket.connect(wallet3).delegate(wallet3.address); + + const timestampBeforeCreate = (await ethers.provider.getBlock('latest')).timestamp; + + await createPromotion(ticket.address); + + const timestampAfterCreate = (await ethers.provider.getBlock('latest')).timestamp; + const elapsedTimeCreate = timestampAfterCreate - timestampBeforeCreate; + + // We adjust time to delegate right in the middle of epoch 3 + await increaseTime(epochDuration * 2 + halfEpoch - (elapsedTimeCreate - 1)); + + await ticket.connect(wallet3).delegate(wallet2.address); + + await increaseTime(halfEpoch + 1); + + expect( + await twabRewards.callStatic.getRewardsAmount( + wallet2.address, + promotionId, + epochIds, + ), + ).to.deep.equal([ + wallet2RewardAmount, + wallet2RewardAmount, + wallet2RewardAmount.add(wallet3HalfRewardAmount), + ]); + + expect( + await twabRewards.callStatic.getRewardsAmount( + wallet3.address, + promotionId, + epochIds, + ), + ).to.deep.equal([wallet3RewardAmount, wallet3RewardAmount, wallet3HalfRewardAmount]); + }); + + it('should return 0 if user has no tickets delegated to him', async () => { + const wallet2Amount = toWei('750'); + const zeroAmount = toWei('0'); + + await ticket.mint(wallet2.address, wallet2Amount); + + await createPromotion(ticket.address); + await increaseTime(epochDuration * 3); + + expect( + await twabRewards.callStatic.getRewardsAmount(wallet2.address, 1, ['0', '1', '2']), + ).to.deep.equal([zeroAmount, zeroAmount, zeroAmount]); + }); + + it('should return 0 if ticket average total supplies is 0', async () => { + const zeroAmount = toWei('0'); + + await createPromotion(ticket.address); + await increaseTime(epochDuration * 3); + + expect( + await twabRewards.callStatic.getRewardsAmount(wallet2.address, 1, ['0', '1', '2']), + ).to.deep.equal([zeroAmount, zeroAmount, zeroAmount]); + }); + + it('should fail to get rewards amount if one or more epochs are not over yet', async () => { + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.mint(wallet3.address, wallet3Amount); + + await createPromotion(ticket.address); + await increaseTime(epochDuration * 3); + + await expect( + twabRewards.callStatic.getRewardsAmount(wallet2.address, 1, ['1', '2', '3']), + ).to.be.revertedWith('TwabRewards/epoch-not-over'); + }); + + it('should revert if promotion id passed is inexistent', async () => { + await expect( + twabRewards.callStatic.getRewardsAmount(wallet2.address, 1, ['0', '1', '2']), + ).to.be.revertedWith('TwabRewards/invalid-promotion'); + }); + }); + + describe('claimRewards()', async () => { + it('should claim rewards for one or more epochs', async () => { + const promotionId = 1; + const epochIds = ['0', '1', '2']; + + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + const totalAmount = wallet2Amount.add(wallet3Amount); + + const wallet2ShareOfTickets = wallet2Amount.mul(100).div(totalAmount); + const wallet2RewardAmount = wallet2ShareOfTickets.mul(tokensPerEpoch).div(100); + const wallet2TotalRewardsAmount = wallet2RewardAmount.mul(3); + + const wallet3ShareOfTickets = wallet3Amount.mul(100).div(totalAmount); + const wallet3RewardAmount = wallet3ShareOfTickets.mul(tokensPerEpoch).div(100); + const wallet3TotalRewardsAmount = wallet3RewardAmount.mul(3); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.connect(wallet2).delegate(wallet2.address); + await ticket.mint(wallet3.address, wallet3Amount); + await ticket.connect(wallet3).delegate(wallet3.address); + + await createPromotion(ticket.address); + await increaseTime(epochDuration * 3); + + await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) + .to.emit(twabRewards, 'RewardsClaimed') + .withArgs(promotionId, epochIds, wallet2.address, wallet2TotalRewardsAmount); + + await expect(twabRewards.claimRewards(wallet3.address, promotionId, epochIds)) + .to.emit(twabRewards, 'RewardsClaimed') + .withArgs(promotionId, epochIds, wallet3.address, wallet3TotalRewardsAmount); + + expect(await rewardToken.balanceOf(wallet2.address)).to.equal( + wallet2TotalRewardsAmount, + ); + + expect(await rewardToken.balanceOf(wallet3.address)).to.equal( + wallet3TotalRewardsAmount, + ); + }); + + it('should decrease rewards amount claimed if user delegate in the middle of an epoch', async () => { + const promotionId = 1; + const epochIds = ['0', '1', '2']; + const halfEpoch = epochDuration / 2; + + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + const totalAmount = wallet2Amount.add(wallet3Amount); + + const wallet3ShareOfTickets = wallet3Amount.mul(100).div(totalAmount); + const wallet3RewardAmount = wallet3ShareOfTickets.mul(tokensPerEpoch).div(100); + const wallet3HalfRewardAmount = wallet3RewardAmount.div(2); + const wallet3TotalRewardsAmount = wallet3RewardAmount + .mul(3) + .sub(wallet3HalfRewardAmount); + + const wallet2ShareOfTickets = wallet2Amount.mul(100).div(totalAmount); + const wallet2RewardAmount = wallet2ShareOfTickets.mul(tokensPerEpoch).div(100); + const wallet2TotalRewardsAmount = wallet2RewardAmount + .mul(3) + .add(wallet3HalfRewardAmount); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.connect(wallet2).delegate(wallet2.address); + await ticket.mint(wallet3.address, wallet3Amount); + await ticket.connect(wallet3).delegate(wallet3.address); + + await createPromotion(ticket.address); + + // We adjust time to delegate right in the middle of epoch 3 + await increaseTime(epochDuration * 2 + halfEpoch - 2); + + await ticket.connect(wallet3).delegate(wallet2.address); + + await increaseTime(halfEpoch + 1); + + await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) + .to.emit(twabRewards, 'RewardsClaimed') + .withArgs(promotionId, epochIds, wallet2.address, wallet2TotalRewardsAmount); + + await expect(twabRewards.claimRewards(wallet3.address, promotionId, epochIds)) + .to.emit(twabRewards, 'RewardsClaimed') + .withArgs(promotionId, epochIds, wallet3.address, wallet3TotalRewardsAmount); + + expect(await rewardToken.balanceOf(wallet2.address)).to.equal( + wallet2TotalRewardsAmount, + ); + + expect(await rewardToken.balanceOf(wallet3.address)).to.equal( + wallet3TotalRewardsAmount, + ); + }); + + it('should claim 0 rewards if user has no tickets delegated to him', async () => { + const promotionId = 1; + const epochIds = ['0', '1', '2']; + const wallet2Amount = toWei('750'); + const zeroAmount = toWei('0'); + + await ticket.mint(wallet2.address, wallet2Amount); + + await createPromotion(ticket.address); + await increaseTime(epochDuration * 3); + + await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) + .to.emit(twabRewards, 'RewardsClaimed') + .withArgs(promotionId, epochIds, wallet2.address, zeroAmount); + + expect(await rewardToken.balanceOf(wallet2.address)).to.equal(zeroAmount); + }); + + it('should return 0 if ticket average total supplies is 0', async () => { + const promotionId = 1; + const epochIds = ['0', '1', '2']; + const zeroAmount = toWei('0'); + + await createPromotion(ticket.address); + await increaseTime(epochDuration * 3); + + await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) + .to.emit(twabRewards, 'RewardsClaimed') + .withArgs(promotionId, epochIds, wallet2.address, zeroAmount); + }); + + it('should fail to claim rewards for an inexistent promotion', async () => { + await expect( + twabRewards.claimRewards(wallet2.address, 1, ['0', '1', '2']), + ).to.be.revertedWith('TwabRewards/invalid-promotion'); + }); + + it('should fail to claim rewards if one or more epochs are not over yet', async () => { + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.mint(wallet3.address, wallet3Amount); + + await createPromotion(ticket.address); + await increaseTime(epochDuration * 3); + + await expect( + twabRewards.claimRewards(wallet2.address, 1, ['1', '2', '3']), + ).to.be.revertedWith('TwabRewards/epoch-not-over'); + }); + + it('should fail to claim rewards if one or more epochs have already been claimed', async () => { + const promotionId = 1; + + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.mint(wallet3.address, wallet3Amount); + + await createPromotion(ticket.address); + await increaseTime(epochDuration * 3); + + await twabRewards.claimRewards(wallet2.address, promotionId, ['0', '1', '2']); + + await expect( + twabRewards.claimRewards(wallet2.address, promotionId, ['2', '3', '4']), + ).to.be.revertedWith('TwabRewards/rewards-already-claimed'); + }); + }); + + describe('_requireTicket()', () => { + it('should revert if ticket address is address zero', async () => { + await expect(twabRewards.requireTicket(AddressZero)).to.be.revertedWith( + 'TwabRewards/ticket-not-zero-address', + ); + }); + + it('should revert if controller does not exist', async () => { + const randomWallet = Wallet.createRandom(); + + await expect(twabRewards.requireTicket(randomWallet.address)).to.be.revertedWith( + 'TwabRewards/invalid-ticket', + ); + }); + + it('should revert if controller address is address zero', async () => { + await mockTicket.mock.controller.returns(AddressZero); + + await expect(twabRewards.requireTicket(mockTicket.address)).to.be.revertedWith( + 'TwabRewards/invalid-ticket', + ); + }); + }); + + describe('_isClaimedEpoch()', () => { + it('should return true for a claimed epoch', async () => { + expect(await twabRewards.callStatic.isClaimedEpoch('01100111', 2)).to.equal(true); + }); + + it('should return false for an unclaimed epoch', async () => { + expect(await twabRewards.callStatic.isClaimedEpoch('01100011', 2)).to.equal(false); + }); + }); +}); From 3765e13b7d3058152c50db796b9057614bae2d51 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 12 Jan 2022 16:08:15 -0600 Subject: [PATCH 04/55] fix(TwabRewards): don't check timestamp in createPromotion --- contracts/TwabRewards.sol | 1 - contracts/interfaces/ITwabRewards.sol | 3 --- test/TwabRewards.test.ts | 16 ++++++++-------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 12bd3a9..d036a58 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -94,7 +94,6 @@ contract TwabRewards is ITwabRewards { uint8 _numberOfEpochs ) external override returns (uint256) { _requireTicket(_ticket); - require(_startTimestamp >= block.timestamp, "TwabRewards/past-start-timestamp"); uint256 _nextPromotionId = _latestPromotionId + 1; _latestPromotionId = _nextPromotionId; diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index 89b523f..f46eaf0 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -35,9 +35,6 @@ interface ITwabRewards { @dev For sake of simplicity, `msg.sender` will be the creator of the promotion. @dev `_latestPromotionId` starts at 0 and is incremented by 1 for each new promotion. So the first promotion will have id 1, the second 2, etc. - @dev Ideally, `_startTimestamp` should be set to a value far in the future. - So the transaction is minted in a block way ahead of the actual start of the promotion. - The transaction will revert if mined in a block with a timestamp lower to start timestamp. @param _ticket Prize Pool ticket address for which the promotion is created @param _token Address of the token to be distributed @param _tokensPerEpoch Number of tokens to be distributed per epoch diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 5319413..a51dfdf 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -130,6 +130,14 @@ describe('TwabRewards', () => { expect(secondPromotion.numberOfEpochs).to.equal(numberOfEpochs); }); + it('should succeed to create a new promotion even if start timestamp is before block timestamp', async () => { + const startTimestamp = (await ethers.provider.getBlock('latest')).timestamp - 1; + + await expect(createPromotion(ticket.address, numberOfEpochs, startTimestamp)) + .to.emit(twabRewards, 'PromotionCreated') + .withArgs(1); + }); + it('should fail to create a new promotion if ticket is address zero', async () => { await expect(createPromotion(AddressZero)).to.be.revertedWith( 'TwabRewards/ticket-not-zero-address', @@ -144,14 +152,6 @@ describe('TwabRewards', () => { ); }); - it('should fail to create a new promotion if start timestamp is before block timestamp', async () => { - const startTimestamp = (await ethers.provider.getBlock('latest')).timestamp - 1; - - await expect( - createPromotion(ticket.address, numberOfEpochs, startTimestamp), - ).to.be.revertedWith('TwabRewards/past-start-timestamp'); - }); - it('should fail to create a new promotion if number of epochs exceeds limit', async () => { await expect(createPromotion(ticket.address, 256)).to.be.reverted; }); From 70f5536520dc2f7ccb2c2758ad566449a779c53b Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 10 Dec 2021 10:18:24 -0600 Subject: [PATCH 05/55] fix(TwabRewards): improve Promotion packing --- contracts/TwabRewards.sol | 8 ++++---- contracts/interfaces/ITwabRewards.sol | 16 ++++++++-------- test/TwabRewards.test.ts | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index d036a58..8819d87 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -88,9 +88,9 @@ contract TwabRewards is ITwabRewards { function createPromotion( address _ticket, IERC20 _token, - uint216 _tokensPerEpoch, - uint32 _startTimestamp, - uint32 _epochDuration, + uint128 _startTimestamp, + uint256 _tokensPerEpoch, + uint56 _epochDuration, uint8 _numberOfEpochs ) external override returns (uint256) { _requireTicket(_ticket); @@ -102,8 +102,8 @@ contract TwabRewards is ITwabRewards { msg.sender, _ticket, _token, - _tokensPerEpoch, _startTimestamp, + _tokensPerEpoch, _epochDuration, _numberOfEpochs ); diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index f46eaf0..fcc6071 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -15,8 +15,8 @@ interface ITwabRewards { @param creator Addresss of the promotion creator @param ticket Prize Pool ticket address for which the promotion has been created @param token Address of the token to be distributed as reward - @param tokensPerEpoch Number of tokens to be distributed per epoch @param startTimestamp Timestamp at which the promotion starts + @param tokensPerEpoch Number of tokens to be distributed per epoch @param epochDuration Duration of one epoch in seconds @param numberOfEpochs Number of epochs the promotion will last for */ @@ -24,9 +24,9 @@ interface ITwabRewards { address creator; address ticket; IERC20 token; - uint216 tokensPerEpoch; - uint32 startTimestamp; - uint32 epochDuration; + uint128 startTimestamp; + uint256 tokensPerEpoch; + uint56 epochDuration; uint8 numberOfEpochs; } @@ -37,8 +37,8 @@ interface ITwabRewards { So the first promotion will have id 1, the second 2, etc. @param _ticket Prize Pool ticket address for which the promotion is created @param _token Address of the token to be distributed - @param _tokensPerEpoch Number of tokens to be distributed per epoch @param _startTimestamp Timestamp at which the promotion starts + @param _tokensPerEpoch Number of tokens to be distributed per epoch @param _epochDuration Duration of one epoch in seconds @param _numberOfEpochs Number of epochs the promotion will last for @return Id of the newly created promotion @@ -46,9 +46,9 @@ interface ITwabRewards { function createPromotion( address _ticket, IERC20 _token, - uint216 _tokensPerEpoch, - uint32 _startTimestamp, - uint32 _epochDuration, + uint128 _startTimestamp, + uint256 _tokensPerEpoch, + uint56 _epochDuration, uint8 _numberOfEpochs ) external returns (uint256); diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index a51dfdf..52fc846 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -71,8 +71,8 @@ describe('TwabRewards', () => { return await twabRewards.createPromotion( ticketAddress, rewardToken.address, - tokensPerEpoch, createPromotionTimestamp, + tokensPerEpoch, epochDuration, epochsNumber, ); @@ -299,7 +299,7 @@ describe('TwabRewards', () => { for (let index = 0; index < numberOfEpochs; index++) { if (index > 0) { - await increaseTime(epochDuration); + await increaseTime(epochDuration.toNumber()); } expect(await twabRewards.getRemainingRewards(promotionId)).to.equal( From f9bbd72a7d99ee52b733ba765cf79a94012afcaa Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 10 Dec 2021 10:38:39 -0600 Subject: [PATCH 06/55] fix(TwabRewards): shorten require messages --- contracts/TwabRewards.sol | 10 +++++----- test/TwabRewards.test.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 8819d87..adb6847 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -77,7 +77,7 @@ contract TwabRewards is ITwabRewards { modifier onlyPromotionCreator(uint256 _promotionId) { require( msg.sender == _getPromotion(_promotionId).creator, - "TwabRewards/only-promotion-creator" + "TwabRewards/only-promo-creator" ); _; } @@ -125,7 +125,7 @@ contract TwabRewards is ITwabRewards { Promotion memory _promotion = _getPromotion(_promotionId); _requirePromotionActive(_promotion); - require(_to != address(0), "TwabRewards/recipient-not-zero-address"); + require(_to != address(0), "TwabRewards/payee-not-zero-addr"); uint256 _remainingRewards = _getRemainingRewards(_promotion); @@ -174,7 +174,7 @@ contract TwabRewards is ITwabRewards { require( !_isClaimedEpoch(_userClaimedEpochs, _epochId), - "TwabRewards/rewards-already-claimed" + "TwabRewards/rewards-claimed" ); _rewardsAmount += _calculateRewardAmount(_user, _promotion, _epochId); @@ -228,7 +228,7 @@ contract TwabRewards is ITwabRewards { @param _ticket Address to check */ function _requireTicket(address _ticket) internal view { - require(_ticket != address(0), "TwabRewards/ticket-not-zero-address"); + require(_ticket != address(0), "TwabRewards/ticket-not-zero-addr"); (bool succeeded, bytes memory data) = address(_ticket).staticcall( abi.encodePacked(ITicket(_ticket).controller.selector) @@ -253,7 +253,7 @@ contract TwabRewards is ITwabRewards { require( _promotionEndTimestamp > 0 && _promotionEndTimestamp >= block.timestamp, - "TwabRewards/promotion-not-active" + "TwabRewards/promotion-inactive" ); } diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 52fc846..d97621d 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -140,7 +140,7 @@ describe('TwabRewards', () => { it('should fail to create a new promotion if ticket is address zero', async () => { await expect(createPromotion(AddressZero)).to.be.revertedWith( - 'TwabRewards/ticket-not-zero-address', + 'TwabRewards/ticket-not-zero-addr', ); }); @@ -191,7 +191,7 @@ describe('TwabRewards', () => { await expect( twabRewards.connect(wallet2).cancelPromotion(1, AddressZero), - ).to.be.revertedWith('TwabRewards/only-promotion-creator'); + ).to.be.revertedWith('TwabRewards/only-promo-creator'); }); it('should fail to cancel an inactive promotion', async () => { @@ -199,7 +199,7 @@ describe('TwabRewards', () => { await increaseTime(epochDuration * 13); await expect(twabRewards.cancelPromotion(1, wallet1.address)).to.be.revertedWith( - 'TwabRewards/promotion-not-active', + 'TwabRewards/promotion-inactive', ); }); @@ -213,7 +213,7 @@ describe('TwabRewards', () => { await createPromotion(ticket.address); await expect(twabRewards.cancelPromotion(1, AddressZero)).to.be.revertedWith( - 'TwabRewards/recipient-not-zero-address', + 'TwabRewards/payee-not-zero-addr', ); }); }); @@ -250,7 +250,7 @@ describe('TwabRewards', () => { await increaseTime(epochDuration * 13); await expect(twabRewards.extendPromotion(1, 6)).to.be.revertedWith( - 'TwabRewards/promotion-not-active', + 'TwabRewards/promotion-inactive', ); }); @@ -640,14 +640,14 @@ describe('TwabRewards', () => { await expect( twabRewards.claimRewards(wallet2.address, promotionId, ['2', '3', '4']), - ).to.be.revertedWith('TwabRewards/rewards-already-claimed'); + ).to.be.revertedWith('TwabRewards/rewards-claimed'); }); }); describe('_requireTicket()', () => { it('should revert if ticket address is address zero', async () => { await expect(twabRewards.requireTicket(AddressZero)).to.be.revertedWith( - 'TwabRewards/ticket-not-zero-address', + 'TwabRewards/ticket-not-zero-addr', ); }); From d3cd51f8e4f7241265c02fa743fb275a0abd2c6b Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 10 Dec 2021 11:19:47 -0600 Subject: [PATCH 07/55] fix(TwabRewards): store array length --- contracts/TwabRewards.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index adb6847..6280229 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -168,8 +168,9 @@ contract TwabRewards is ITwabRewards { uint256 _rewardsAmount; uint256 _userClaimedEpochs = _claimedEpochs[_promotionId][_user]; + uint256 _epochIdsLength = _epochIds.length; - for (uint256 index = 0; index < _epochIds.length; index++) { + for (uint256 index = 0; index < _epochIdsLength; index++) { uint256 _epochId = _epochIds[index]; require( @@ -212,9 +213,11 @@ contract TwabRewards is ITwabRewards { uint256[] calldata _epochIds ) external view override returns (uint256[] memory) { Promotion memory _promotion = _getPromotion(_promotionId); - uint256[] memory _rewardsAmount = new uint256[](_epochIds.length); - for (uint256 index = 0; index < _epochIds.length; index++) { + uint256 _epochIdsLength = _epochIds.length; + uint256[] memory _rewardsAmount = new uint256[](_epochIdsLength); + + for (uint256 index = 0; index < _epochIdsLength; index++) { _rewardsAmount[index] = _calculateRewardAmount(_user, _promotion, _epochIds[index]); } From 88ee7ca6891f164f7b5948fa1ee390e6334cde0f Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 10 Dec 2021 11:22:23 -0600 Subject: [PATCH 08/55] fix(TwabRewards): simplify _requirePromotionActive --- contracts/TwabRewards.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 6280229..9bb5e79 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -255,7 +255,7 @@ contract TwabRewards is ITwabRewards { (_promotion.epochDuration * _promotion.numberOfEpochs); require( - _promotionEndTimestamp > 0 && _promotionEndTimestamp >= block.timestamp, + _promotionEndTimestamp > block.timestamp, "TwabRewards/promotion-inactive" ); } From 06d4c33602782ff6ed1910c5a182c34f33aeb03a Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 10 Dec 2021 11:51:47 -0600 Subject: [PATCH 09/55] fix(TwabRewards): check simple params first --- contracts/TwabRewards.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 9bb5e79..141fe3c 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -122,10 +122,10 @@ contract TwabRewards is ITwabRewards { onlyPromotionCreator(_promotionId) returns (bool) { - Promotion memory _promotion = _getPromotion(_promotionId); + require(_to != address(0), "TwabRewards/payee-not-zero-addr"); + Promotion memory _promotion = _getPromotion(_promotionId); _requirePromotionActive(_promotion); - require(_to != address(0), "TwabRewards/payee-not-zero-addr"); uint256 _remainingRewards = _getRemainingRewards(_promotion); From d7192add4e9fed464165e08936cb50cb52084f86 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 10 Dec 2021 16:19:08 -0600 Subject: [PATCH 10/55] fix(TwabRewards): check createPromotion amount --- contracts/TwabRewards.sol | 20 +++++----- test/TwabRewards.test.ts | 80 ++++++++++++++++++++++++--------------- 2 files changed, 61 insertions(+), 39 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 141fe3c..dd3323d 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -16,6 +16,7 @@ import "./interfaces/ITwabRewards.sol"; * In order to calculate user rewards, we use the TWAB (Time-Weighted Average Balance) from the Ticket contract. * This way, users simply need to hold their tickets to be eligible to claim rewards. * Rewards are calculated based on the average amount of tickets they hold during the epoch duration. + * @dev This contract does not support the use of fee on transfer tokens. */ contract TwabRewards is ITwabRewards { using SafeERC20 for IERC20; @@ -108,7 +109,14 @@ contract TwabRewards is ITwabRewards { _numberOfEpochs ); - _token.safeTransferFrom(msg.sender, address(this), _tokensPerEpoch * _numberOfEpochs); + uint256 _beforeBalance = _token.balanceOf(address(this)); + + uint256 _amount = _tokensPerEpoch * _numberOfEpochs; + _token.safeTransferFrom(msg.sender, address(this), _amount); + + uint256 _afterBalance = _token.balanceOf(address(this)); + + require(_beforeBalance + _amount == _afterBalance, "TwabRewards/promo-amount-diff"); emit PromotionCreated(_nextPromotionId); @@ -173,10 +181,7 @@ contract TwabRewards is ITwabRewards { for (uint256 index = 0; index < _epochIdsLength; index++) { uint256 _epochId = _epochIds[index]; - require( - !_isClaimedEpoch(_userClaimedEpochs, _epochId), - "TwabRewards/rewards-claimed" - ); + require(!_isClaimedEpoch(_userClaimedEpochs, _epochId), "TwabRewards/rewards-claimed"); _rewardsAmount += _calculateRewardAmount(_user, _promotion, _epochId); _userClaimedEpochs = _updateClaimedEpoch(_userClaimedEpochs, _epochId); @@ -254,10 +259,7 @@ contract TwabRewards is ITwabRewards { uint256 _promotionEndTimestamp = _promotion.startTimestamp + (_promotion.epochDuration * _promotion.numberOfEpochs); - require( - _promotionEndTimestamp > block.timestamp, - "TwabRewards/promotion-inactive" - ); + require(_promotionEndTimestamp > block.timestamp, "TwabRewards/promotion-inactive"); } /** diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index d97621d..393b8d1 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -1,8 +1,9 @@ +import ERC20MintableInterface from '@pooltogether/v4-core/abis/ERC20Mintable.json'; import TicketInterface from '@pooltogether/v4-core/abis/ITicket.json'; import { deployMockContract, MockContract } from '@ethereum-waffle/mock-contract'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { Contract, ContractFactory, Signer } from 'ethers'; +import { Contract, ContractFactory } from 'ethers'; import { ethers } from 'hardhat'; import { increaseTime as increaseTimeUtil } from './utils/increaseTime'; @@ -27,6 +28,7 @@ describe('TwabRewards', () => { let ticket: Contract; let twabRewards: Contract; + let mockRewardToken: MockContract; let mockTicket: MockContract; let createPromotionTimestamp: number; @@ -46,6 +48,7 @@ describe('TwabRewards', () => { ticket = await ticketFactory.deploy('Ticket', 'TICK', 18, wallet1.address); + mockRewardToken = await deployMockContract(wallet1, ERC20MintableInterface); mockTicket = await deployMockContract(wallet1, TicketInterface); }); @@ -55,12 +58,23 @@ describe('TwabRewards', () => { const promotionAmount = tokensPerEpoch.mul(numberOfEpochs); const createPromotion = async ( - ticketAddress: string, + ticketAddress: string = ticket.address, epochsNumber: number = numberOfEpochs, + token: Contract | MockContract = rewardToken, startTimestamp?: number, ) => { - await rewardToken.mint(wallet1.address, promotionAmount); - await rewardToken.approve(twabRewards.address, promotionAmount); + if (token.mock) { + await token.mock.transferFrom + .withArgs(wallet1.address, twabRewards.address, promotionAmount) + .returns(promotionAmount); + + await token.mock.balanceOf + .withArgs(twabRewards.address) + .returns(promotionAmount.sub(toWei('1'))); + } else { + await token.mint(wallet1.address, promotionAmount); + await token.approve(twabRewards.address, promotionAmount); + } if (startTimestamp) { createPromotionTimestamp = startTimestamp; @@ -70,7 +84,7 @@ describe('TwabRewards', () => { return await twabRewards.createPromotion( ticketAddress, - rewardToken.address, + token.address, createPromotionTimestamp, tokensPerEpoch, epochDuration, @@ -82,7 +96,7 @@ describe('TwabRewards', () => { it('should create a new promotion', async () => { const promotionId = 1; - await expect(createPromotion(ticket.address)) + await expect(createPromotion()) .to.emit(twabRewards, 'PromotionCreated') .withArgs(promotionId); @@ -101,7 +115,7 @@ describe('TwabRewards', () => { const promotionIdOne = 1; const promotionIdTwo = 2; - await expect(createPromotion(ticket.address)) + await expect(createPromotion()) .to.emit(twabRewards, 'PromotionCreated') .withArgs(promotionIdOne); @@ -115,7 +129,7 @@ describe('TwabRewards', () => { expect(firstPromotion.epochDuration).to.equal(epochDuration); expect(firstPromotion.numberOfEpochs).to.equal(numberOfEpochs); - await expect(createPromotion(ticket.address)) + await expect(createPromotion()) .to.emit(twabRewards, 'PromotionCreated') .withArgs(promotionIdTwo); @@ -133,11 +147,17 @@ describe('TwabRewards', () => { it('should succeed to create a new promotion even if start timestamp is before block timestamp', async () => { const startTimestamp = (await ethers.provider.getBlock('latest')).timestamp - 1; - await expect(createPromotion(ticket.address, numberOfEpochs, startTimestamp)) + await expect(createPromotion(ticket.address, numberOfEpochs, rewardToken, startTimestamp)) .to.emit(twabRewards, 'PromotionCreated') .withArgs(1); }); + it('should fail to create a new promotion if reward token is a fee on transfer token', async () => { + await expect( + createPromotion(ticket.address, numberOfEpochs, mockRewardToken), + ).to.be.revertedWith('TwabRewards/promo-amount-diff'); + }); + it('should fail to create a new promotion if ticket is address zero', async () => { await expect(createPromotion(AddressZero)).to.be.revertedWith( 'TwabRewards/ticket-not-zero-addr', @@ -162,7 +182,7 @@ describe('TwabRewards', () => { for (let index = 0; index < numberOfEpochs; index++) { let promotionId = index + 1; - await createPromotion(ticket.address); + await createPromotion(); const { epochDuration, numberOfEpochs, tokensPerEpoch } = await twabRewards.callStatic.getPromotion(promotionId); @@ -187,7 +207,7 @@ describe('TwabRewards', () => { }); it('should fail to cancel promotion if not owner', async () => { - await createPromotion(ticket.address); + await createPromotion(); await expect( twabRewards.connect(wallet2).cancelPromotion(1, AddressZero), @@ -195,7 +215,7 @@ describe('TwabRewards', () => { }); it('should fail to cancel an inactive promotion', async () => { - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 13); await expect(twabRewards.cancelPromotion(1, wallet1.address)).to.be.revertedWith( @@ -210,7 +230,7 @@ describe('TwabRewards', () => { }); it('should fail to cancel promotion if recipient is address zero', async () => { - await createPromotion(ticket.address); + await createPromotion(); await expect(twabRewards.cancelPromotion(1, AddressZero)).to.be.revertedWith( 'TwabRewards/payee-not-zero-addr', @@ -220,7 +240,7 @@ describe('TwabRewards', () => { describe('extendPromotion()', async () => { it('should extend a promotion', async () => { - await createPromotion(ticket.address); + await createPromotion(); const numberOfEpochsAdded = 6; const extendedPromotionAmount = tokensPerEpoch.mul(numberOfEpochsAdded); @@ -246,7 +266,7 @@ describe('TwabRewards', () => { }); it('should fail to extend an inactive promotion', async () => { - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 13); await expect(twabRewards.extendPromotion(1, 6)).to.be.revertedWith( @@ -261,7 +281,7 @@ describe('TwabRewards', () => { }); it('should fail to extend a promotion over the epochs limit', async () => { - await createPromotion(ticket.address); + await createPromotion(); await expect(twabRewards.extendPromotion(1, 244)).to.be.reverted; }); @@ -269,7 +289,7 @@ describe('TwabRewards', () => { describe('getPromotion()', async () => { it('should get promotion by id', async () => { - await createPromotion(ticket.address); + await createPromotion(); const promotion = await twabRewards.callStatic.getPromotion(1); @@ -291,7 +311,7 @@ describe('TwabRewards', () => { describe('getRemainingRewards()', async () => { it('should return the correct amount of reward tokens left', async () => { - await createPromotion(ticket.address); + await createPromotion(); const promotionId = 1; const { epochDuration, numberOfEpochs, tokensPerEpoch } = @@ -317,7 +337,7 @@ describe('TwabRewards', () => { describe('getCurrentEpochId()', async () => { it('should get the current epoch id of a promotion', async () => { - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 3); expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(3); @@ -351,7 +371,7 @@ describe('TwabRewards', () => { await ticket.mint(wallet3.address, wallet3Amount); await ticket.connect(wallet3).delegate(wallet3.address); - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 3); expect( @@ -395,7 +415,7 @@ describe('TwabRewards', () => { const timestampBeforeCreate = (await ethers.provider.getBlock('latest')).timestamp; - await createPromotion(ticket.address); + await createPromotion(); const timestampAfterCreate = (await ethers.provider.getBlock('latest')).timestamp; const elapsedTimeCreate = timestampAfterCreate - timestampBeforeCreate; @@ -434,7 +454,7 @@ describe('TwabRewards', () => { await ticket.mint(wallet2.address, wallet2Amount); - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 3); expect( @@ -445,7 +465,7 @@ describe('TwabRewards', () => { it('should return 0 if ticket average total supplies is 0', async () => { const zeroAmount = toWei('0'); - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 3); expect( @@ -460,7 +480,7 @@ describe('TwabRewards', () => { await ticket.mint(wallet2.address, wallet2Amount); await ticket.mint(wallet3.address, wallet3Amount); - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 3); await expect( @@ -498,7 +518,7 @@ describe('TwabRewards', () => { await ticket.mint(wallet3.address, wallet3Amount); await ticket.connect(wallet3).delegate(wallet3.address); - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 3); await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) @@ -546,7 +566,7 @@ describe('TwabRewards', () => { await ticket.mint(wallet3.address, wallet3Amount); await ticket.connect(wallet3).delegate(wallet3.address); - await createPromotion(ticket.address); + await createPromotion(); // We adjust time to delegate right in the middle of epoch 3 await increaseTime(epochDuration * 2 + halfEpoch - 2); @@ -580,7 +600,7 @@ describe('TwabRewards', () => { await ticket.mint(wallet2.address, wallet2Amount); - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 3); await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) @@ -595,7 +615,7 @@ describe('TwabRewards', () => { const epochIds = ['0', '1', '2']; const zeroAmount = toWei('0'); - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 3); await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) @@ -616,7 +636,7 @@ describe('TwabRewards', () => { await ticket.mint(wallet2.address, wallet2Amount); await ticket.mint(wallet3.address, wallet3Amount); - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 3); await expect( @@ -633,7 +653,7 @@ describe('TwabRewards', () => { await ticket.mint(wallet2.address, wallet2Amount); await ticket.mint(wallet3.address, wallet3Amount); - await createPromotion(ticket.address); + await createPromotion(); await increaseTime(epochDuration * 3); await twabRewards.claimRewards(wallet2.address, promotionId, ['0', '1', '2']); From 2f847e3d96f31637abe67ca583cfe95b094aa32e Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 10 Dec 2021 17:04:33 -0600 Subject: [PATCH 11/55] fix(TwabRewards): cancel but don't delete promotion --- contracts/TwabRewards.sol | 4 +-- test/TwabRewards.test.ts | 67 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index dd3323d..47e24e2 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -135,9 +135,9 @@ contract TwabRewards is ITwabRewards { Promotion memory _promotion = _getPromotion(_promotionId); _requirePromotionActive(_promotion); - uint256 _remainingRewards = _getRemainingRewards(_promotion); + _promotions[_promotionId].numberOfEpochs = uint8(_getCurrentEpochId(_promotion)); - delete _promotions[_promotionId]; + uint256 _remainingRewards = _getRemainingRewards(_promotion); _promotion.token.safeTransfer(_to, _remainingRewards); emit PromotionCancelled(_promotionId, _remainingRewards); diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 393b8d1..db190bf 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -200,12 +200,67 @@ describe('TwabRewards', () => { .withArgs(promotionId, transferredAmount); expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); + expect( + (await twabRewards.callStatic.getPromotion(promotionId)).numberOfEpochs, + ).to.equal(await twabRewards.callStatic.getCurrentEpochId(promotionId)); // We burn tokens from wallet1 to reset balance await rewardToken.burn(wallet1.address, transferredAmount); } }); + it('should cancel promotion and still allow users to claim their rewards', async () => { + const promotionId = 1; + const epochNumber = 6; + const epochIds = ['0', '1', '2', '3', '4', '5']; + + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + const totalAmount = wallet2Amount.add(wallet3Amount); + + const wallet2ShareOfTickets = wallet2Amount.mul(100).div(totalAmount); + const wallet2RewardAmount = wallet2ShareOfTickets.mul(tokensPerEpoch).div(100); + const wallet2TotalRewardsAmount = wallet2RewardAmount.mul(epochNumber); + + const wallet3ShareOfTickets = wallet3Amount.mul(100).div(totalAmount); + const wallet3RewardAmount = wallet3ShareOfTickets.mul(tokensPerEpoch).div(100); + const wallet3TotalRewardsAmount = wallet3RewardAmount.mul(epochNumber); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.connect(wallet2).delegate(wallet2.address); + await ticket.mint(wallet3.address, wallet3Amount); + await ticket.connect(wallet3).delegate(wallet3.address); + + await createPromotion(); + await increaseTime(epochDuration * epochNumber); + + const transferredAmount = tokensPerEpoch + .mul(numberOfEpochs) + .sub(tokensPerEpoch.mul(epochNumber)); + + await expect(twabRewards.cancelPromotion(promotionId, wallet1.address)) + .to.emit(twabRewards, 'PromotionCancelled') + .withArgs(promotionId, transferredAmount); + + expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); + expect( + (await twabRewards.callStatic.getPromotion(promotionId)).numberOfEpochs, + ).to.equal(await twabRewards.callStatic.getCurrentEpochId(promotionId)); + + await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) + .to.emit(twabRewards, 'RewardsClaimed') + .withArgs(promotionId, epochIds, wallet2.address, wallet2TotalRewardsAmount); + + expect(await rewardToken.balanceOf(wallet2.address)).to.equal( + wallet2TotalRewardsAmount, + ); + + expect(await rewardToken.balanceOf(twabRewards.address)).to.equal( + wallet3TotalRewardsAmount, + ); + }); + it('should fail to cancel promotion if not owner', async () => { await createPromotion(); @@ -498,6 +553,7 @@ describe('TwabRewards', () => { describe('claimRewards()', async () => { it('should claim rewards for one or more epochs', async () => { const promotionId = 1; + const epochNumber = 3; const epochIds = ['0', '1', '2']; const wallet2Amount = toWei('750'); @@ -507,11 +563,11 @@ describe('TwabRewards', () => { const wallet2ShareOfTickets = wallet2Amount.mul(100).div(totalAmount); const wallet2RewardAmount = wallet2ShareOfTickets.mul(tokensPerEpoch).div(100); - const wallet2TotalRewardsAmount = wallet2RewardAmount.mul(3); + const wallet2TotalRewardsAmount = wallet2RewardAmount.mul(epochNumber); const wallet3ShareOfTickets = wallet3Amount.mul(100).div(totalAmount); const wallet3RewardAmount = wallet3ShareOfTickets.mul(tokensPerEpoch).div(100); - const wallet3TotalRewardsAmount = wallet3RewardAmount.mul(3); + const wallet3TotalRewardsAmount = wallet3RewardAmount.mul(epochNumber); await ticket.mint(wallet2.address, wallet2Amount); await ticket.connect(wallet2).delegate(wallet2.address); @@ -519,7 +575,7 @@ describe('TwabRewards', () => { await ticket.connect(wallet3).delegate(wallet3.address); await createPromotion(); - await increaseTime(epochDuration * 3); + await increaseTime(epochDuration * epochNumber); await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) .to.emit(twabRewards, 'RewardsClaimed') @@ -540,6 +596,7 @@ describe('TwabRewards', () => { it('should decrease rewards amount claimed if user delegate in the middle of an epoch', async () => { const promotionId = 1; + const epochNumber = 3; const epochIds = ['0', '1', '2']; const halfEpoch = epochDuration / 2; @@ -552,13 +609,13 @@ describe('TwabRewards', () => { const wallet3RewardAmount = wallet3ShareOfTickets.mul(tokensPerEpoch).div(100); const wallet3HalfRewardAmount = wallet3RewardAmount.div(2); const wallet3TotalRewardsAmount = wallet3RewardAmount - .mul(3) + .mul(epochNumber) .sub(wallet3HalfRewardAmount); const wallet2ShareOfTickets = wallet2Amount.mul(100).div(totalAmount); const wallet2RewardAmount = wallet2ShareOfTickets.mul(tokensPerEpoch).div(100); const wallet2TotalRewardsAmount = wallet2RewardAmount - .mul(3) + .mul(epochNumber) .add(wallet3HalfRewardAmount); await ticket.mint(wallet2.address, wallet2Amount); From b49d7fa8f3776078da550fa47146cd63a7d40eb0 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 13 Dec 2021 17:00:46 -0600 Subject: [PATCH 12/55] fix(TwabRewards): pass recipient to PromotionCancelled --- contracts/TwabRewards.sol | 11 ++++++++--- test/TwabRewards.test.ts | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 47e24e2..9b968b1 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -46,9 +46,14 @@ contract TwabRewards is ITwabRewards { /** @notice Emitted when a promotion is cancelled. @param promotionId Id of the promotion being cancelled - @param amount Amount of tokens transferred to the promotion creator + @param recipient Address of the recipient that will receive the remaining rewards + @param amount Amount of tokens transferred to the recipient */ - event PromotionCancelled(uint256 indexed promotionId, uint256 amount); + event PromotionCancelled( + uint256 indexed promotionId, + address indexed recipient, + uint256 amount + ); /** @notice Emitted when a promotion is extended. @@ -140,7 +145,7 @@ contract TwabRewards is ITwabRewards { uint256 _remainingRewards = _getRemainingRewards(_promotion); _promotion.token.safeTransfer(_to, _remainingRewards); - emit PromotionCancelled(_promotionId, _remainingRewards); + emit PromotionCancelled(_promotionId, _to, _remainingRewards); return true; } diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index db190bf..a31d881 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -197,7 +197,7 @@ describe('TwabRewards', () => { await expect(twabRewards.cancelPromotion(promotionId, wallet1.address)) .to.emit(twabRewards, 'PromotionCancelled') - .withArgs(promotionId, transferredAmount); + .withArgs(promotionId, wallet1.address, transferredAmount); expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); expect( @@ -241,7 +241,7 @@ describe('TwabRewards', () => { await expect(twabRewards.cancelPromotion(promotionId, wallet1.address)) .to.emit(twabRewards, 'PromotionCancelled') - .withArgs(promotionId, transferredAmount); + .withArgs(promotionId, wallet1.address, transferredAmount); expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); expect( From fb9105ec92d6031cca83806aa311560fc16345d6 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 13 Dec 2021 18:12:20 -0600 Subject: [PATCH 13/55] fix(TwabRewards): check create and cancel inputs --- contracts/TwabRewards.sol | 14 +++++++++- test/TwabRewards.test.ts | 56 ++++++++++++++++++++++++++++++++------- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 9b968b1..33000d1 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -99,6 +99,9 @@ contract TwabRewards is ITwabRewards { uint56 _epochDuration, uint8 _numberOfEpochs ) external override returns (uint256) { + require(_tokensPerEpoch > 0, "TwabRewards/tokens-not-zero"); + require(_epochDuration > 0, "TwabRewards/duration-not-zero"); + _requireNumberOfEpochs(_numberOfEpochs); _requireTicket(_ticket); uint256 _nextPromotionId = _latestPromotionId + 1; @@ -156,8 +159,9 @@ contract TwabRewards is ITwabRewards { override returns (bool) { - Promotion memory _promotion = _getPromotion(_promotionId); + _requireNumberOfEpochs(_numberOfEpochs); + Promotion memory _promotion = _getPromotion(_promotionId); _requirePromotionActive(_promotion); uint8 _extendedNumberOfEpochs = _promotion.numberOfEpochs + _numberOfEpochs; @@ -256,6 +260,14 @@ contract TwabRewards is ITwabRewards { require(succeeded && controllerAddress != address(0), "TwabRewards/invalid-ticket"); } + /** + @notice Allow a promotion to be created or extended only by a positive number of epochs. + @param _numberOfEpochs Number of epochs to check + */ + function _requireNumberOfEpochs(uint8 _numberOfEpochs) internal view { + require(_numberOfEpochs > 0, "TwabRewards/epochs-not-zero"); + } + /** @notice Determine if a promotion is active. @param _promotion Promotion to check diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index a31d881..4ac6217 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -3,7 +3,7 @@ import TicketInterface from '@pooltogether/v4-core/abis/ITicket.json'; import { deployMockContract, MockContract } from '@ethereum-waffle/mock-contract'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { Contract, ContractFactory } from 'ethers'; +import { BigNumber, Contract, ContractFactory } from 'ethers'; import { ethers } from 'hardhat'; import { increaseTime as increaseTimeUtil } from './utils/increaseTime'; @@ -44,7 +44,6 @@ describe('TwabRewards', () => { beforeEach(async () => { rewardToken = await erc20MintableFactory.deploy('Reward', 'REWA'); twabRewards = await twabRewardsFactory.deploy(); - ticket = await erc20MintableFactory.deploy('Ticket', 'TICK'); ticket = await ticketFactory.deploy('Ticket', 'TICK', 18, wallet1.address); @@ -59,8 +58,10 @@ describe('TwabRewards', () => { const createPromotion = async ( ticketAddress: string = ticket.address, - epochsNumber: number = numberOfEpochs, token: Contract | MockContract = rewardToken, + epochTokens: BigNumber = tokensPerEpoch, + epochTimestamp: number = epochDuration, + epochsNumber: number = numberOfEpochs, startTimestamp?: number, ) => { if (token.mock) { @@ -86,8 +87,8 @@ describe('TwabRewards', () => { ticketAddress, token.address, createPromotionTimestamp, - tokensPerEpoch, - epochDuration, + epochTokens, + epochTimestamp, epochsNumber, ); }; @@ -147,15 +148,24 @@ describe('TwabRewards', () => { it('should succeed to create a new promotion even if start timestamp is before block timestamp', async () => { const startTimestamp = (await ethers.provider.getBlock('latest')).timestamp - 1; - await expect(createPromotion(ticket.address, numberOfEpochs, rewardToken, startTimestamp)) + await expect( + createPromotion( + ticket.address, + rewardToken, + tokensPerEpoch, + epochDuration, + numberOfEpochs, + startTimestamp, + ), + ) .to.emit(twabRewards, 'PromotionCreated') .withArgs(1); }); it('should fail to create a new promotion if reward token is a fee on transfer token', async () => { - await expect( - createPromotion(ticket.address, numberOfEpochs, mockRewardToken), - ).to.be.revertedWith('TwabRewards/promo-amount-diff'); + await expect(createPromotion(ticket.address, mockRewardToken)).to.be.revertedWith( + 'TwabRewards/promo-amount-diff', + ); }); it('should fail to create a new promotion if ticket is address zero', async () => { @@ -172,8 +182,28 @@ describe('TwabRewards', () => { ); }); + it('should fail to create a new promotion if tokens per epoch is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, toWei('0')), + ).to.be.revertedWith('TwabRewards/tokens-not-zero'); + }); + + it('should fail to create a new promotion if epoch duration is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, tokensPerEpoch, 0), + ).to.be.revertedWith('TwabRewards/duration-not-zero'); + }); + + it('should fail to create a new promotion if number of epochs is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 0), + ).to.be.revertedWith('TwabRewards/epochs-not-zero'); + }); + it('should fail to create a new promotion if number of epochs exceeds limit', async () => { - await expect(createPromotion(ticket.address, 256)).to.be.reverted; + await expect( + createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 256), + ).to.be.reverted; }); }); @@ -329,6 +359,12 @@ describe('TwabRewards', () => { ); }); + it('should fail to extend a promotion by zero epochs', async () => { + await expect(twabRewards.extendPromotion(1, 0)).to.be.revertedWith( + 'TwabRewards/epochs-not-zero', + ); + }); + it('should fail to extend an inexistent promotion', async () => { await expect(twabRewards.extendPromotion(1, 6)).to.be.revertedWith( 'TwabRewards/invalid-promotion', From 15e0625c00ac31e14e7bd25c45daf7cbd1e727c9 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 13 Dec 2021 18:33:13 -0600 Subject: [PATCH 14/55] fix(TwabRewards): fix epoch end in _calculateRewardAmount --- contracts/TwabRewards.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 33000d1..8aa5844 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -319,7 +319,7 @@ contract TwabRewards is ITwabRewards { uint256 _epochStartTimestamp = _promotion.startTimestamp + (_epochDuration * _epochId); uint256 _epochEndTimestamp = _epochStartTimestamp + _epochDuration; - require(block.timestamp > _epochEndTimestamp, "TwabRewards/epoch-not-over"); + require(block.timestamp >= _epochEndTimestamp, "TwabRewards/epoch-not-over"); ITicket _ticket = ITicket(_promotion.ticket); From a7212de200757e07f807b226a3dd08669904b442 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 13 Dec 2021 18:49:41 -0600 Subject: [PATCH 15/55] fix(TwabRewards): cast timestamps to uint64 --- contracts/TwabRewards.sol | 26 +++++++++++++------------- contracts/interfaces/ITwabRewards.sol | 4 ++-- test/TwabRewards.test.ts | 14 +++++++------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 8aa5844..5df8f51 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -71,7 +71,7 @@ contract TwabRewards is ITwabRewards { */ event RewardsClaimed( uint256 indexed promotionId, - uint256[] epochIds, + uint8[] epochIds, address indexed user, uint256 amount ); @@ -179,7 +179,7 @@ contract TwabRewards is ITwabRewards { function claimRewards( address _user, uint256 _promotionId, - uint256[] calldata _epochIds + uint8[] calldata _epochIds ) external override returns (uint256) { Promotion memory _promotion = _getPromotion(_promotionId); @@ -188,7 +188,7 @@ contract TwabRewards is ITwabRewards { uint256 _epochIdsLength = _epochIds.length; for (uint256 index = 0; index < _epochIdsLength; index++) { - uint256 _epochId = _epochIds[index]; + uint8 _epochId = _epochIds[index]; require(!_isClaimedEpoch(_userClaimedEpochs, _epochId), "TwabRewards/rewards-claimed"); @@ -224,7 +224,7 @@ contract TwabRewards is ITwabRewards { function getRewardsAmount( address _user, uint256 _promotionId, - uint256[] calldata _epochIds + uint8[] calldata _epochIds ) external view override returns (uint256[] memory) { Promotion memory _promotion = _getPromotion(_promotionId); @@ -264,7 +264,7 @@ contract TwabRewards is ITwabRewards { @notice Allow a promotion to be created or extended only by a positive number of epochs. @param _numberOfEpochs Number of epochs to check */ - function _requireNumberOfEpochs(uint8 _numberOfEpochs) internal view { + function _requireNumberOfEpochs(uint8 _numberOfEpochs) internal pure { require(_numberOfEpochs > 0, "TwabRewards/epochs-not-zero"); } @@ -313,11 +313,11 @@ contract TwabRewards is ITwabRewards { function _calculateRewardAmount( address _user, Promotion memory _promotion, - uint256 _epochId + uint8 _epochId ) internal view returns (uint256) { - uint256 _epochDuration = _promotion.epochDuration; - uint256 _epochStartTimestamp = _promotion.startTimestamp + (_epochDuration * _epochId); - uint256 _epochEndTimestamp = _epochStartTimestamp + _epochDuration; + uint56 _epochDuration = _promotion.epochDuration; + uint64 _epochStartTimestamp = uint64(_promotion.startTimestamp) + (_epochDuration * _epochId); + uint64 _epochEndTimestamp = _epochStartTimestamp + _epochDuration; require(block.timestamp >= _epochEndTimestamp, "TwabRewards/epoch-not-over"); @@ -330,10 +330,10 @@ contract TwabRewards is ITwabRewards { ); uint64[] memory _epochStartTimestamps = new uint64[](1); - _epochStartTimestamps[0] = uint64(_epochStartTimestamp); + _epochStartTimestamps[0] = _epochStartTimestamp; uint64[] memory _epochEndTimestamps = new uint64[](1); - _epochEndTimestamps[0] = uint64(_epochEndTimestamp); + _epochEndTimestamps[0] = _epochEndTimestamp; uint256[] memory _averageTotalSupplies = _ticket.getAverageTotalSuppliesBetween( _epochStartTimestamps, @@ -371,7 +371,7 @@ contract TwabRewards is ITwabRewards { @param _epochId Id of the epoch to set the boolean for @return Tightly packed epoch ids with the newly boolean value set */ - function _updateClaimedEpoch(uint256 _userClaimedEpochs, uint256 _epochId) + function _updateClaimedEpoch(uint256 _userClaimedEpochs, uint8 _epochId) internal pure returns (uint256) @@ -392,7 +392,7 @@ contract TwabRewards is ITwabRewards { @param _epochId Epoch id to check @return true if the rewards have already been claimed for the given epoch, false otherwise */ - function _isClaimedEpoch(uint256 _userClaimedEpochs, uint256 _epochId) + function _isClaimedEpoch(uint256 _userClaimedEpochs, uint8 _epochId) internal pure returns (bool) diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index fcc6071..88142c2 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -80,7 +80,7 @@ interface ITwabRewards { function claimRewards( address _user, uint256 _promotionId, - uint256[] calldata _epochIds + uint8[] calldata _epochIds ) external returns (uint256); /** @@ -116,6 +116,6 @@ interface ITwabRewards { function getRewardsAmount( address _user, uint256 _promotionId, - uint256[] calldata _epochIds + uint8[] calldata _epochIds ) external view returns (uint256[] memory); } diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 4ac6217..ad57664 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -242,7 +242,7 @@ describe('TwabRewards', () => { it('should cancel promotion and still allow users to claim their rewards', async () => { const promotionId = 1; const epochNumber = 6; - const epochIds = ['0', '1', '2', '3', '4', '5']; + const epochIds = [0, 1, 2, 3, 4, 5]; const wallet2Amount = toWei('750'); const wallet3Amount = toWei('250'); @@ -444,7 +444,7 @@ describe('TwabRewards', () => { describe('getRewardsAmount()', async () => { it('should get rewards amount for one or more epochs', async () => { const promotionId = 1; - const epochIds = ['0', '1', '2']; + const epochIds = [0, 1, 2]; const wallet2Amount = toWei('750'); const wallet3Amount = toWei('250'); @@ -484,7 +484,7 @@ describe('TwabRewards', () => { it('should decrease rewards amount if user delegate in the middle of an epoch', async () => { const promotionId = 1; - const epochIds = ['0', '1', '2']; + const epochIds = [0, 1, 2]; const halfEpoch = epochDuration / 2; const wallet2Amount = toWei('750'); @@ -590,7 +590,7 @@ describe('TwabRewards', () => { it('should claim rewards for one or more epochs', async () => { const promotionId = 1; const epochNumber = 3; - const epochIds = ['0', '1', '2']; + const epochIds = [0, 1, 2]; const wallet2Amount = toWei('750'); const wallet3Amount = toWei('250'); @@ -633,7 +633,7 @@ describe('TwabRewards', () => { it('should decrease rewards amount claimed if user delegate in the middle of an epoch', async () => { const promotionId = 1; const epochNumber = 3; - const epochIds = ['0', '1', '2']; + const epochIds = [0, 1, 2]; const halfEpoch = epochDuration / 2; const wallet2Amount = toWei('750'); @@ -687,7 +687,7 @@ describe('TwabRewards', () => { it('should claim 0 rewards if user has no tickets delegated to him', async () => { const promotionId = 1; - const epochIds = ['0', '1', '2']; + const epochIds = [0, 1, 2]; const wallet2Amount = toWei('750'); const zeroAmount = toWei('0'); @@ -705,7 +705,7 @@ describe('TwabRewards', () => { it('should return 0 if ticket average total supplies is 0', async () => { const promotionId = 1; - const epochIds = ['0', '1', '2']; + const epochIds = [0, 1, 2]; const zeroAmount = toWei('0'); await createPromotion(); From 32f4d8426a55ff28a434cd14197cbc8b97146186 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 14 Dec 2021 09:57:04 -0600 Subject: [PATCH 16/55] fix(TwabRewards): remove onlyPromotionCreator modifier --- contracts/TwabRewards.sol | 20 ++------------------ test/TwabRewards.test.ts | 2 +- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 5df8f51..99380dd 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -76,18 +76,6 @@ contract TwabRewards is ITwabRewards { uint256 amount ); - /* ============ Modifiers ============ */ - - /// @dev Ensure that the caller is the creator of the promotion. - /// @param _promotionId Id of the promotion to check - modifier onlyPromotionCreator(uint256 _promotionId) { - require( - msg.sender == _getPromotion(_promotionId).creator, - "TwabRewards/only-promo-creator" - ); - _; - } - /* ============ External Functions ============ */ /// @inheritdoc ITwabRewards @@ -132,15 +120,11 @@ contract TwabRewards is ITwabRewards { } /// @inheritdoc ITwabRewards - function cancelPromotion(uint256 _promotionId, address _to) - external - override - onlyPromotionCreator(_promotionId) - returns (bool) - { + function cancelPromotion(uint256 _promotionId, address _to) external override returns (bool) { require(_to != address(0), "TwabRewards/payee-not-zero-addr"); Promotion memory _promotion = _getPromotion(_promotionId); + require(msg.sender == _promotion.creator, "TwabRewards/only-promo-creator"); _requirePromotionActive(_promotion); _promotions[_promotionId].numberOfEpochs = uint8(_getCurrentEpochId(_promotion)); diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index ad57664..fa6950c 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -295,7 +295,7 @@ describe('TwabRewards', () => { await createPromotion(); await expect( - twabRewards.connect(wallet2).cancelPromotion(1, AddressZero), + twabRewards.connect(wallet2).cancelPromotion(1, wallet1.address), ).to.be.revertedWith('TwabRewards/only-promo-creator'); }); From bfdae2de58398b252b10a51a3563a8198995a9c9 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 14 Dec 2021 10:13:11 -0600 Subject: [PATCH 17/55] fix(TwabRewards): simplify _requireTicket --- contracts/TwabRewards.sol | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 99380dd..a0abb5c 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -231,17 +231,14 @@ contract TwabRewards is ITwabRewards { function _requireTicket(address _ticket) internal view { require(_ticket != address(0), "TwabRewards/ticket-not-zero-addr"); - (bool succeeded, bytes memory data) = address(_ticket).staticcall( + (bool succeeded, bytes memory data) = _ticket.staticcall( abi.encodePacked(ITicket(_ticket).controller.selector) ); - address controllerAddress; - - if (data.length > 0) { - controllerAddress = abi.decode(data, (address)); - } - - require(succeeded && controllerAddress != address(0), "TwabRewards/invalid-ticket"); + require( + succeeded && data.length > 0 && abi.decode(data, (uint160)) != 0, + "TwabRewards/invalid-ticket" + ); } /** From e2780aa28d26df22b4933e4015e95e3573ae901f Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 14 Dec 2021 10:19:10 -0600 Subject: [PATCH 18/55] fix(TwabRewards): store averageTotalSupply --- contracts/TwabRewards.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index a0abb5c..682417c 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -316,13 +316,13 @@ contract TwabRewards is ITwabRewards { uint64[] memory _epochEndTimestamps = new uint64[](1); _epochEndTimestamps[0] = _epochEndTimestamp; - uint256[] memory _averageTotalSupplies = _ticket.getAverageTotalSuppliesBetween( + uint256 _averageTotalSupply = _ticket.getAverageTotalSuppliesBetween( _epochStartTimestamps, _epochEndTimestamps - ); + )[0]; - if (_averageTotalSupplies[0] > 0) { - return (_promotion.tokensPerEpoch * _averageBalance) / _averageTotalSupplies[0]; + if (_averageTotalSupply > 0) { + return (_promotion.tokensPerEpoch * _averageBalance) / _averageTotalSupply; } return 0; From 0a77e98dfef9b720cb8f4aac18364e2474a669f3 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 14 Dec 2021 10:33:01 -0600 Subject: [PATCH 19/55] fix(TwabRewards): improve _calculateRewardAmount --- contracts/TwabRewards.sol | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 682417c..20c3d6b 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -310,18 +310,18 @@ contract TwabRewards is ITwabRewards { uint64(_epochEndTimestamp) ); - uint64[] memory _epochStartTimestamps = new uint64[](1); - _epochStartTimestamps[0] = _epochStartTimestamp; + if (_averageBalance > 0) { + uint64[] memory _epochStartTimestamps = new uint64[](1); + _epochStartTimestamps[0] = _epochStartTimestamp; - uint64[] memory _epochEndTimestamps = new uint64[](1); - _epochEndTimestamps[0] = _epochEndTimestamp; + uint64[] memory _epochEndTimestamps = new uint64[](1); + _epochEndTimestamps[0] = _epochEndTimestamp; - uint256 _averageTotalSupply = _ticket.getAverageTotalSuppliesBetween( - _epochStartTimestamps, - _epochEndTimestamps - )[0]; + uint256 _averageTotalSupply = _ticket.getAverageTotalSuppliesBetween( + _epochStartTimestamps, + _epochEndTimestamps + )[0]; - if (_averageTotalSupply > 0) { return (_promotion.tokensPerEpoch * _averageBalance) / _averageTotalSupply; } From f17e6b6610312973580360754050ada22304252b Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 12 Jan 2022 14:45:15 -0600 Subject: [PATCH 20/55] fix(TwabRewards): improve extendPromotion error handling --- contracts/TwabRewards.sol | 9 ++++++++- test/TwabRewards.test.ts | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 20c3d6b..579d025 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -148,7 +148,14 @@ contract TwabRewards is ITwabRewards { Promotion memory _promotion = _getPromotion(_promotionId); _requirePromotionActive(_promotion); - uint8 _extendedNumberOfEpochs = _promotion.numberOfEpochs + _numberOfEpochs; + uint8 _currentNumberOfEpochs = _promotion.numberOfEpochs; + + require( + _numberOfEpochs < (type(uint8).max - _currentNumberOfEpochs), + "TwabRewards/epochs-over-limit" + ); + + uint8 _extendedNumberOfEpochs = _currentNumberOfEpochs + _numberOfEpochs; _promotions[_promotionId].numberOfEpochs = _extendedNumberOfEpochs; uint256 _amount = _numberOfEpochs * _promotion.tokensPerEpoch; diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index fa6950c..f40f438 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -374,7 +374,9 @@ describe('TwabRewards', () => { it('should fail to extend a promotion over the epochs limit', async () => { await createPromotion(); - await expect(twabRewards.extendPromotion(1, 244)).to.be.reverted; + await expect(twabRewards.extendPromotion(1, 244)).to.be.revertedWith( + 'TwabRewards/epochs-over-limit', + ); }); }); From a3ac58fc42b7a199566ed49186930ab1a8d2c759 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 10 Dec 2021 16:19:08 -0600 Subject: [PATCH 21/55] fix(TwabRewards): check createPromotion amount --- contracts/interfaces/ITwabRewards.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index 88142c2..99039d1 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -35,6 +35,8 @@ interface ITwabRewards { @dev For sake of simplicity, `msg.sender` will be the creator of the promotion. @dev `_latestPromotionId` starts at 0 and is incremented by 1 for each new promotion. So the first promotion will have id 1, the second 2, etc. + @dev The transaction will revert if the amount of reward tokens provided is not equal to `_tokensPerEpoch * _numberOfEpochs`. + This scenario could happen if the token supplied is a fee on transfer one. @param _ticket Prize Pool ticket address for which the promotion is created @param _token Address of the token to be distributed @param _startTimestamp Timestamp at which the promotion starts From 51e3bba69db05efea407fe1b6bb8d6881365c1b1 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 13 Dec 2021 18:12:20 -0600 Subject: [PATCH 22/55] fix(TwabRewards): check create and cancel inputs --- test/TwabRewards.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index f40f438..ff87eac 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -200,6 +200,24 @@ describe('TwabRewards', () => { ).to.be.revertedWith('TwabRewards/epochs-not-zero'); }); + it('should fail to create a new promotion if tokens per epoch is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, toWei('0')), + ).to.be.revertedWith('TwabRewards/tokens-not-zero'); + }); + + it('should fail to create a new promotion if epoch duration is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, tokensPerEpoch, 0), + ).to.be.revertedWith('TwabRewards/duration-not-zero'); + }); + + it('should fail to create a new promotion if number of epochs is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 0), + ).to.be.revertedWith('TwabRewards/epochs-not-zero'); + }); + it('should fail to create a new promotion if number of epochs exceeds limit', async () => { await expect( createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 256), From b8fda6c2650a6d49713a8ef54c69076b652a951a Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 14 Dec 2021 15:41:03 -0600 Subject: [PATCH 23/55] feat(TwabRewards): add unchecked to save on gas --- contracts/TwabRewards.sol | 48 ++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 579d025..895ebdc 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -105,9 +105,14 @@ contract TwabRewards is ITwabRewards { _numberOfEpochs ); + uint256 _amount; + + unchecked { + _amount = _tokensPerEpoch * _numberOfEpochs; + } + uint256 _beforeBalance = _token.balanceOf(address(this)); - uint256 _amount = _tokensPerEpoch * _numberOfEpochs; _token.safeTransferFrom(msg.sender, address(this), _amount); uint256 _afterBalance = _token.balanceOf(address(this)); @@ -158,8 +163,17 @@ contract TwabRewards is ITwabRewards { uint8 _extendedNumberOfEpochs = _currentNumberOfEpochs + _numberOfEpochs; _promotions[_promotionId].numberOfEpochs = _extendedNumberOfEpochs; - uint256 _amount = _numberOfEpochs * _promotion.tokensPerEpoch; - _promotion.token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _extendedAmount; + + unchecked { + _extendedAmount = _numberOfEpochs * _promotion.tokensPerEpoch; + } + + _promotion.token.safeTransferFrom( + msg.sender, + address(this), + _extendedAmount + ); emit PromotionExtended(_promotionId, _numberOfEpochs); @@ -261,10 +275,14 @@ contract TwabRewards is ITwabRewards { @param _promotion Promotion to check */ function _requirePromotionActive(Promotion memory _promotion) internal view { - uint256 _promotionEndTimestamp = _promotion.startTimestamp + - (_promotion.epochDuration * _promotion.numberOfEpochs); - - require(_promotionEndTimestamp > block.timestamp, "TwabRewards/promotion-inactive"); + unchecked { + // promotionEndTimestamp > block.timestamp + require( + (_promotion.startTimestamp + + (_promotion.epochDuration * _promotion.numberOfEpochs)) > block.timestamp, + "TwabRewards/promotion-inactive" + ); + } } /** @@ -286,8 +304,10 @@ contract TwabRewards is ITwabRewards { @return Epoch id */ function _getCurrentEpochId(Promotion memory _promotion) internal view returns (uint256) { - // elapsedTimestamp / epochDurationTimestamp - return (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; + unchecked { + // elapsedTimestamp / epochDurationTimestamp + return (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; + } } /** @@ -341,10 +361,12 @@ contract TwabRewards is ITwabRewards { @return Amount of tokens left to be rewarded */ function _getRemainingRewards(Promotion memory _promotion) internal view returns (uint256) { - // _tokensPerEpoch * _numberOfEpochsLeft - return - _promotion.tokensPerEpoch * - (_promotion.numberOfEpochs - _getCurrentEpochId(_promotion)); + unchecked { + // _tokensPerEpoch * _numberOfEpochsLeft + return + _promotion.tokensPerEpoch * + (_promotion.numberOfEpochs - _getCurrentEpochId(_promotion)); + } } /** From f0de5fdf0719e42986f5f3f3344bb3176916ce11 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 14 Dec 2021 16:43:56 -0600 Subject: [PATCH 24/55] fix(TwabRewards): fix _getCurrentEpochId value --- contracts/TwabRewards.sol | 11 ++++++++--- test/TwabRewards.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index c134471..8835fb1 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -300,14 +300,19 @@ contract TwabRewards is ITwabRewards { /** @notice Get the current epoch id of a promotion. @dev Epoch ids and their boolean values are tightly packed and stored in a uint256, so epoch id starts at 0. + @dev We calculate the current epoch id if the promotion has started. Otherwise, we return 0. @param _promotion Promotion to get current epoch for @return Epoch id */ function _getCurrentEpochId(Promotion memory _promotion) internal view returns (uint256) { - unchecked { - // elapsedTimestamp / epochDurationTimestamp - return (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; + if (block.timestamp > _promotion.startTimestamp) { + unchecked { + // elapsedTimestamp / epochDurationTimestamp + return (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; + } } + + return 0; } /** diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 2dd731d..a3a2d20 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -248,6 +248,7 @@ describe('TwabRewards', () => { .withArgs(promotionId, wallet1.address, transferredAmount); expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); + expect( (await twabRewards.callStatic.getPromotion(promotionId)).numberOfEpochs, ).to.equal(await twabRewards.callStatic.getCurrentEpochId(promotionId)); @@ -257,6 +258,30 @@ describe('TwabRewards', () => { } }); + it('should cancel a promotion before it starts and transfer the full amount of reward tokens', async () => { + const promotionId = 1; + const startTimestamp = (await ethers.provider.getBlock('latest')).timestamp + 60; + + await createPromotion( + ticket.address, + rewardToken, + tokensPerEpoch, + epochDuration, + numberOfEpochs, + startTimestamp, + ); + + await expect(twabRewards.cancelPromotion(promotionId, wallet1.address)) + .to.emit(twabRewards, 'PromotionCancelled') + .withArgs(promotionId, wallet1.address, promotionAmount); + + expect(await rewardToken.balanceOf(wallet1.address)).to.equal(promotionAmount); + + expect( + (await twabRewards.callStatic.getPromotion(promotionId)).numberOfEpochs, + ).to.equal(await twabRewards.callStatic.getCurrentEpochId(promotionId)); + }); + it('should cancel promotion and still allow users to claim their rewards', async () => { const promotionId = 1; const epochNumber = 6; From 6979d776a02d790ce26c64d94b780746c5ba1aac Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 11 Jan 2022 11:35:09 -0600 Subject: [PATCH 25/55] fix(TwabRewards): improve _getRemainingRewards --- contracts/TwabRewards.sol | 8 +++++++- test/TwabRewards.test.ts | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 895ebdc..c134471 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -361,11 +361,17 @@ contract TwabRewards is ITwabRewards { @return Amount of tokens left to be rewarded */ function _getRemainingRewards(Promotion memory _promotion) internal view returns (uint256) { + uint256 _currentEpochId = _getCurrentEpochId(_promotion); + + if (_currentEpochId >= _promotion.numberOfEpochs) { + return 0; + } + unchecked { // _tokensPerEpoch * _numberOfEpochsLeft return _promotion.tokensPerEpoch * - (_promotion.numberOfEpochs - _getCurrentEpochId(_promotion)); + (_promotion.numberOfEpochs - _currentEpochId); } } diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index ff87eac..2dd731d 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -439,6 +439,18 @@ describe('TwabRewards', () => { } }); + it('should return 0 if promotion has ended', async () => { + await createPromotion(); + + const promotionId = 1; + const { epochDuration } = + await twabRewards.callStatic.getPromotion(promotionId); + + await increaseTime(epochDuration * 13); + + expect(await twabRewards.getRemainingRewards(promotionId)).to.equal(0); + }); + it('should revert if promotion id passed is inexistent', async () => { await expect(twabRewards.callStatic.getPromotion(1)).to.be.revertedWith( 'TwabRewards/invalid-promotion', From 0b45346d84126ad23cf7ede57aaded9a51c1b221 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 13 Dec 2021 18:12:20 -0600 Subject: [PATCH 26/55] fix(TwabRewards): check create and cancel inputs --- test/TwabRewards.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index a3a2d20..af41862 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -218,6 +218,24 @@ describe('TwabRewards', () => { ).to.be.revertedWith('TwabRewards/epochs-not-zero'); }); + it('should fail to create a new promotion if tokens per epoch is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, toWei('0')), + ).to.be.revertedWith('TwabRewards/tokens-not-zero'); + }); + + it('should fail to create a new promotion if epoch duration is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, tokensPerEpoch, 0), + ).to.be.revertedWith('TwabRewards/duration-not-zero'); + }); + + it('should fail to create a new promotion if number of epochs is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 0), + ).to.be.revertedWith('TwabRewards/epochs-not-zero'); + }); + it('should fail to create a new promotion if number of epochs exceeds limit', async () => { await expect( createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 256), From 2def8d77e55c87326a21f10fd6af7ad88e51c23e Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 15 Dec 2021 11:16:25 -0600 Subject: [PATCH 27/55] fix(TwabRewards): test claimRewards epochs limit --- contracts/TwabRewards.sol | 2 +- test/TwabRewards.test.ts | 28 ++++++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index e4b9e1b..2fa090b 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -32,7 +32,7 @@ contract TwabRewards is ITwabRewards { /// @notice Keeps track of claimed rewards per user. /// @dev _claimedEpochs[promotionId][user] => claimedEpochs - /// @dev We pack epochs claimed by a user into a uint256. So we can't store more than 255 epochs. + /// @dev We pack epochs claimed by a user into a uint256. So we can't store more than 256 epochs. mapping(uint256 => mapping(address => uint256)) internal _claimedEpochs; /* ============ Events ============ */ diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 86ecfaf..6032a7a 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -54,7 +54,7 @@ describe('TwabRewards', () => { const tokensPerEpoch = toWei('10000'); const epochDuration = 604800; // 1 week in seconds const numberOfEpochs = 12; // 3 months since 1 epoch runs for 1 week - const promotionAmount = tokensPerEpoch.mul(numberOfEpochs); + let promotionAmount: BigNumber; const createPromotion = async ( ticketAddress: string = ticket.address, @@ -64,6 +64,8 @@ describe('TwabRewards', () => { epochsNumber: number = numberOfEpochs, startTimestamp?: number, ) => { + promotionAmount = epochTokens.mul(epochsNumber); + if (token.mock) { await token.mock.transferFrom .withArgs(wallet1.address, twabRewards.address, promotionAmount) @@ -527,8 +529,7 @@ describe('TwabRewards', () => { epochDuration, numberOfEpochs, startTimestamp, - ), - + ); expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(0); }); @@ -536,9 +537,7 @@ describe('TwabRewards', () => { await createPromotion(); await increaseTime(epochDuration * 12); - expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal( - numberOfEpochs - 1, - ); + expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(numberOfEpochs - 1); }); it('should revert if promotion id passed is inexistent', async () => { @@ -862,6 +861,23 @@ describe('TwabRewards', () => { twabRewards.claimRewards(wallet2.address, promotionId, ['2', '3', '4']), ).to.be.revertedWith('TwabRewards/rewards-claimed'); }); + + it('should fail to claim rewards past 255', async () => { + const promotionId = 1; + + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.mint(wallet3.address, wallet3Amount); + + await createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 255); + + await increaseTime(epochDuration * 256); + + await expect(twabRewards.claimRewards(wallet2.address, promotionId, ['256'])).to.be + .reverted; + }); }); describe('_requireTicket()', () => { From f2b2836b9aeef1a00dd74c863386d72e247fb8dc Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 14 Dec 2021 18:42:01 -0600 Subject: [PATCH 28/55] fix(TwabRewards): fix getCurrentEpochId edge cases --- contracts/TwabRewards.sol | 41 ++++++++++++++++++++++++++++----------- test/TwabRewards.test.ts | 40 ++++++++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 8835fb1..e4b9e1b 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -278,8 +278,7 @@ contract TwabRewards is ITwabRewards { unchecked { // promotionEndTimestamp > block.timestamp require( - (_promotion.startTimestamp + - (_promotion.epochDuration * _promotion.numberOfEpochs)) > block.timestamp, + _getPromotionEndTimestamp(_promotion) >= block.timestamp, "TwabRewards/promotion-inactive" ); } @@ -297,22 +296,44 @@ contract TwabRewards is ITwabRewards { return _promotion; } + /** + @notice Compute promotion end timestamp. + @param _promotion Promotion to compute end timestamp for + @return Promotion end timestamp + */ + function _getPromotionEndTimestamp(Promotion memory _promotion) + internal + pure + returns (uint256) + { + return _promotion.startTimestamp + (_promotion.epochDuration * _promotion.numberOfEpochs); + } + /** @notice Get the current epoch id of a promotion. @dev Epoch ids and their boolean values are tightly packed and stored in a uint256, so epoch id starts at 0. - @dev We calculate the current epoch id if the promotion has started. Otherwise, we return 0. + @dev We return the current epoch id if the promotion has not ended. + Otherwise, we return the last epoch id or 0 if the promotion has not started. @param _promotion Promotion to get current epoch for @return Epoch id */ function _getCurrentEpochId(Promotion memory _promotion) internal view returns (uint256) { + + uint256 _currentEpochId; + if (block.timestamp > _promotion.startTimestamp) { - unchecked { - // elapsedTimestamp / epochDurationTimestamp - return (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; + _currentEpochId = (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; + } + + if (_currentEpochId > 0) { + if (_currentEpochId >= _promotion.numberOfEpochs) { + _currentEpochId = _promotion.numberOfEpochs - uint8(1); } + } else { + _currentEpochId = 0; } - return 0; + return _currentEpochId; } /** @@ -366,9 +387,7 @@ contract TwabRewards is ITwabRewards { @return Amount of tokens left to be rewarded */ function _getRemainingRewards(Promotion memory _promotion) internal view returns (uint256) { - uint256 _currentEpochId = _getCurrentEpochId(_promotion); - - if (_currentEpochId >= _promotion.numberOfEpochs) { + if (block.timestamp > _getPromotionEndTimestamp(_promotion)) { return 0; } @@ -376,7 +395,7 @@ contract TwabRewards is ITwabRewards { // _tokensPerEpoch * _numberOfEpochsLeft return _promotion.tokensPerEpoch * - (_promotion.numberOfEpochs - _currentEpochId); + (_promotion.numberOfEpochs - _getCurrentEpochId(_promotion)); } } diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index af41862..86ecfaf 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -267,9 +267,16 @@ describe('TwabRewards', () => { expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); - expect( - (await twabRewards.callStatic.getPromotion(promotionId)).numberOfEpochs, - ).to.equal(await twabRewards.callStatic.getCurrentEpochId(promotionId)); + let latestEpochId = (await twabRewards.callStatic.getPromotion(promotionId)) + .numberOfEpochs; + + if (latestEpochId !== 0) { + latestEpochId--; + } + + expect(latestEpochId).to.equal( + await twabRewards.callStatic.getCurrentEpochId(promotionId), + ); // We burn tokens from wallet1 to reset balance await rewardToken.burn(wallet1.address, transferredAmount); @@ -335,8 +342,9 @@ describe('TwabRewards', () => { .withArgs(promotionId, wallet1.address, transferredAmount); expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); + expect( - (await twabRewards.callStatic.getPromotion(promotionId)).numberOfEpochs, + (await twabRewards.callStatic.getPromotion(promotionId)).numberOfEpochs - 1, ).to.equal(await twabRewards.callStatic.getCurrentEpochId(promotionId)); await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) @@ -509,6 +517,30 @@ describe('TwabRewards', () => { expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(3); }); + it('should return the first epoch id if the promotion has not started yet', async () => { + const startTimestamp = (await ethers.provider.getBlock('latest')).timestamp + 60; + + await createPromotion( + ticket.address, + rewardToken, + tokensPerEpoch, + epochDuration, + numberOfEpochs, + startTimestamp, + ), + + expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(0); + }); + + it('should return the last active epoch id if the promotion is inactive', async () => { + await createPromotion(); + await increaseTime(epochDuration * 12); + + expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal( + numberOfEpochs - 1, + ); + }); + it('should revert if promotion id passed is inexistent', async () => { await expect(twabRewards.callStatic.getCurrentEpochId(1)).to.be.revertedWith( 'TwabRewards/invalid-promotion', From 24944a86e88949d29a6b8283cc660726664c2b8b Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 15 Dec 2021 14:33:23 -0600 Subject: [PATCH 29/55] fix(TwabRewards): revert on invalid epoch id --- contracts/TwabRewards.sol | 5 +++- contracts/interfaces/ITwabRewards.sol | 3 +++ test/TwabRewards.test.ts | 33 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 2fa090b..81b3dab 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -338,7 +338,9 @@ contract TwabRewards is ITwabRewards { /** @notice Get reward amount for a specific user. - @dev Rewards can only be claimed once the epoch is over. + @dev Rewards can only be calculated once the epoch is over. + @dev Will revert if `_epochId` is over the total number of epochs or if epoch is not over. + @dev Will return 0 if the user average balance of tickets is 0. @param _user User to get reward amount for @param _promotion Promotion from which the epoch is @param _epochId Epoch id to get reward amount for @@ -354,6 +356,7 @@ contract TwabRewards is ITwabRewards { uint64 _epochEndTimestamp = _epochStartTimestamp + _epochDuration; require(block.timestamp >= _epochEndTimestamp, "TwabRewards/epoch-not-over"); + require(_epochId < _promotion.numberOfEpochs, "TwabRewards/invalid-epoch-id"); ITicket _ticket = ITicket(_promotion.ticket); diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index 99039d1..fe2561e 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -109,6 +109,9 @@ interface ITwabRewards { /** @notice Get amount of tokens to be rewarded for a given epoch. + @dev Rewards amount can only be retrieved for epochs that are over. + @dev Will revert if `_epochId` is over the total number of epochs or if epoch is not over. + @dev Will return 0 if the user average balance of tickets is 0. @dev Will be 0 if user has already claimed rewards for the epoch. @param _user Address of the user to get amount of rewards for @param _promotionId Promotion id from which the epoch is diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 6032a7a..5959080 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -530,6 +530,7 @@ describe('TwabRewards', () => { numberOfEpochs, startTimestamp, ); + expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(0); }); @@ -685,6 +686,21 @@ describe('TwabRewards', () => { ).to.be.revertedWith('TwabRewards/epoch-not-over'); }); + it('should fail to get rewards amount for epoch ids that does not exist', async () => { + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.mint(wallet3.address, wallet3Amount); + + await createPromotion(); + await increaseTime(epochDuration * 13); + + await expect( + twabRewards.callStatic.getRewardsAmount(wallet2.address, 1, ['12', '13', '14']), + ).to.be.revertedWith('TwabRewards/invalid-epoch-id'); + }); + it('should revert if promotion id passed is inexistent', async () => { await expect( twabRewards.callStatic.getRewardsAmount(wallet2.address, 1, ['0', '1', '2']), @@ -862,6 +878,23 @@ describe('TwabRewards', () => { ).to.be.revertedWith('TwabRewards/rewards-claimed'); }); + it('should fail to claim rewards for epoch ids that does not exist', async () => { + const promotionId = 1; + + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.mint(wallet3.address, wallet3Amount); + + await createPromotion(); + await increaseTime(epochDuration * 13); + + await expect( + twabRewards.claimRewards(wallet2.address, promotionId, ['12', '13', '14']), + ).to.be.revertedWith('TwabRewards/invalid-epoch-id'); + }); + it('should fail to claim rewards past 255', async () => { const promotionId = 1; From 25dc47d0af75ff794d90cb5ba6ac1f90d25bbe4d Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 15 Dec 2021 15:43:42 -0600 Subject: [PATCH 30/55] fix(TwabRewards): return 0 if epoch has been claimed --- contracts/TwabRewards.sol | 6 ++++- test/TwabRewards.test.ts | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 81b3dab..996e535 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -237,7 +237,11 @@ contract TwabRewards is ITwabRewards { uint256[] memory _rewardsAmount = new uint256[](_epochIdsLength); for (uint256 index = 0; index < _epochIdsLength; index++) { - _rewardsAmount[index] = _calculateRewardAmount(_user, _promotion, _epochIds[index]); + if (_isClaimedEpoch(_claimedEpochs[_promotionId][_user], uint8(index))) { + _rewardsAmount[index] = 0; + } else { + _rewardsAmount[index] = _calculateRewardAmount(_user, _promotion, _epochIds[index]); + } } return _rewardsAmount; diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 5959080..7303b31 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -646,6 +646,55 @@ describe('TwabRewards', () => { ).to.deep.equal([wallet3RewardAmount, wallet3RewardAmount, wallet3HalfRewardAmount]); }); + it('should return 0 for epochs that have already been claimed', async () => { + const promotionId = 1; + const epochIds = [0, 1, 2]; + + const zeroAmount = toWei('0'); + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + const totalAmount = wallet2Amount.add(wallet3Amount); + + const wallet2ShareOfTickets = wallet2Amount.mul(100).div(totalAmount); + const wallet2RewardAmount = wallet2ShareOfTickets.mul(tokensPerEpoch).div(100); + + const wallet3ShareOfTickets = wallet3Amount.mul(100).div(totalAmount); + const wallet3RewardAmount = wallet3ShareOfTickets.mul(tokensPerEpoch).div(100); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.connect(wallet2).delegate(wallet2.address); + await ticket.mint(wallet3.address, wallet3Amount); + await ticket.connect(wallet3).delegate(wallet3.address); + + await createPromotion(); + await increaseTime(epochDuration * 3); + + await expect(twabRewards.claimRewards(wallet2.address, promotionId, [0, 2])) + .to.emit(twabRewards, 'RewardsClaimed') + .withArgs(promotionId, [0, 2], wallet2.address, wallet2RewardAmount.mul(2)); + + await expect(twabRewards.claimRewards(wallet3.address, promotionId, [2])) + .to.emit(twabRewards, 'RewardsClaimed') + .withArgs(promotionId, [2], wallet3.address, wallet3RewardAmount); + + expect( + await twabRewards.callStatic.getRewardsAmount( + wallet2.address, + promotionId, + epochIds, + ), + ).to.deep.equal([zeroAmount, wallet2RewardAmount, zeroAmount]); + + expect( + await twabRewards.callStatic.getRewardsAmount( + wallet3.address, + promotionId, + epochIds, + ), + ).to.deep.equal([wallet3RewardAmount, wallet3RewardAmount, zeroAmount]); + }); + it('should return 0 if user has no tickets delegated to him', async () => { const wallet2Amount = toWei('750'); const zeroAmount = toWei('0'); From 721a1c7c46a92c1afaefca0a9d30dae4b743892d Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 12 Jan 2022 15:42:45 -0600 Subject: [PATCH 31/55] fix(TwabRewards): simplify _getCurrentEpochId --- contracts/TwabRewards.sol | 17 ++++++----------- test/TwabRewards.test.ts | 14 +++----------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 996e535..fafc2c9 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -282,7 +282,7 @@ contract TwabRewards is ITwabRewards { unchecked { // promotionEndTimestamp > block.timestamp require( - _getPromotionEndTimestamp(_promotion) >= block.timestamp, + _getPromotionEndTimestamp(_promotion) > block.timestamp, "TwabRewards/promotion-inactive" ); } @@ -317,24 +317,19 @@ contract TwabRewards is ITwabRewards { @notice Get the current epoch id of a promotion. @dev Epoch ids and their boolean values are tightly packed and stored in a uint256, so epoch id starts at 0. @dev We return the current epoch id if the promotion has not ended. - Otherwise, we return the last epoch id or 0 if the promotion has not started. + If the current time is before the promotion start timestamp, we return 0. + Otherwise, we return the epoch id at the current timestamp. This could be greater than the number of epochs of the promotion. @param _promotion Promotion to get current epoch for @return Epoch id */ function _getCurrentEpochId(Promotion memory _promotion) internal view returns (uint256) { - uint256 _currentEpochId; if (block.timestamp > _promotion.startTimestamp) { - _currentEpochId = (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; - } - - if (_currentEpochId > 0) { - if (_currentEpochId >= _promotion.numberOfEpochs) { - _currentEpochId = _promotion.numberOfEpochs - uint8(1); + unchecked { + // elapsedTimestamp / epochDurationTimestamp + _currentEpochId = (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; } - } else { - _currentEpochId = 0; } return _currentEpochId; diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 7303b31..fed816e 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -272,10 +272,6 @@ describe('TwabRewards', () => { let latestEpochId = (await twabRewards.callStatic.getPromotion(promotionId)) .numberOfEpochs; - if (latestEpochId !== 0) { - latestEpochId--; - } - expect(latestEpochId).to.equal( await twabRewards.callStatic.getCurrentEpochId(promotionId), ); @@ -345,10 +341,6 @@ describe('TwabRewards', () => { expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); - expect( - (await twabRewards.callStatic.getPromotion(promotionId)).numberOfEpochs - 1, - ).to.equal(await twabRewards.callStatic.getCurrentEpochId(promotionId)); - await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) .to.emit(twabRewards, 'RewardsClaimed') .withArgs(promotionId, epochIds, wallet2.address, wallet2TotalRewardsAmount); @@ -534,11 +526,11 @@ describe('TwabRewards', () => { expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(0); }); - it('should return the last active epoch id if the promotion is inactive', async () => { + it('should return the epoch id for the current timestamp', async () => { await createPromotion(); - await increaseTime(epochDuration * 12); + await increaseTime(epochDuration * 13); - expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(numberOfEpochs - 1); + expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(13); }); it('should revert if promotion id passed is inexistent', async () => { From 7e30994860906989a4f23c876a7f134a9fc62daf Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 10 Dec 2021 16:19:08 -0600 Subject: [PATCH 32/55] fix(TwabRewards): check createPromotion amount --- contracts/TwabRewards.sol | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index fafc2c9..6bed6a2 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -279,13 +279,10 @@ contract TwabRewards is ITwabRewards { @param _promotion Promotion to check */ function _requirePromotionActive(Promotion memory _promotion) internal view { - unchecked { - // promotionEndTimestamp > block.timestamp - require( - _getPromotionEndTimestamp(_promotion) > block.timestamp, - "TwabRewards/promotion-inactive" - ); - } + require( + _getPromotionEndTimestamp(_promotion) > block.timestamp, + "TwabRewards/promotion-inactive" + ); } /** @@ -310,7 +307,9 @@ contract TwabRewards is ITwabRewards { pure returns (uint256) { - return _promotion.startTimestamp + (_promotion.epochDuration * _promotion.numberOfEpochs); + unchecked { + return _promotion.startTimestamp + (_promotion.epochDuration * _promotion.numberOfEpochs); + } } /** From d75ae570a7d0686789752439c214e5c9261bc7f8 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 13 Dec 2021 18:12:20 -0600 Subject: [PATCH 33/55] fix(TwabRewards): check create and cancel inputs --- test/TwabRewards.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index fed816e..8ffc14b 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -238,6 +238,24 @@ describe('TwabRewards', () => { ).to.be.revertedWith('TwabRewards/epochs-not-zero'); }); + it('should fail to create a new promotion if tokens per epoch is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, toWei('0')), + ).to.be.revertedWith('TwabRewards/tokens-not-zero'); + }); + + it('should fail to create a new promotion if epoch duration is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, tokensPerEpoch, 0), + ).to.be.revertedWith('TwabRewards/duration-not-zero'); + }); + + it('should fail to create a new promotion if number of epochs is zero', async () => { + await expect( + createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 0), + ).to.be.revertedWith('TwabRewards/epochs-not-zero'); + }); + it('should fail to create a new promotion if number of epochs exceeds limit', async () => { await expect( createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 256), From aa9d28fe0e59d7c48bc0b261c07e67265bfc2954 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 15 Dec 2021 11:16:25 -0600 Subject: [PATCH 34/55] fix(TwabRewards): test claimRewards epochs limit --- test/TwabRewards.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 8ffc14b..c74a42a 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -540,7 +540,10 @@ describe('TwabRewards', () => { numberOfEpochs, startTimestamp, ); +<<<<<<< HEAD +======= +>>>>>>> 2c8b71d (fix(TwabRewards): test claimRewards epochs limit) expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(0); }); From 354f439ba249b526f566bef7349000dba80081ca Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 15 Dec 2021 14:33:23 -0600 Subject: [PATCH 35/55] fix(TwabRewards): revert on invalid epoch id --- test/TwabRewards.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index c74a42a..8ffc14b 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -540,10 +540,7 @@ describe('TwabRewards', () => { numberOfEpochs, startTimestamp, ); -<<<<<<< HEAD -======= ->>>>>>> 2c8b71d (fix(TwabRewards): test claimRewards epochs limit) expect(await twabRewards.callStatic.getCurrentEpochId(1)).to.equal(0); }); From badaace834469fa644aa6b85a2913a5f0a20ae62 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 15 Dec 2021 17:56:00 -0600 Subject: [PATCH 36/55] fix(TwabRewards): store prize pool ticket address --- contracts/TwabRewards.sol | 32 ++++--- contracts/interfaces/ITwabRewards.sol | 5 +- contracts/test/TwabRewardsHarness.sol | 4 +- test/TwabRewards.test.ts | 122 +++++++------------------- 4 files changed, 57 insertions(+), 106 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 6bed6a2..32f10af 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -16,6 +16,7 @@ import "./interfaces/ITwabRewards.sol"; * In order to calculate user rewards, we use the TWAB (Time-Weighted Average Balance) from the Ticket contract. * This way, users simply need to hold their tickets to be eligible to claim rewards. * Rewards are calculated based on the average amount of tickets they hold during the epoch duration. + * @dev This contract supports only one prize pool ticket. * @dev This contract does not support the use of fee on transfer tokens. */ contract TwabRewards is ITwabRewards { @@ -23,6 +24,9 @@ contract TwabRewards is ITwabRewards { /* ============ Global Variables ============ */ + /// @notice Prize pool ticket for which the promotions are created. + ITicket public immutable ticket; + /// @notice Settings of each promotion. mapping(uint256 => Promotion) internal _promotions; @@ -76,11 +80,21 @@ contract TwabRewards is ITwabRewards { uint256 amount ); + /* ============ Constructor ============ */ + + /** + @notice Constructor of the contract. + @param _ticket Prize Pool ticket address for which the promotions will be created + */ + constructor(ITicket _ticket) { + _requireTicket(_ticket); + ticket = _ticket; + } + /* ============ External Functions ============ */ /// @inheritdoc ITwabRewards function createPromotion( - address _ticket, IERC20 _token, uint128 _startTimestamp, uint256 _tokensPerEpoch, @@ -90,14 +104,12 @@ contract TwabRewards is ITwabRewards { require(_tokensPerEpoch > 0, "TwabRewards/tokens-not-zero"); require(_epochDuration > 0, "TwabRewards/duration-not-zero"); _requireNumberOfEpochs(_numberOfEpochs); - _requireTicket(_ticket); uint256 _nextPromotionId = _latestPromotionId + 1; _latestPromotionId = _nextPromotionId; _promotions[_nextPromotionId] = Promotion( msg.sender, - _ticket, _token, _startTimestamp, _tokensPerEpoch, @@ -253,11 +265,11 @@ contract TwabRewards is ITwabRewards { @notice Determine if address passed is actually a ticket. @param _ticket Address to check */ - function _requireTicket(address _ticket) internal view { - require(_ticket != address(0), "TwabRewards/ticket-not-zero-addr"); + function _requireTicket(ITicket _ticket) internal view { + require(address(_ticket) != address(0), "TwabRewards/ticket-not-zero-addr"); - (bool succeeded, bytes memory data) = _ticket.staticcall( - abi.encodePacked(ITicket(_ticket).controller.selector) + (bool succeeded, bytes memory data) = address(_ticket).staticcall( + abi.encodePacked(_ticket.controller.selector) ); require( @@ -356,9 +368,7 @@ contract TwabRewards is ITwabRewards { require(block.timestamp >= _epochEndTimestamp, "TwabRewards/epoch-not-over"); require(_epochId < _promotion.numberOfEpochs, "TwabRewards/invalid-epoch-id"); - ITicket _ticket = ITicket(_promotion.ticket); - - uint256 _averageBalance = _ticket.getAverageBalanceBetween( + uint256 _averageBalance = ticket.getAverageBalanceBetween( _user, uint64(_epochStartTimestamp), uint64(_epochEndTimestamp) @@ -371,7 +381,7 @@ contract TwabRewards is ITwabRewards { uint64[] memory _epochEndTimestamps = new uint64[](1); _epochEndTimestamps[0] = _epochEndTimestamp; - uint256 _averageTotalSupply = _ticket.getAverageTotalSuppliesBetween( + uint256 _averageTotalSupply = ticket.getAverageTotalSuppliesBetween( _epochStartTimestamps, _epochEndTimestamps )[0]; diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index fe2561e..9794ec5 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -13,7 +13,7 @@ interface ITwabRewards { /** @notice Struct to keep track of each promotion's settings. @param creator Addresss of the promotion creator - @param ticket Prize Pool ticket address for which the promotion has been created + @param ticket Prize Pool ticket address @param token Address of the token to be distributed as reward @param startTimestamp Timestamp at which the promotion starts @param tokensPerEpoch Number of tokens to be distributed per epoch @@ -22,7 +22,6 @@ interface ITwabRewards { */ struct Promotion { address creator; - address ticket; IERC20 token; uint128 startTimestamp; uint256 tokensPerEpoch; @@ -37,7 +36,6 @@ interface ITwabRewards { So the first promotion will have id 1, the second 2, etc. @dev The transaction will revert if the amount of reward tokens provided is not equal to `_tokensPerEpoch * _numberOfEpochs`. This scenario could happen if the token supplied is a fee on transfer one. - @param _ticket Prize Pool ticket address for which the promotion is created @param _token Address of the token to be distributed @param _startTimestamp Timestamp at which the promotion starts @param _tokensPerEpoch Number of tokens to be distributed per epoch @@ -46,7 +44,6 @@ interface ITwabRewards { @return Id of the newly created promotion */ function createPromotion( - address _ticket, IERC20 _token, uint128 _startTimestamp, uint256 _tokensPerEpoch, diff --git a/contracts/test/TwabRewardsHarness.sol b/contracts/test/TwabRewardsHarness.sol index a2d07b5..582ff7e 100644 --- a/contracts/test/TwabRewardsHarness.sol +++ b/contracts/test/TwabRewardsHarness.sol @@ -5,7 +5,9 @@ pragma solidity 0.8.6; import "../TwabRewards.sol"; contract TwabRewardsHarness is TwabRewards { - function requireTicket(address _ticket) external view { + constructor(ITicket _ticket) TwabRewards(_ticket) {} + + function requireTicket(ITicket _ticket) external view { return _requireTicket(_ticket); } diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 8ffc14b..70f4181 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -43,9 +43,9 @@ describe('TwabRewards', () => { beforeEach(async () => { rewardToken = await erc20MintableFactory.deploy('Reward', 'REWA'); - twabRewards = await twabRewardsFactory.deploy(); ticket = await ticketFactory.deploy('Ticket', 'TICK', 18, wallet1.address); + twabRewards = await twabRewardsFactory.deploy(ticket.address); mockRewardToken = await deployMockContract(wallet1, ERC20MintableInterface); mockTicket = await deployMockContract(wallet1, TicketInterface); @@ -57,7 +57,6 @@ describe('TwabRewards', () => { let promotionAmount: BigNumber; const createPromotion = async ( - ticketAddress: string = ticket.address, token: Contract | MockContract = rewardToken, epochTokens: BigNumber = tokensPerEpoch, epochTimestamp: number = epochDuration, @@ -86,7 +85,6 @@ describe('TwabRewards', () => { } return await twabRewards.createPromotion( - ticketAddress, token.address, createPromotionTimestamp, epochTokens, @@ -95,6 +93,26 @@ describe('TwabRewards', () => { ); }; + describe('constructor()', () => { + it('should deploy contract', async () => { + expect(await twabRewards.callStatic.ticket()).to.equal(ticket.address); + }); + + it('should fail to deploy contract if ticket is address zero', async () => { + await expect(twabRewardsFactory.deploy(AddressZero)).to.be.revertedWith( + 'TwabRewards/ticket-not-zero-addr', + ); + }); + + it('should fail to deploy contract if ticket is not an actual ticket', async () => { + const randomWallet = Wallet.createRandom(); + + await expect(twabRewardsFactory.deploy(randomWallet.address)).to.be.revertedWith( + 'TwabRewards/invalid-ticket', + ); + }); + }); + describe('createPromotion()', async () => { it('should create a new promotion', async () => { const promotionId = 1; @@ -106,7 +124,6 @@ describe('TwabRewards', () => { const promotion = await twabRewards.callStatic.getPromotion(promotionId); expect(promotion.creator).to.equal(wallet1.address); - expect(promotion.ticket).to.equal(ticket.address); expect(promotion.token).to.equal(rewardToken.address); expect(promotion.tokensPerEpoch).to.equal(tokensPerEpoch); expect(promotion.startTimestamp).to.equal(createPromotionTimestamp); @@ -125,7 +142,6 @@ describe('TwabRewards', () => { const firstPromotion = await twabRewards.callStatic.getPromotion(promotionIdOne); expect(firstPromotion.creator).to.equal(wallet1.address); - expect(firstPromotion.ticket).to.equal(ticket.address); expect(firstPromotion.token).to.equal(rewardToken.address); expect(firstPromotion.tokensPerEpoch).to.equal(tokensPerEpoch); expect(firstPromotion.startTimestamp).to.equal(createPromotionTimestamp); @@ -139,7 +155,6 @@ describe('TwabRewards', () => { const secondPromotion = await twabRewards.callStatic.getPromotion(promotionIdTwo); expect(secondPromotion.creator).to.equal(wallet1.address); - expect(secondPromotion.ticket).to.equal(ticket.address); expect(secondPromotion.token).to.equal(rewardToken.address); expect(secondPromotion.tokensPerEpoch).to.equal(tokensPerEpoch); expect(secondPromotion.startTimestamp).to.equal(createPromotionTimestamp); @@ -152,7 +167,6 @@ describe('TwabRewards', () => { await expect( createPromotion( - ticket.address, rewardToken, tokensPerEpoch, epochDuration, @@ -165,101 +179,32 @@ describe('TwabRewards', () => { }); it('should fail to create a new promotion if reward token is a fee on transfer token', async () => { - await expect(createPromotion(ticket.address, mockRewardToken)).to.be.revertedWith( + await expect(createPromotion(mockRewardToken)).to.be.revertedWith( 'TwabRewards/promo-amount-diff', ); }); - it('should fail to create a new promotion if ticket is address zero', async () => { - await expect(createPromotion(AddressZero)).to.be.revertedWith( - 'TwabRewards/ticket-not-zero-addr', - ); - }); - - it('should fail to create a new promotion if ticket is not an actual ticket', async () => { - const randomWallet = Wallet.createRandom(); - - await expect(createPromotion(randomWallet.address)).to.be.revertedWith( - 'TwabRewards/invalid-ticket', - ); - }); - - it('should fail to create a new promotion if tokens per epoch is zero', async () => { - await expect( - createPromotion(ticket.address, rewardToken, toWei('0')), - ).to.be.revertedWith('TwabRewards/tokens-not-zero'); - }); - - it('should fail to create a new promotion if epoch duration is zero', async () => { - await expect( - createPromotion(ticket.address, rewardToken, tokensPerEpoch, 0), - ).to.be.revertedWith('TwabRewards/duration-not-zero'); - }); - - it('should fail to create a new promotion if number of epochs is zero', async () => { - await expect( - createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 0), - ).to.be.revertedWith('TwabRewards/epochs-not-zero'); - }); - - it('should fail to create a new promotion if tokens per epoch is zero', async () => { - await expect( - createPromotion(ticket.address, rewardToken, toWei('0')), - ).to.be.revertedWith('TwabRewards/tokens-not-zero'); - }); - - it('should fail to create a new promotion if epoch duration is zero', async () => { - await expect( - createPromotion(ticket.address, rewardToken, tokensPerEpoch, 0), - ).to.be.revertedWith('TwabRewards/duration-not-zero'); - }); - - it('should fail to create a new promotion if number of epochs is zero', async () => { - await expect( - createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 0), - ).to.be.revertedWith('TwabRewards/epochs-not-zero'); - }); - - it('should fail to create a new promotion if tokens per epoch is zero', async () => { - await expect( - createPromotion(ticket.address, rewardToken, toWei('0')), - ).to.be.revertedWith('TwabRewards/tokens-not-zero'); - }); - - it('should fail to create a new promotion if epoch duration is zero', async () => { - await expect( - createPromotion(ticket.address, rewardToken, tokensPerEpoch, 0), - ).to.be.revertedWith('TwabRewards/duration-not-zero'); - }); - - it('should fail to create a new promotion if number of epochs is zero', async () => { - await expect( - createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 0), - ).to.be.revertedWith('TwabRewards/epochs-not-zero'); - }); - it('should fail to create a new promotion if tokens per epoch is zero', async () => { - await expect( - createPromotion(ticket.address, rewardToken, toWei('0')), - ).to.be.revertedWith('TwabRewards/tokens-not-zero'); + await expect(createPromotion(rewardToken, toWei('0'))).to.be.revertedWith( + 'TwabRewards/tokens-not-zero', + ); }); it('should fail to create a new promotion if epoch duration is zero', async () => { - await expect( - createPromotion(ticket.address, rewardToken, tokensPerEpoch, 0), - ).to.be.revertedWith('TwabRewards/duration-not-zero'); + await expect(createPromotion(rewardToken, tokensPerEpoch, 0)).to.be.revertedWith( + 'TwabRewards/duration-not-zero', + ); }); it('should fail to create a new promotion if number of epochs is zero', async () => { await expect( - createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 0), + createPromotion(rewardToken, tokensPerEpoch, epochDuration, 0), ).to.be.revertedWith('TwabRewards/epochs-not-zero'); }); it('should fail to create a new promotion if number of epochs exceeds limit', async () => { - await expect( - createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 256), - ).to.be.reverted; + await expect(createPromotion(rewardToken, tokensPerEpoch, epochDuration, 256)).to.be + .reverted; }); }); @@ -304,7 +249,6 @@ describe('TwabRewards', () => { const startTimestamp = (await ethers.provider.getBlock('latest')).timestamp + 60; await createPromotion( - ticket.address, rewardToken, tokensPerEpoch, epochDuration, @@ -468,7 +412,6 @@ describe('TwabRewards', () => { const promotion = await twabRewards.callStatic.getPromotion(1); expect(promotion.creator).to.equal(wallet1.address); - expect(promotion.ticket).to.equal(ticket.address); expect(promotion.token).to.equal(rewardToken.address); expect(promotion.tokensPerEpoch).to.equal(tokensPerEpoch); expect(promotion.startTimestamp).to.equal(createPromotionTimestamp); @@ -533,7 +476,6 @@ describe('TwabRewards', () => { const startTimestamp = (await ethers.provider.getBlock('latest')).timestamp + 60; await createPromotion( - ticket.address, rewardToken, tokensPerEpoch, epochDuration, @@ -963,7 +905,7 @@ describe('TwabRewards', () => { await ticket.mint(wallet2.address, wallet2Amount); await ticket.mint(wallet3.address, wallet3Amount); - await createPromotion(ticket.address, rewardToken, tokensPerEpoch, epochDuration, 255); + await createPromotion(rewardToken, tokensPerEpoch, epochDuration, 255); await increaseTime(epochDuration * 256); From 746bb316bbce814d79096d5861a4b5b837485dbe Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 12 Jan 2022 10:41:01 -0600 Subject: [PATCH 37/55] fix(TwabRewards): improve Promotion packing --- contracts/TwabRewards.sol | 24 ++++++++++-------------- contracts/interfaces/ITwabRewards.sol | 17 ++++++++--------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 32f10af..db170e2 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -96,9 +96,9 @@ contract TwabRewards is ITwabRewards { /// @inheritdoc ITwabRewards function createPromotion( IERC20 _token, - uint128 _startTimestamp, + uint64 _startTimestamp, uint256 _tokensPerEpoch, - uint56 _epochDuration, + uint64 _epochDuration, uint8 _numberOfEpochs ) external override returns (uint256) { require(_tokensPerEpoch > 0, "TwabRewards/tokens-not-zero"); @@ -110,11 +110,11 @@ contract TwabRewards is ITwabRewards { _promotions[_nextPromotionId] = Promotion( msg.sender, - _token, _startTimestamp, - _tokensPerEpoch, + _numberOfEpochs, _epochDuration, - _numberOfEpochs + _token, + _tokensPerEpoch ); uint256 _amount; @@ -181,11 +181,7 @@ contract TwabRewards is ITwabRewards { _extendedAmount = _numberOfEpochs * _promotion.tokensPerEpoch; } - _promotion.token.safeTransferFrom( - msg.sender, - address(this), - _extendedAmount - ); + _promotion.token.safeTransferFrom(msg.sender, address(this), _extendedAmount); emit PromotionExtended(_promotionId, _numberOfEpochs); @@ -361,8 +357,8 @@ contract TwabRewards is ITwabRewards { Promotion memory _promotion, uint8 _epochId ) internal view returns (uint256) { - uint56 _epochDuration = _promotion.epochDuration; - uint64 _epochStartTimestamp = uint64(_promotion.startTimestamp) + (_epochDuration * _epochId); + uint64 _epochDuration = _promotion.epochDuration; + uint64 _epochStartTimestamp = _promotion.startTimestamp + (_epochDuration * _epochId); uint64 _epochEndTimestamp = _epochStartTimestamp + _epochDuration; require(block.timestamp >= _epochEndTimestamp, "TwabRewards/epoch-not-over"); @@ -370,8 +366,8 @@ contract TwabRewards is ITwabRewards { uint256 _averageBalance = ticket.getAverageBalanceBetween( _user, - uint64(_epochStartTimestamp), - uint64(_epochEndTimestamp) + _epochStartTimestamp, + _epochEndTimestamp ); if (_averageBalance > 0) { diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index 9794ec5..88645a4 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -13,20 +13,19 @@ interface ITwabRewards { /** @notice Struct to keep track of each promotion's settings. @param creator Addresss of the promotion creator - @param ticket Prize Pool ticket address - @param token Address of the token to be distributed as reward @param startTimestamp Timestamp at which the promotion starts - @param tokensPerEpoch Number of tokens to be distributed per epoch - @param epochDuration Duration of one epoch in seconds @param numberOfEpochs Number of epochs the promotion will last for + @param epochDuration Duration of one epoch in seconds + @param token Address of the token to be distributed as reward + @param tokensPerEpoch Number of tokens to be distributed per epoch */ struct Promotion { address creator; + uint64 startTimestamp; + uint8 numberOfEpochs; + uint64 epochDuration; IERC20 token; - uint128 startTimestamp; uint256 tokensPerEpoch; - uint56 epochDuration; - uint8 numberOfEpochs; } /** @@ -45,9 +44,9 @@ interface ITwabRewards { */ function createPromotion( IERC20 _token, - uint128 _startTimestamp, + uint64 _startTimestamp, uint256 _tokensPerEpoch, - uint56 _epochDuration, + uint64 _epochDuration, uint8 _numberOfEpochs ) external returns (uint256); From e7e21d8a100dbb5a552947f9fa0d620930499456 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 10 Dec 2021 16:19:08 -0600 Subject: [PATCH 38/55] fix(TwabRewards): check createPromotion amount --- contracts/TwabRewards.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index db170e2..77a4765 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -16,7 +16,10 @@ import "./interfaces/ITwabRewards.sol"; * In order to calculate user rewards, we use the TWAB (Time-Weighted Average Balance) from the Ticket contract. * This way, users simply need to hold their tickets to be eligible to claim rewards. * Rewards are calculated based on the average amount of tickets they hold during the epoch duration. +<<<<<<< HEAD * @dev This contract supports only one prize pool ticket. +======= +>>>>>>> d8c9803 (fix(TwabRewards): check createPromotion amount) * @dev This contract does not support the use of fee on transfer tokens. */ contract TwabRewards is ITwabRewards { From 413713122aaebcd8a03df2abd3a2089a37525faf Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 13 Dec 2021 18:12:20 -0600 Subject: [PATCH 39/55] fix(TwabRewards): check create and cancel inputs --- contracts/TwabRewards.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 77a4765..db170e2 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -16,10 +16,7 @@ import "./interfaces/ITwabRewards.sol"; * In order to calculate user rewards, we use the TWAB (Time-Weighted Average Balance) from the Ticket contract. * This way, users simply need to hold their tickets to be eligible to claim rewards. * Rewards are calculated based on the average amount of tickets they hold during the epoch duration. -<<<<<<< HEAD * @dev This contract supports only one prize pool ticket. -======= ->>>>>>> d8c9803 (fix(TwabRewards): check createPromotion amount) * @dev This contract does not support the use of fee on transfer tokens. */ contract TwabRewards is ITwabRewards { From 4dc6c977d797b83b535a182c530ecf79f3659d4a Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 21 Dec 2021 16:23:25 +0100 Subject: [PATCH 40/55] feat(TwabRewards): add destroyPromotion function --- contracts/PrizeTierHistory.sol | 6 +- contracts/TwabRewards.sol | 76 ++++++++++--- contracts/interfaces/ITwabRewards.sol | 21 +++- test/TwabRewards.test.ts | 148 +++++++++++++++++--------- 4 files changed, 175 insertions(+), 76 deletions(-) diff --git a/contracts/PrizeTierHistory.sol b/contracts/PrizeTierHistory.sol index 96e8770..2caa24c 100644 --- a/contracts/PrizeTierHistory.sol +++ b/contracts/PrizeTierHistory.sol @@ -22,11 +22,7 @@ contract PrizeTierHistory is IPrizeTierHistory, Manageable { /* ============ External Functions ============ */ // @inheritdoc IPrizeTierHistory - function push(PrizeTier calldata _nextPrizeTier) - external - override - onlyManagerOrOwner - { + function push(PrizeTier calldata _nextPrizeTier) external override onlyManagerOrOwner { PrizeTier[] memory _history = history; if (_history.length > 0) { diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index db170e2..54ab66c 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -27,6 +27,9 @@ contract TwabRewards is ITwabRewards { /// @notice Prize pool ticket for which the promotions are created. ITicket public immutable ticket; + /// @notice Period during which the promotion owner can't destroy an inactive promotion. + uint32 internal _gracePeriod = 5184000; // 60 days in seconds + /// @notice Settings of each promotion. mapping(uint256 => Promotion) internal _promotions; @@ -48,12 +51,20 @@ contract TwabRewards is ITwabRewards { event PromotionCreated(uint256 indexed promotionId); /** - @notice Emitted when a promotion is cancelled. - @param promotionId Id of the promotion being cancelled + @notice Emitted when a promotion is ended. + @param promotionId Id of the promotion being ended @param recipient Address of the recipient that will receive the remaining rewards @param amount Amount of tokens transferred to the recipient */ - event PromotionCancelled( + event PromotionEnded(uint256 indexed promotionId, address indexed recipient, uint256 amount); + + /** + @notice Emitted when a promotion is destroyed. + @param promotionId Id of the promotion being destroyed + @param recipient Address of the recipient that will receive the unclaimed rewards + @param amount Amount of tokens transferred to the recipient + */ + event PromotionDestroyed( uint256 indexed promotionId, address indexed recipient, uint256 amount @@ -108,21 +119,22 @@ contract TwabRewards is ITwabRewards { uint256 _nextPromotionId = _latestPromotionId + 1; _latestPromotionId = _nextPromotionId; + uint256 _amount; + + unchecked { + _amount = _tokensPerEpoch * _numberOfEpochs; + } + _promotions[_nextPromotionId] = Promotion( msg.sender, _startTimestamp, _numberOfEpochs, _epochDuration, _token, - _tokensPerEpoch + _tokensPerEpoch, + _amount ); - uint256 _amount; - - unchecked { - _amount = _tokensPerEpoch * _numberOfEpochs; - } - uint256 _beforeBalance = _token.balanceOf(address(this)); _token.safeTransferFrom(msg.sender, address(this), _amount); @@ -137,11 +149,11 @@ contract TwabRewards is ITwabRewards { } /// @inheritdoc ITwabRewards - function cancelPromotion(uint256 _promotionId, address _to) external override returns (bool) { + function endPromotion(uint256 _promotionId, address _to) external override returns (bool) { require(_to != address(0), "TwabRewards/payee-not-zero-addr"); Promotion memory _promotion = _getPromotion(_promotionId); - require(msg.sender == _promotion.creator, "TwabRewards/only-promo-creator"); + _requirePromotionCreator(_promotion); _requirePromotionActive(_promotion); _promotions[_promotionId].numberOfEpochs = uint8(_getCurrentEpochId(_promotion)); @@ -149,7 +161,29 @@ contract TwabRewards is ITwabRewards { uint256 _remainingRewards = _getRemainingRewards(_promotion); _promotion.token.safeTransfer(_to, _remainingRewards); - emit PromotionCancelled(_promotionId, _to, _remainingRewards); + emit PromotionEnded(_promotionId, _to, _remainingRewards); + + return true; + } + + /// @inheritdoc ITwabRewards + function destroyPromotion(uint256 _promotionId, address _to) external override returns (bool) { + require(_to != address(0), "TwabRewards/payee-not-zero-addr"); + + Promotion memory _promotion = _getPromotion(_promotionId); + _requirePromotionCreator(_promotion); + + require( + (_getPromotionEndTimestamp(_promotion) + _gracePeriod) < block.timestamp, + "TwabRewards/promotion-active" + ); + + uint256 _rewardsUnclaimed = _promotion.rewardsUnclaimed; + delete _promotions[_promotionId]; + + _promotion.token.safeTransfer(_to, _rewardsUnclaimed); + + emit PromotionDestroyed(_promotionId, _to, _rewardsUnclaimed); return true; } @@ -175,13 +209,14 @@ contract TwabRewards is ITwabRewards { uint8 _extendedNumberOfEpochs = _currentNumberOfEpochs + _numberOfEpochs; _promotions[_promotionId].numberOfEpochs = _extendedNumberOfEpochs; - uint256 _extendedAmount; + uint256 _amount; unchecked { - _extendedAmount = _numberOfEpochs * _promotion.tokensPerEpoch; + _amount = _numberOfEpochs * _promotion.tokensPerEpoch; } - _promotion.token.safeTransferFrom(msg.sender, address(this), _extendedAmount); + _promotions[_promotionId].rewardsUnclaimed += _amount; + _promotion.token.safeTransferFrom(msg.sender, address(this), _amount); emit PromotionExtended(_promotionId, _numberOfEpochs); @@ -210,6 +245,7 @@ contract TwabRewards is ITwabRewards { } _claimedEpochs[_promotionId][_user] = _userClaimedEpochs; + _promotions[_promotionId].rewardsUnclaimed -= _rewardsAmount; _promotion.token.safeTransfer(_user, _rewardsAmount); @@ -293,6 +329,14 @@ contract TwabRewards is ITwabRewards { ); } + /** + @notice Determine if msg.sender is the promotion creator. + @param _promotion Promotion to check + */ + function _requirePromotionCreator(Promotion memory _promotion) internal view { + require(msg.sender == _promotion.creator, "TwabRewards/only-promo-creator"); + } + /** @notice Get settings for a specific promotion. @dev Will revert if the promotion does not exist. diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index 88645a4..49aa4a1 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -18,6 +18,7 @@ interface ITwabRewards { @param epochDuration Duration of one epoch in seconds @param token Address of the token to be distributed as reward @param tokensPerEpoch Number of tokens to be distributed per epoch + @param rewardsUnclaimed Amount of rewards that have not been claimed yet */ struct Promotion { address creator; @@ -26,6 +27,7 @@ interface ITwabRewards { uint64 epochDuration; IERC20 token; uint256 tokensPerEpoch; + uint256 rewardsUnclaimed; } /** @@ -51,12 +53,23 @@ interface ITwabRewards { ) external returns (uint256); /** - @notice Cancel currently active promotion and send promotion tokens back to the creator. - @param _promotionId Promotion id to cancel + @notice End currently active promotion and send promotion tokens back to the creator. + @dev Will only send back tokens from the epochs that have not yet started. + @param _promotionId Promotion id to end @param _to Address that will receive the remaining tokens if there are any left - @return true if cancelation was successful + @return true if operation was successful */ - function cancelPromotion(uint256 _promotionId, address _to) external returns (bool); + function endPromotion(uint256 _promotionId, address _to) external returns (bool); + + /** + @notice Delete an inactive promotion and send promotion tokens back to the creator. + @dev Will send back all the tokens that have not been claimed yet by users. + @dev This function will revert if the grace period is not over yet. + @param _promotionId Promotion id to destroy + @param _to Address that will receive the remaining tokens if there are any left + @return true if operation was successful + */ + function destroyPromotion(uint256 _promotionId, address _to) external returns (bool); /** @notice Extend promotion by adding more epochs. diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 70f4181..b2153ea 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -129,37 +129,7 @@ describe('TwabRewards', () => { expect(promotion.startTimestamp).to.equal(createPromotionTimestamp); expect(promotion.epochDuration).to.equal(epochDuration); expect(promotion.numberOfEpochs).to.equal(numberOfEpochs); - }); - - it('should create a second promotion and handle allowance properly', async () => { - const promotionIdOne = 1; - const promotionIdTwo = 2; - - await expect(createPromotion()) - .to.emit(twabRewards, 'PromotionCreated') - .withArgs(promotionIdOne); - - const firstPromotion = await twabRewards.callStatic.getPromotion(promotionIdOne); - - expect(firstPromotion.creator).to.equal(wallet1.address); - expect(firstPromotion.token).to.equal(rewardToken.address); - expect(firstPromotion.tokensPerEpoch).to.equal(tokensPerEpoch); - expect(firstPromotion.startTimestamp).to.equal(createPromotionTimestamp); - expect(firstPromotion.epochDuration).to.equal(epochDuration); - expect(firstPromotion.numberOfEpochs).to.equal(numberOfEpochs); - - await expect(createPromotion()) - .to.emit(twabRewards, 'PromotionCreated') - .withArgs(promotionIdTwo); - - const secondPromotion = await twabRewards.callStatic.getPromotion(promotionIdTwo); - - expect(secondPromotion.creator).to.equal(wallet1.address); - expect(secondPromotion.token).to.equal(rewardToken.address); - expect(secondPromotion.tokensPerEpoch).to.equal(tokensPerEpoch); - expect(secondPromotion.startTimestamp).to.equal(createPromotionTimestamp); - expect(secondPromotion.epochDuration).to.equal(epochDuration); - expect(secondPromotion.numberOfEpochs).to.equal(numberOfEpochs); + expect(promotion.rewardsUnclaimed).to.equal(tokensPerEpoch.mul(numberOfEpochs)); }); it('should succeed to create a new promotion even if start timestamp is before block timestamp', async () => { @@ -208,8 +178,8 @@ describe('TwabRewards', () => { }); }); - describe('cancelPromotion()', async () => { - it('should cancel a promotion and transfer the correct amount of reward tokens', async () => { + describe('endPromotion()', async () => { + it('should end a promotion and transfer the correct amount of reward tokens', async () => { for (let index = 0; index < numberOfEpochs; index++) { let promotionId = index + 1; @@ -226,8 +196,8 @@ describe('TwabRewards', () => { .mul(numberOfEpochs) .sub(tokensPerEpoch.mul(index)); - await expect(twabRewards.cancelPromotion(promotionId, wallet1.address)) - .to.emit(twabRewards, 'PromotionCancelled') + await expect(twabRewards.endPromotion(promotionId, wallet1.address)) + .to.emit(twabRewards, 'PromotionEnded') .withArgs(promotionId, wallet1.address, transferredAmount); expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); @@ -244,7 +214,7 @@ describe('TwabRewards', () => { } }); - it('should cancel a promotion before it starts and transfer the full amount of reward tokens', async () => { + it('should end a promotion before it starts and transfer the full amount of reward tokens', async () => { const promotionId = 1; const startTimestamp = (await ethers.provider.getBlock('latest')).timestamp + 60; @@ -256,8 +226,8 @@ describe('TwabRewards', () => { startTimestamp, ); - await expect(twabRewards.cancelPromotion(promotionId, wallet1.address)) - .to.emit(twabRewards, 'PromotionCancelled') + await expect(twabRewards.endPromotion(promotionId, wallet1.address)) + .to.emit(twabRewards, 'PromotionEnded') .withArgs(promotionId, wallet1.address, promotionAmount); expect(await rewardToken.balanceOf(wallet1.address)).to.equal(promotionAmount); @@ -267,7 +237,7 @@ describe('TwabRewards', () => { ).to.equal(await twabRewards.callStatic.getCurrentEpochId(promotionId)); }); - it('should cancel promotion and still allow users to claim their rewards', async () => { + it('should end promotion and still allow users to claim their rewards', async () => { const promotionId = 1; const epochNumber = 6; const epochIds = [0, 1, 2, 3, 4, 5]; @@ -297,8 +267,8 @@ describe('TwabRewards', () => { .mul(numberOfEpochs) .sub(tokensPerEpoch.mul(epochNumber)); - await expect(twabRewards.cancelPromotion(promotionId, wallet1.address)) - .to.emit(twabRewards, 'PromotionCancelled') + await expect(twabRewards.endPromotion(promotionId, wallet1.address)) + .to.emit(twabRewards, 'PromotionEnded') .withArgs(promotionId, wallet1.address, transferredAmount); expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); @@ -316,36 +286,113 @@ describe('TwabRewards', () => { ); }); - it('should fail to cancel promotion if not owner', async () => { + it('should fail to end promotion if not owner', async () => { await createPromotion(); await expect( - twabRewards.connect(wallet2).cancelPromotion(1, wallet1.address), + twabRewards.connect(wallet2).endPromotion(1, wallet1.address), ).to.be.revertedWith('TwabRewards/only-promo-creator'); }); - it('should fail to cancel an inactive promotion', async () => { + it('should fail to end an inactive promotion', async () => { await createPromotion(); await increaseTime(epochDuration * 13); - await expect(twabRewards.cancelPromotion(1, wallet1.address)).to.be.revertedWith( + await expect(twabRewards.endPromotion(1, wallet1.address)).to.be.revertedWith( 'TwabRewards/promotion-inactive', ); }); - it('should fail to cancel an inexistent promotion', async () => { - await expect(twabRewards.cancelPromotion(1, wallet1.address)).to.be.revertedWith( + it('should fail to end an inexistent promotion', async () => { + await expect(twabRewards.endPromotion(1, wallet1.address)).to.be.revertedWith( 'TwabRewards/invalid-promotion', ); }); - it('should fail to cancel promotion if recipient is address zero', async () => { + it('should fail to end promotion if recipient is address zero', async () => { + await createPromotion(); + + await expect(twabRewards.endPromotion(1, AddressZero)).to.be.revertedWith( + 'TwabRewards/payee-not-zero-addr', + ); + }); + }); + + describe('destroyPromotion()', () => { + it('should destroy a promotion and transfer the correct amount of unclaimed reward tokens', async () => { + const promotionId = 1; + const epochIds = [0, 1]; + + const zeroAmount = toWei('0'); + const wallet2Amount = toWei('750'); + const wallet3Amount = toWei('250'); + + const totalAmount = wallet2Amount.add(wallet3Amount); + + const wallet2ShareOfTickets = wallet2Amount.mul(100).div(totalAmount); + const wallet2RewardAmount = wallet2ShareOfTickets.mul(tokensPerEpoch).div(100); + + const wallet3ShareOfTickets = wallet3Amount.mul(100).div(totalAmount); + const wallet3RewardAmount = wallet3ShareOfTickets.mul(tokensPerEpoch).div(100); + + await ticket.mint(wallet2.address, wallet2Amount); + await ticket.connect(wallet2).delegate(wallet2.address); + await ticket.mint(wallet3.address, wallet3Amount); + await ticket.connect(wallet3).delegate(wallet3.address); + + await createPromotion(); + + await increaseTime(epochDuration * 2); + + await twabRewards.claimRewards(wallet2.address, promotionId, epochIds); + await twabRewards.claimRewards(wallet3.address, promotionId, epochIds); + + await increaseTime(epochDuration * 10 + 5184000); + + const transferredAmount = tokensPerEpoch + .mul(numberOfEpochs) + .sub(wallet2RewardAmount.add(wallet3RewardAmount).mul(2)); + + await expect(twabRewards.destroyPromotion(promotionId, wallet1.address)) + .to.emit(twabRewards, 'PromotionDestroyed') + .withArgs(promotionId, wallet1.address, transferredAmount); + + expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); + }); + + it('should fail if recipient is address zero', async () => { await createPromotion(); - await expect(twabRewards.cancelPromotion(1, AddressZero)).to.be.revertedWith( + await expect(twabRewards.destroyPromotion(1, AddressZero)).to.be.revertedWith( 'TwabRewards/payee-not-zero-addr', ); }); + + it('should fail if not creator', async () => { + await createPromotion(); + + await expect( + twabRewards.connect(wallet2).destroyPromotion(1, wallet1.address), + ).to.be.revertedWith('TwabRewards/only-promo-creator'); + }); + + it('should fail if grace period is not over', async () => { + await createPromotion(); + + await increaseTime(epochDuration * 12); + + await expect( + twabRewards.connect(wallet2).destroyPromotion(1, wallet1.address), + ).to.be.revertedWith('TwabRewards/only-promo-creator'); + }); + + it('should fail if promotion is still active', async () => { + await createPromotion(); + + await expect(twabRewards.destroyPromotion(1, wallet1.address)).to.be.revertedWith( + 'TwabRewards/promotion-active', + ); + }); }); describe('extendPromotion()', async () => { @@ -449,8 +496,7 @@ describe('TwabRewards', () => { await createPromotion(); const promotionId = 1; - const { epochDuration } = - await twabRewards.callStatic.getPromotion(promotionId); + const { epochDuration } = await twabRewards.callStatic.getPromotion(promotionId); await increaseTime(epochDuration * 13); From 551159aaff95559b8c00eab878e80b1b48d71941 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 13 Jan 2022 11:49:29 -0600 Subject: [PATCH 41/55] fix(TwabRewards): fix epochs limit require --- contracts/TwabRewards.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 54ab66c..d2c9098 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -202,7 +202,7 @@ contract TwabRewards is ITwabRewards { uint8 _currentNumberOfEpochs = _promotion.numberOfEpochs; require( - _numberOfEpochs < (type(uint8).max - _currentNumberOfEpochs), + _numberOfEpochs <= (type(uint8).max - _currentNumberOfEpochs), "TwabRewards/epochs-over-limit" ); From 50f57ca5101c7dda78afa66ed932b7a1ac18c77a Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 13 Jan 2022 16:36:45 -0600 Subject: [PATCH 42/55] fix(TwabRewards): fix destroyPromotion gracePeriod --- contracts/TwabRewards.sol | 15 ++++++-- contracts/interfaces/ITwabRewards.sol | 7 ++-- test/TwabRewards.test.ts | 49 ++++++++++----------------- 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index d2c9098..bc94c82 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -109,7 +109,7 @@ contract TwabRewards is ITwabRewards { IERC20 _token, uint64 _startTimestamp, uint256 _tokensPerEpoch, - uint64 _epochDuration, + uint48 _epochDuration, uint8 _numberOfEpochs ) external override returns (uint256) { require(_tokensPerEpoch > 0, "TwabRewards/tokens-not-zero"); @@ -130,6 +130,7 @@ contract TwabRewards is ITwabRewards { _startTimestamp, _numberOfEpochs, _epochDuration, + uint48(block.timestamp), _token, _tokensPerEpoch, _amount @@ -178,6 +179,11 @@ contract TwabRewards is ITwabRewards { "TwabRewards/promotion-active" ); + require( + (_promotion.createdAt + _gracePeriod) < block.timestamp, + "TwabRewards/grace-period-active" + ); + uint256 _rewardsUnclaimed = _promotion.rewardsUnclaimed; delete _promotions[_promotionId]; @@ -360,7 +366,8 @@ contract TwabRewards is ITwabRewards { returns (uint256) { unchecked { - return _promotion.startTimestamp + (_promotion.epochDuration * _promotion.numberOfEpochs); + return + _promotion.startTimestamp + (_promotion.epochDuration * _promotion.numberOfEpochs); } } @@ -379,7 +386,9 @@ contract TwabRewards is ITwabRewards { if (block.timestamp > _promotion.startTimestamp) { unchecked { // elapsedTimestamp / epochDurationTimestamp - _currentEpochId = (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; + _currentEpochId = + (block.timestamp - _promotion.startTimestamp) / + _promotion.epochDuration; } } diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index 49aa4a1..e4242a1 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -16,6 +16,7 @@ interface ITwabRewards { @param startTimestamp Timestamp at which the promotion starts @param numberOfEpochs Number of epochs the promotion will last for @param epochDuration Duration of one epoch in seconds + @param createdAt Timestamp at which the promotion was created @param token Address of the token to be distributed as reward @param tokensPerEpoch Number of tokens to be distributed per epoch @param rewardsUnclaimed Amount of rewards that have not been claimed yet @@ -24,7 +25,8 @@ interface ITwabRewards { address creator; uint64 startTimestamp; uint8 numberOfEpochs; - uint64 epochDuration; + uint48 epochDuration; + uint48 createdAt; IERC20 token; uint256 tokensPerEpoch; uint256 rewardsUnclaimed; @@ -48,7 +50,7 @@ interface ITwabRewards { IERC20 _token, uint64 _startTimestamp, uint256 _tokensPerEpoch, - uint64 _epochDuration, + uint48 _epochDuration, uint8 _numberOfEpochs ) external returns (uint256); @@ -64,6 +66,7 @@ interface ITwabRewards { /** @notice Delete an inactive promotion and send promotion tokens back to the creator. @dev Will send back all the tokens that have not been claimed yet by users. + @dev This function will revert if the promotion is still active. @dev This function will revert if the grace period is not over yet. @param _promotionId Promotion id to destroy @param _to Address that will receive the remaining tokens if there are any left diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index b2153ea..5e92611 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -132,22 +132,6 @@ describe('TwabRewards', () => { expect(promotion.rewardsUnclaimed).to.equal(tokensPerEpoch.mul(numberOfEpochs)); }); - it('should succeed to create a new promotion even if start timestamp is before block timestamp', async () => { - const startTimestamp = (await ethers.provider.getBlock('latest')).timestamp - 1; - - await expect( - createPromotion( - rewardToken, - tokensPerEpoch, - epochDuration, - numberOfEpochs, - startTimestamp, - ), - ) - .to.emit(twabRewards, 'PromotionCreated') - .withArgs(1); - }); - it('should fail to create a new promotion if reward token is a fee on transfer token', async () => { await expect(createPromotion(mockRewardToken)).to.be.revertedWith( 'TwabRewards/promo-amount-diff', @@ -376,21 +360,28 @@ describe('TwabRewards', () => { ).to.be.revertedWith('TwabRewards/only-promo-creator'); }); - it('should fail if grace period is not over', async () => { + it('should fail if promotion is still active', async () => { await createPromotion(); - await increaseTime(epochDuration * 12); - - await expect( - twabRewards.connect(wallet2).destroyPromotion(1, wallet1.address), - ).to.be.revertedWith('TwabRewards/only-promo-creator'); + await expect(twabRewards.destroyPromotion(1, wallet1.address)).to.be.revertedWith( + 'TwabRewards/promotion-active', + ); }); - it('should fail if promotion is still active', async () => { - await createPromotion(); + it('should fail if trying to destroy a promotion that was just created', async () => { + const startTimestamp = + (await ethers.provider.getBlock('latest')).timestamp - (epochDuration * 21); + + await createPromotion( + rewardToken, + tokensPerEpoch, + epochDuration, + numberOfEpochs, + startTimestamp, + ); await expect(twabRewards.destroyPromotion(1, wallet1.address)).to.be.revertedWith( - 'TwabRewards/promotion-active', + 'TwabRewards/grace-period-active', ); }); }); @@ -483,7 +474,7 @@ describe('TwabRewards', () => { for (let index = 0; index < numberOfEpochs; index++) { if (index > 0) { - await increaseTime(epochDuration.toNumber()); + await increaseTime(epochDuration); } expect(await twabRewards.getRemainingRewards(promotionId)).to.equal( @@ -502,12 +493,6 @@ describe('TwabRewards', () => { expect(await twabRewards.getRemainingRewards(promotionId)).to.equal(0); }); - - it('should revert if promotion id passed is inexistent', async () => { - await expect(twabRewards.callStatic.getPromotion(1)).to.be.revertedWith( - 'TwabRewards/invalid-promotion', - ); - }); }); describe('getCurrentEpochId()', async () => { From b5bb9dd76a30c4ac15838b2b3dfb0598fc416d5f Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 13 Jan 2022 16:47:54 -0600 Subject: [PATCH 43/55] fix(PrizeDistributionFactory): fix flaky tests --- test/PrizeDistributionFactory.test.ts | 65 ++++++++++++++++----------- test/TwabRewards.test.ts | 2 +- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/test/PrizeDistributionFactory.test.ts b/test/PrizeDistributionFactory.test.ts index 5a43b82..22e716d 100644 --- a/test/PrizeDistributionFactory.test.ts +++ b/test/PrizeDistributionFactory.test.ts @@ -14,7 +14,6 @@ const { parseEther: toWei } = utils; describe('PrizeDistributionFactory', () => { let wallet1: SignerWithAddress; let wallet2: SignerWithAddress; - let wallet3: SignerWithAddress; let prizeDistributionFactory: Contract; let maxPickCost: BigNumber; @@ -27,7 +26,7 @@ describe('PrizeDistributionFactory', () => { let ticket: MockContract; before(async () => { - [wallet1, wallet2, wallet3] = await getSigners(); + [wallet1, wallet2] = await getSigners(); prizeDistributionFactoryFactory = await ethers.getContractFactory( 'PrizeDistributionFactory', ); @@ -234,43 +233,57 @@ describe('PrizeDistributionFactory', () => { describe('calculatePrizeDistribution()', () => { it('should require that the passed total supply is gte ticket total supply', async () => { await setupMocks({}, {}, toWei('100')); + await expect( prizeDistributionFactory.calculatePrizeDistribution(1, toWei('10')), - ).be.revertedWith('PDF/invalid-network-supply'); + ).to.be.revertedWith('PDF/invalid-network-supply'); }); it('should copy in all of the prize tier values', async () => { await setupMocks(); - expect( - toObject( - await prizeDistributionFactory.calculatePrizeDistribution(1, toWei('1000')), - ), - ).to.deep.include(createPrizeDistribution({ matchCardinality: 4 })); + + const prizeDistributionObject = toObject( + await prizeDistributionFactory.calculatePrizeDistribution(1, toWei('1000')), + ); + + const prizeDistribution = createPrizeDistribution({ matchCardinality: 4 }); + + expect(JSON.stringify(prizeDistributionObject)).to.equal( + JSON.stringify(prizeDistribution), + ); }); it('ensure minimum cardinality is 1', async () => { await setupMocks({}, {}, toWei('0')); - expect( - toObject(await prizeDistributionFactory.calculatePrizeDistribution(1, toWei('0'))), - ).to.deep.include( - createPrizeDistribution({ - matchCardinality: 1, - numberOfPicks: toWei('0'), - }), + + const prizeDistributionObject = toObject( + await prizeDistributionFactory.calculatePrizeDistribution(1, toWei('0')), + ); + + const prizeDistribution = createPrizeDistribution({ + matchCardinality: 1, + numberOfPicks: toWei('0'), + }); + + expect(JSON.stringify(prizeDistributionObject)).to.equal( + JSON.stringify(prizeDistribution), ); }); it('should handle when tickets equal total supply', async () => { await setupMocks({}, {}, toWei('100')); - expect( - toObject( - await prizeDistributionFactory.calculatePrizeDistribution(1, toWei('100')), - ), - ).to.deep.include( - createPrizeDistribution({ - matchCardinality: 3, - numberOfPicks: BigNumber.from(64), - }), + + const prizeDistributionObject = toObject( + await prizeDistributionFactory.calculatePrizeDistribution(1, toWei('100')), + ); + + const prizeDistribution = createPrizeDistribution({ + matchCardinality: 3, + numberOfPicks: BigNumber.from(64), + }); + + expect(JSON.stringify(prizeDistributionObject)).to.equal( + JSON.stringify(prizeDistribution), ); }); }); @@ -286,7 +299,7 @@ describe('PrizeDistributionFactory', () => { it('requires the manager or owner', async () => { await expect( - prizeDistributionFactory.connect(wallet3).pushPrizeDistribution(1, toWei('1000')), + prizeDistributionFactory.connect(wallet2).pushPrizeDistribution(1, toWei('1000')), ).to.be.revertedWith('Manageable/caller-not-manager-or-owner'); }); }); @@ -302,7 +315,7 @@ describe('PrizeDistributionFactory', () => { it('requires the owner', async () => { await expect( - prizeDistributionFactory.connect(wallet3).setPrizeDistribution(1, toWei('1000')), + prizeDistributionFactory.connect(wallet2).setPrizeDistribution(1, toWei('1000')), ).to.be.revertedWith('Ownable/caller-not-owner'); }); }); diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 5e92611..f7801ab 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -370,7 +370,7 @@ describe('TwabRewards', () => { it('should fail if trying to destroy a promotion that was just created', async () => { const startTimestamp = - (await ethers.provider.getBlock('latest')).timestamp - (epochDuration * 21); + (await ethers.provider.getBlock('latest')).timestamp - epochDuration * 21; await createPromotion( rewardToken, From 6f2237b273cc8705010a99395b9ffaf0be3d59f6 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 13 Jan 2022 18:31:08 -0600 Subject: [PATCH 44/55] fix(TwabRewards): add epoch number to PromotionEnded event --- contracts/TwabRewards.sol | 13 ++++++++++--- contracts/interfaces/ITwabRewards.sol | 2 +- test/TwabRewards.test.ts | 6 +++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index bc94c82..fba8ee3 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -55,8 +55,14 @@ contract TwabRewards is ITwabRewards { @param promotionId Id of the promotion being ended @param recipient Address of the recipient that will receive the remaining rewards @param amount Amount of tokens transferred to the recipient + @param epochNumber Epoch number at which the promotion ended */ - event PromotionEnded(uint256 indexed promotionId, address indexed recipient, uint256 amount); + event PromotionEnded( + uint256 indexed promotionId, + address indexed recipient, + uint256 amount, + uint8 epochNumber + ); /** @notice Emitted when a promotion is destroyed. @@ -157,12 +163,13 @@ contract TwabRewards is ITwabRewards { _requirePromotionCreator(_promotion); _requirePromotionActive(_promotion); - _promotions[_promotionId].numberOfEpochs = uint8(_getCurrentEpochId(_promotion)); + uint8 _epochNumber = uint8(_getCurrentEpochId(_promotion)); + _promotions[_promotionId].numberOfEpochs = _epochNumber; uint256 _remainingRewards = _getRemainingRewards(_promotion); _promotion.token.safeTransfer(_to, _remainingRewards); - emit PromotionEnded(_promotionId, _to, _remainingRewards); + emit PromotionEnded(_promotionId, _to, _remainingRewards, _epochNumber); return true; } diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index e4242a1..061b528 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -56,7 +56,7 @@ interface ITwabRewards { /** @notice End currently active promotion and send promotion tokens back to the creator. - @dev Will only send back tokens from the epochs that have not yet started. + @dev Will only send back tokens from the epochs that have not completed. @param _promotionId Promotion id to end @param _to Address that will receive the remaining tokens if there are any left @return true if operation was successful diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index f7801ab..6397340 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -182,7 +182,7 @@ describe('TwabRewards', () => { await expect(twabRewards.endPromotion(promotionId, wallet1.address)) .to.emit(twabRewards, 'PromotionEnded') - .withArgs(promotionId, wallet1.address, transferredAmount); + .withArgs(promotionId, wallet1.address, transferredAmount, index); expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); @@ -212,7 +212,7 @@ describe('TwabRewards', () => { await expect(twabRewards.endPromotion(promotionId, wallet1.address)) .to.emit(twabRewards, 'PromotionEnded') - .withArgs(promotionId, wallet1.address, promotionAmount); + .withArgs(promotionId, wallet1.address, promotionAmount, 0); expect(await rewardToken.balanceOf(wallet1.address)).to.equal(promotionAmount); @@ -253,7 +253,7 @@ describe('TwabRewards', () => { await expect(twabRewards.endPromotion(promotionId, wallet1.address)) .to.emit(twabRewards, 'PromotionEnded') - .withArgs(promotionId, wallet1.address, transferredAmount); + .withArgs(promotionId, wallet1.address, transferredAmount, epochNumber); expect(await rewardToken.balanceOf(wallet1.address)).to.equal(transferredAmount); From ee0c9368a2a5541c65d89083b49b2b8c9a7b2358 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 13 Jan 2022 18:43:28 -0600 Subject: [PATCH 45/55] fix(TwabRewards): fix flaky tests --- test/TwabRewards.test.ts | 140 ++++++++++++++++++++++++--------------- 1 file changed, 88 insertions(+), 52 deletions(-) diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 6397340..323b180 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -555,21 +555,25 @@ describe('TwabRewards', () => { await createPromotion(); await increaseTime(epochDuration * 3); - expect( - await twabRewards.callStatic.getRewardsAmount( - wallet2.address, - promotionId, - epochIds, - ), - ).to.deep.equal([wallet2RewardAmount, wallet2RewardAmount, wallet2RewardAmount]); + const wallet2RewardsAmount = await twabRewards.callStatic.getRewardsAmount( + wallet2.address, + promotionId, + epochIds, + ); - expect( - await twabRewards.callStatic.getRewardsAmount( - wallet3.address, - promotionId, - epochIds, - ), - ).to.deep.equal([wallet3RewardAmount, wallet3RewardAmount, wallet3RewardAmount]); + wallet2RewardsAmount.map((rewardAmount: BigNumber) => { + expect(rewardAmount).to.equal(wallet2RewardAmount); + }) + + const wallet3RewardsAmount = await twabRewards.callStatic.getRewardsAmount( + wallet3.address, + promotionId, + epochIds, + ) + + wallet3RewardsAmount.map((rewardAmount: BigNumber) => { + expect(rewardAmount).to.equal(wallet3RewardAmount); + }) }); it('should decrease rewards amount if user delegate in the middle of an epoch', async () => { @@ -608,25 +612,33 @@ describe('TwabRewards', () => { await increaseTime(halfEpoch + 1); - expect( - await twabRewards.callStatic.getRewardsAmount( - wallet2.address, - promotionId, - epochIds, - ), - ).to.deep.equal([ - wallet2RewardAmount, - wallet2RewardAmount, - wallet2RewardAmount.add(wallet3HalfRewardAmount), - ]); + const wallet2RewardsAmount = await twabRewards.callStatic.getRewardsAmount( + wallet2.address, + promotionId, + epochIds, + ); - expect( - await twabRewards.callStatic.getRewardsAmount( - wallet3.address, - promotionId, - epochIds, - ), - ).to.deep.equal([wallet3RewardAmount, wallet3RewardAmount, wallet3HalfRewardAmount]); + wallet2RewardsAmount.map((rewardAmount: BigNumber, index: number) => { + if (index !== 2) { + expect(rewardAmount).to.equal(wallet2RewardAmount); + } else { + expect(rewardAmount).to.equal(wallet2RewardAmount.add(wallet3HalfRewardAmount)); + } + }) + + const wallet3RewardsAmount = await twabRewards.callStatic.getRewardsAmount( + wallet3.address, + promotionId, + epochIds, + ) + + wallet3RewardsAmount.map((rewardAmount: BigNumber, index: number) => { + if (index !== 2) { + expect(rewardAmount).to.equal(wallet3RewardAmount); + } else { + expect(rewardAmount).to.equal(wallet3HalfRewardAmount); + } + }) }); it('should return 0 for epochs that have already been claimed', async () => { @@ -661,21 +673,33 @@ describe('TwabRewards', () => { .to.emit(twabRewards, 'RewardsClaimed') .withArgs(promotionId, [2], wallet3.address, wallet3RewardAmount); - expect( - await twabRewards.callStatic.getRewardsAmount( - wallet2.address, - promotionId, - epochIds, - ), - ).to.deep.equal([zeroAmount, wallet2RewardAmount, zeroAmount]); + const wallet2RewardsAmount = await twabRewards.callStatic.getRewardsAmount( + wallet2.address, + promotionId, + epochIds, + ); - expect( - await twabRewards.callStatic.getRewardsAmount( - wallet3.address, - promotionId, - epochIds, - ), - ).to.deep.equal([wallet3RewardAmount, wallet3RewardAmount, zeroAmount]); + wallet2RewardsAmount.map((rewardAmount: BigNumber, index: number) => { + if (index !== 1) { + expect(rewardAmount).to.equal(zeroAmount); + } else { + expect(rewardAmount).to.equal(wallet2RewardAmount); + } + }) + + const wallet3RewardsAmount = await twabRewards.callStatic.getRewardsAmount( + wallet3.address, + promotionId, + epochIds, + ) + + wallet3RewardsAmount.map((rewardAmount: BigNumber, index: number) => { + if (index !== 2) { + expect(rewardAmount).to.equal(wallet3RewardAmount); + } else { + expect(rewardAmount).to.equal(zeroAmount); + } + }) }); it('should return 0 if user has no tickets delegated to him', async () => { @@ -687,9 +711,15 @@ describe('TwabRewards', () => { await createPromotion(); await increaseTime(epochDuration * 3); - expect( - await twabRewards.callStatic.getRewardsAmount(wallet2.address, 1, ['0', '1', '2']), - ).to.deep.equal([zeroAmount, zeroAmount, zeroAmount]); + const wallet2RewardsAmount = await twabRewards.callStatic.getRewardsAmount( + wallet2.address, + 1, + ['0', '1', '2'], + ); + + wallet2RewardsAmount.map((rewardAmount: BigNumber) => { + expect(rewardAmount).to.equal(zeroAmount); + }) }); it('should return 0 if ticket average total supplies is 0', async () => { @@ -698,9 +728,15 @@ describe('TwabRewards', () => { await createPromotion(); await increaseTime(epochDuration * 3); - expect( - await twabRewards.callStatic.getRewardsAmount(wallet2.address, 1, ['0', '1', '2']), - ).to.deep.equal([zeroAmount, zeroAmount, zeroAmount]); + const wallet2RewardsAmount = await twabRewards.callStatic.getRewardsAmount( + wallet2.address, + 1, + ['0', '1', '2'], + ); + + wallet2RewardsAmount.map((rewardAmount: BigNumber) => { + expect(rewardAmount).to.equal(zeroAmount); + }) }); it('should fail to get rewards amount if one or more epochs are not over yet', async () => { From 51b0b2c30062e97be0d17748bb03b15ec399d47a Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 18 Jan 2022 18:03:33 -0600 Subject: [PATCH 46/55] fix(TwabRewards): turn grace period into constant --- contracts/TwabRewards.sol | 8 ++++---- test/TwabRewards.test.ts | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index fba8ee3..5a192d7 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -27,8 +27,8 @@ contract TwabRewards is ITwabRewards { /// @notice Prize pool ticket for which the promotions are created. ITicket public immutable ticket; - /// @notice Period during which the promotion owner can't destroy an inactive promotion. - uint32 internal _gracePeriod = 5184000; // 60 days in seconds + /// @notice Period during which the promotion owner can't destroy a promotion. + uint32 public constant GRACE_PERIOD = 60 days; /// @notice Settings of each promotion. mapping(uint256 => Promotion) internal _promotions; @@ -182,12 +182,12 @@ contract TwabRewards is ITwabRewards { _requirePromotionCreator(_promotion); require( - (_getPromotionEndTimestamp(_promotion) + _gracePeriod) < block.timestamp, + (_getPromotionEndTimestamp(_promotion) + GRACE_PERIOD) < block.timestamp, "TwabRewards/promotion-active" ); require( - (_promotion.createdAt + _gracePeriod) < block.timestamp, + (_promotion.createdAt + GRACE_PERIOD) < block.timestamp, "TwabRewards/grace-period-active" ); diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 323b180..9a5945f 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -563,17 +563,17 @@ describe('TwabRewards', () => { wallet2RewardsAmount.map((rewardAmount: BigNumber) => { expect(rewardAmount).to.equal(wallet2RewardAmount); - }) + }); const wallet3RewardsAmount = await twabRewards.callStatic.getRewardsAmount( wallet3.address, promotionId, epochIds, - ) + ); wallet3RewardsAmount.map((rewardAmount: BigNumber) => { expect(rewardAmount).to.equal(wallet3RewardAmount); - }) + }); }); it('should decrease rewards amount if user delegate in the middle of an epoch', async () => { @@ -624,13 +624,13 @@ describe('TwabRewards', () => { } else { expect(rewardAmount).to.equal(wallet2RewardAmount.add(wallet3HalfRewardAmount)); } - }) + }); const wallet3RewardsAmount = await twabRewards.callStatic.getRewardsAmount( wallet3.address, promotionId, epochIds, - ) + ); wallet3RewardsAmount.map((rewardAmount: BigNumber, index: number) => { if (index !== 2) { @@ -638,7 +638,7 @@ describe('TwabRewards', () => { } else { expect(rewardAmount).to.equal(wallet3HalfRewardAmount); } - }) + }); }); it('should return 0 for epochs that have already been claimed', async () => { @@ -685,13 +685,13 @@ describe('TwabRewards', () => { } else { expect(rewardAmount).to.equal(wallet2RewardAmount); } - }) + }); const wallet3RewardsAmount = await twabRewards.callStatic.getRewardsAmount( wallet3.address, promotionId, epochIds, - ) + ); wallet3RewardsAmount.map((rewardAmount: BigNumber, index: number) => { if (index !== 2) { @@ -699,7 +699,7 @@ describe('TwabRewards', () => { } else { expect(rewardAmount).to.equal(zeroAmount); } - }) + }); }); it('should return 0 if user has no tickets delegated to him', async () => { @@ -719,7 +719,7 @@ describe('TwabRewards', () => { wallet2RewardsAmount.map((rewardAmount: BigNumber) => { expect(rewardAmount).to.equal(zeroAmount); - }) + }); }); it('should return 0 if ticket average total supplies is 0', async () => { @@ -736,7 +736,7 @@ describe('TwabRewards', () => { wallet2RewardsAmount.map((rewardAmount: BigNumber) => { expect(rewardAmount).to.equal(zeroAmount); - }) + }); }); it('should fail to get rewards amount if one or more epochs are not over yet', async () => { From ac29ad436f9460f7eef04eb838a2306387498b24 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 18 Jan 2022 18:10:50 -0600 Subject: [PATCH 47/55] fix(TwabRewards): remove _extendedNumberOfEpochs --- contracts/TwabRewards.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 5a192d7..7aa034b 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -219,8 +219,7 @@ contract TwabRewards is ITwabRewards { "TwabRewards/epochs-over-limit" ); - uint8 _extendedNumberOfEpochs = _currentNumberOfEpochs + _numberOfEpochs; - _promotions[_promotionId].numberOfEpochs = _extendedNumberOfEpochs; + _promotions[_promotionId].numberOfEpochs = _currentNumberOfEpochs + _numberOfEpochs; uint256 _amount; @@ -392,7 +391,6 @@ contract TwabRewards is ITwabRewards { if (block.timestamp > _promotion.startTimestamp) { unchecked { - // elapsedTimestamp / epochDurationTimestamp _currentEpochId = (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; @@ -459,7 +457,6 @@ contract TwabRewards is ITwabRewards { } unchecked { - // _tokensPerEpoch * _numberOfEpochsLeft return _promotion.tokensPerEpoch * (_promotion.numberOfEpochs - _getCurrentEpochId(_promotion)); From 86e130f68e5beac4fa7078fa560d475cfb679582 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 18 Jan 2022 18:17:19 -0600 Subject: [PATCH 48/55] fix(TwabRewards): use named params for Promotion --- contracts/TwabRewards.sol | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 7aa034b..4e927ad 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -131,16 +131,16 @@ contract TwabRewards is ITwabRewards { _amount = _tokensPerEpoch * _numberOfEpochs; } - _promotions[_nextPromotionId] = Promotion( - msg.sender, - _startTimestamp, - _numberOfEpochs, - _epochDuration, - uint48(block.timestamp), - _token, - _tokensPerEpoch, - _amount - ); + _promotions[_nextPromotionId] = Promotion({ + creator: msg.sender, + startTimestamp: _startTimestamp, + numberOfEpochs: _numberOfEpochs, + epochDuration: _epochDuration, + createdAt: uint48(block.timestamp), + token: _token, + tokensPerEpoch: _tokensPerEpoch, + rewardsUnclaimed: _amount + }); uint256 _beforeBalance = _token.balanceOf(address(this)); From f2fce3efe5158e62a50a68ff5c4e2fa1d9b0cf47 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 18 Jan 2022 18:35:13 -0600 Subject: [PATCH 49/55] fix(TwabRewards): fix grace period require --- contracts/TwabRewards.sol | 17 +++++++++-------- test/TwabRewards.test.ts | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 4e927ad..9e96be5 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -181,15 +181,16 @@ contract TwabRewards is ITwabRewards { Promotion memory _promotion = _getPromotion(_promotionId); _requirePromotionCreator(_promotion); - require( - (_getPromotionEndTimestamp(_promotion) + GRACE_PERIOD) < block.timestamp, - "TwabRewards/promotion-active" - ); + uint256 _promotionEndTimestamp = _getPromotionEndTimestamp(_promotion); + uint256 _promotionCreatedAt = _promotion.createdAt; - require( - (_promotion.createdAt + GRACE_PERIOD) < block.timestamp, - "TwabRewards/grace-period-active" - ); + uint256 _gracePeriodEndTimestamp = ( + _promotionEndTimestamp < _promotionCreatedAt + ? _promotionCreatedAt + : _promotionEndTimestamp + ) + GRACE_PERIOD; + + require(block.timestamp >= _gracePeriodEndTimestamp, "TwabRewards/grace-period-active"); uint256 _rewardsUnclaimed = _promotion.rewardsUnclaimed; delete _promotions[_promotionId]; diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 9a5945f..159336e 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -364,7 +364,7 @@ describe('TwabRewards', () => { await createPromotion(); await expect(twabRewards.destroyPromotion(1, wallet1.address)).to.be.revertedWith( - 'TwabRewards/promotion-active', + 'TwabRewards/grace-period-active', ); }); From 6d9967e08508a1756bbed3b27927dfa28f32cc2a Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 18 Jan 2022 15:43:43 -0600 Subject: [PATCH 50/55] fix(TwabRewards): fix flaky tests --- test/TwabRewards.test.ts | 20 +++++++++----------- test/utils/increaseTime.ts | 5 +++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 159336e..47e4c10 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -6,10 +6,9 @@ import { expect } from 'chai'; import { BigNumber, Contract, ContractFactory } from 'ethers'; import { ethers } from 'hardhat'; -import { increaseTime as increaseTimeUtil } from './utils/increaseTime'; +import { increaseTime as increaseTimeUtil, setTime as setTimeUtil } from './utils/increaseTime'; -// We add 1 cause promotion starts a latest timestamp + 1 -const increaseTime = (time: number) => increaseTimeUtil(provider, time + 1); +const increaseTime = (time: number) => increaseTimeUtil(provider, time); const { constants, getContractFactory, getSigners, provider, utils, Wallet } = ethers; const { parseEther: toWei } = utils; @@ -33,6 +32,8 @@ describe('TwabRewards', () => { let createPromotionTimestamp: number; + const setTime = (time: number) => setTimeUtil(provider, createPromotionTimestamp + time); + before(async () => { [wallet1, wallet2, wallet3] = await getSigners(); @@ -81,7 +82,7 @@ describe('TwabRewards', () => { if (startTimestamp) { createPromotionTimestamp = startTimestamp; } else { - createPromotionTimestamp = (await ethers.provider.getBlock('latest')).timestamp + 1; + createPromotionTimestamp = (await provider.getBlock('latest')).timestamp; } return await twabRewards.createPromotion( @@ -602,15 +603,12 @@ describe('TwabRewards', () => { await createPromotion(); - const timestampAfterCreate = (await ethers.provider.getBlock('latest')).timestamp; - const elapsedTimeCreate = timestampAfterCreate - timestampBeforeCreate; - // We adjust time to delegate right in the middle of epoch 3 - await increaseTime(epochDuration * 2 + halfEpoch - (elapsedTimeCreate - 1)); + await setTime(epochDuration * 2 + halfEpoch - 1); await ticket.connect(wallet3).delegate(wallet2.address); - await increaseTime(halfEpoch + 1); + await increaseTime(halfEpoch); const wallet2RewardsAmount = await twabRewards.callStatic.getRewardsAmount( wallet2.address, @@ -852,11 +850,11 @@ describe('TwabRewards', () => { await createPromotion(); // We adjust time to delegate right in the middle of epoch 3 - await increaseTime(epochDuration * 2 + halfEpoch - 2); + await setTime((epochDuration * 2) + halfEpoch - 1) await ticket.connect(wallet3).delegate(wallet2.address); - await increaseTime(halfEpoch + 1); + await increaseTime(halfEpoch); await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds)) .to.emit(twabRewards, 'RewardsClaimed') diff --git a/test/utils/increaseTime.ts b/test/utils/increaseTime.ts index 0ffb089..e989004 100644 --- a/test/utils/increaseTime.ts +++ b/test/utils/increaseTime.ts @@ -4,3 +4,8 @@ export const increaseTime = async (provider: providers.JsonRpcProvider, time: nu await provider.send("evm_increaseTime", [time]); await provider.send("evm_mine", []); }; + +export const setTime = async (provider: providers.JsonRpcProvider, time: number) => { + await provider.send("evm_setNextBlockTimestamp", [time]); + await provider.send("evm_mine", []); +}; From 0c7a7d0876b19120f26d68f58b5b6be4a092225e Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 19 Jan 2022 10:35:10 -0600 Subject: [PATCH 51/55] fix(TwabRewards): remove unchecked that overflow --- contracts/TwabRewards.sol | 20 +++++--------------- test/TwabRewards.test.ts | 2 +- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 9e96be5..ee0aaca 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -125,11 +125,7 @@ contract TwabRewards is ITwabRewards { uint256 _nextPromotionId = _latestPromotionId + 1; _latestPromotionId = _nextPromotionId; - uint256 _amount; - - unchecked { - _amount = _tokensPerEpoch * _numberOfEpochs; - } + uint256 _amount = _tokensPerEpoch * _numberOfEpochs; _promotions[_nextPromotionId] = Promotion({ creator: msg.sender, @@ -222,11 +218,7 @@ contract TwabRewards is ITwabRewards { _promotions[_promotionId].numberOfEpochs = _currentNumberOfEpochs + _numberOfEpochs; - uint256 _amount; - - unchecked { - _amount = _numberOfEpochs * _promotion.tokensPerEpoch; - } + uint256 _amount = _numberOfEpochs * _promotion.tokensPerEpoch; _promotions[_promotionId].rewardsUnclaimed += _amount; _promotion.token.safeTransferFrom(msg.sender, address(this), _amount); @@ -457,11 +449,9 @@ contract TwabRewards is ITwabRewards { return 0; } - unchecked { - return - _promotion.tokensPerEpoch * - (_promotion.numberOfEpochs - _getCurrentEpochId(_promotion)); - } + return + _promotion.tokensPerEpoch * + (_promotion.numberOfEpochs - _getCurrentEpochId(_promotion)); } /** diff --git a/test/TwabRewards.test.ts b/test/TwabRewards.test.ts index 47e4c10..c2e5e90 100644 --- a/test/TwabRewards.test.ts +++ b/test/TwabRewards.test.ts @@ -850,7 +850,7 @@ describe('TwabRewards', () => { await createPromotion(); // We adjust time to delegate right in the middle of epoch 3 - await setTime((epochDuration * 2) + halfEpoch - 1) + await setTime(epochDuration * 2 + halfEpoch - 1); await ticket.connect(wallet3).delegate(wallet2.address); From 0d76674b04b6168a8c5f7df3b737a690dd712ac1 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 21 Dec 2021 18:58:14 +0100 Subject: [PATCH 52/55] fix(TwabRewards): fix natspec doc --- contracts/TwabRewards.sol | 116 +++++++++++++------------- contracts/interfaces/ITwabRewards.sol | 80 +++++++++--------- 2 files changed, 98 insertions(+), 98 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index ee0aaca..04791e9 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -45,9 +45,9 @@ contract TwabRewards is ITwabRewards { /* ============ Events ============ */ /** - @notice Emitted when a promotion is created. - @param promotionId Id of the newly created promotion - */ + * @notice Emitted when a promotion is created. + * @param promotionId Id of the newly created promotion + */ event PromotionCreated(uint256 indexed promotionId); /** @@ -65,11 +65,11 @@ contract TwabRewards is ITwabRewards { ); /** - @notice Emitted when a promotion is destroyed. - @param promotionId Id of the promotion being destroyed - @param recipient Address of the recipient that will receive the unclaimed rewards - @param amount Amount of tokens transferred to the recipient - */ + * @notice Emitted when a promotion is destroyed. + * @param promotionId Id of the promotion being destroyed + * @param recipient Address of the recipient that will receive the unclaimed rewards + * @param amount Amount of tokens transferred to the recipient + */ event PromotionDestroyed( uint256 indexed promotionId, address indexed recipient, @@ -77,19 +77,19 @@ contract TwabRewards is ITwabRewards { ); /** - @notice Emitted when a promotion is extended. - @param promotionId Id of the promotion being extended - @param numberOfEpochs Number of epochs the promotion has been extended by - */ + * @notice Emitted when a promotion is extended. + * @param promotionId Id of the promotion being extended + * @param numberOfEpochs Number of epochs the promotion has been extended by + */ event PromotionExtended(uint256 indexed promotionId, uint256 numberOfEpochs); /** - @notice Emitted when rewards have been claimed. - @param promotionId Id of the promotion for which epoch rewards were claimed - @param epochIds Ids of the epochs being claimed - @param user Address of the user for which the rewards were claimed - @param amount Amount of tokens transferred to the recipient address - */ + * @notice Emitted when rewards have been claimed. + * @param promotionId Id of the promotion for which epoch rewards were claimed + * @param epochIds Ids of the epochs being claimed + * @param user Address of the user for which the rewards were claimed + * @param amount Amount of tokens transferred to the recipient address + */ event RewardsClaimed( uint256 indexed promotionId, uint8[] epochIds, @@ -100,9 +100,9 @@ contract TwabRewards is ITwabRewards { /* ============ Constructor ============ */ /** - @notice Constructor of the contract. - @param _ticket Prize Pool ticket address for which the promotions will be created - */ + * @notice Constructor of the contract. + * @param _ticket Prize Pool ticket address for which the promotions will be created + */ constructor(ITicket _ticket) { _requireTicket(_ticket); ticket = _ticket; @@ -299,9 +299,9 @@ contract TwabRewards is ITwabRewards { /* ============ Internal Functions ============ */ /** - @notice Determine if address passed is actually a ticket. - @param _ticket Address to check - */ + * @notice Determine if address passed is actually a ticket. + * @param _ticket Address to check + */ function _requireTicket(ITicket _ticket) internal view { require(address(_ticket) != address(0), "TwabRewards/ticket-not-zero-addr"); @@ -316,17 +316,17 @@ contract TwabRewards is ITwabRewards { } /** - @notice Allow a promotion to be created or extended only by a positive number of epochs. - @param _numberOfEpochs Number of epochs to check - */ + * @notice Allow a promotion to be created or extended only by a positive number of epochs. + * @param _numberOfEpochs Number of epochs to check + */ function _requireNumberOfEpochs(uint8 _numberOfEpochs) internal pure { require(_numberOfEpochs > 0, "TwabRewards/epochs-not-zero"); } /** - @notice Determine if a promotion is active. - @param _promotion Promotion to check - */ + * @notice Determine if a promotion is active. + * @param _promotion Promotion to check + */ function _requirePromotionActive(Promotion memory _promotion) internal view { require( _getPromotionEndTimestamp(_promotion) > block.timestamp, @@ -335,18 +335,18 @@ contract TwabRewards is ITwabRewards { } /** - @notice Determine if msg.sender is the promotion creator. - @param _promotion Promotion to check - */ + * @notice Determine if msg.sender is the promotion creator. + * @param _promotion Promotion to check + */ function _requirePromotionCreator(Promotion memory _promotion) internal view { require(msg.sender == _promotion.creator, "TwabRewards/only-promo-creator"); } /** - @notice Get settings for a specific promotion. - @dev Will revert if the promotion does not exist. - @param _promotionId Promotion id to get settings for - @return Promotion settings + * @notice Get settings for a specific promotion. + * @dev Will revert if the promotion does not exist. + * @param _promotionId Promotion id to get settings for + * @return Promotion settings */ function _getPromotion(uint256 _promotionId) internal view returns (Promotion memory) { Promotion memory _promotion = _promotions[_promotionId]; @@ -394,14 +394,14 @@ contract TwabRewards is ITwabRewards { } /** - @notice Get reward amount for a specific user. - @dev Rewards can only be calculated once the epoch is over. - @dev Will revert if `_epochId` is over the total number of epochs or if epoch is not over. - @dev Will return 0 if the user average balance of tickets is 0. - @param _user User to get reward amount for - @param _promotion Promotion from which the epoch is - @param _epochId Epoch id to get reward amount for - @return Reward amount + * @notice Get reward amount for a specific user. + * @dev Rewards can only be calculated once the epoch is over. + * @dev Will revert if `_epochId` is over the total number of epochs or if epoch is not over. + * @dev Will return 0 if the user average balance of tickets is 0. + * @param _user User to get reward amount for + * @param _promotion Promotion from which the epoch is + * @param _epochId Epoch id to get reward amount for + * @return Reward amount */ function _calculateRewardAmount( address _user, @@ -440,9 +440,9 @@ contract TwabRewards is ITwabRewards { } /** - @notice Get the total amount of tokens left to be rewarded. - @param _promotion Promotion to get the total amount of tokens left to be rewarded for - @return Amount of tokens left to be rewarded + * @notice Get the total amount of tokens left to be rewarded. + * @param _promotion Promotion to get the total amount of tokens left to be rewarded for + * @return Amount of tokens left to be rewarded */ function _getRemainingRewards(Promotion memory _promotion) internal view returns (uint256) { if (block.timestamp > _getPromotionEndTimestamp(_promotion)) { @@ -455,16 +455,16 @@ contract TwabRewards is ITwabRewards { } /** - @notice Set boolean value for a specific epoch. - @dev Bits are stored in a uint256 from right to left. + * @notice Set boolean value for a specific epoch. + * @dev Bits are stored in a uint256 from right to left. Let's take the example of the following 8 bits word. 0110 0011 To set the boolean value to 1 for the epoch id 2, we need to create a mask by shifting 1 to the left by 2 bits. We get: 0000 0001 << 2 = 0000 0100 We then OR the mask with the word to set the value. We get: 0110 0011 | 0000 0100 = 0110 0111 - @param _userClaimedEpochs Tightly packed epoch ids with their boolean values - @param _epochId Id of the epoch to set the boolean for - @return Tightly packed epoch ids with the newly boolean value set + * @param _userClaimedEpochs Tightly packed epoch ids with their boolean values + * @param _epochId Id of the epoch to set the boolean for + * @return Tightly packed epoch ids with the newly boolean value set */ function _updateClaimedEpoch(uint256 _userClaimedEpochs, uint8 _epochId) internal @@ -475,17 +475,17 @@ contract TwabRewards is ITwabRewards { } /** - @notice Check if rewards of an epoch for a given promotion have already been claimed by the user. - @dev Bits are stored in a uint256 from right to left. + * @notice Check if rewards of an epoch for a given promotion have already been claimed by the user. + * @dev Bits are stored in a uint256 from right to left. Let's take the example of the following 8 bits word. 0110 0111 To retrieve the boolean value for the epoch id 2, we need to shift the word to the right by 2 bits. We get: 0110 0111 >> 2 = 0001 1001 We then get the value of the last bit by masking with 1. We get: 0001 1001 & 0000 0001 = 0000 0001 = 1 We then return the boolean value true since the last bit is 1. - @param _userClaimedEpochs Record of epochs already claimed by the user - @param _epochId Epoch id to check - @return true if the rewards have already been claimed for the given epoch, false otherwise + * @param _userClaimedEpochs Record of epochs already claimed by the user + * @param _epochId Epoch id to check + * @return true if the rewards have already been claimed for the given epoch, false otherwise */ function _isClaimedEpoch(uint256 _userClaimedEpochs, uint8 _epochId) internal diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index 061b528..5dc2263 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -33,18 +33,18 @@ interface ITwabRewards { } /** - @notice Create a new promotion. - @dev For sake of simplicity, `msg.sender` will be the creator of the promotion. - @dev `_latestPromotionId` starts at 0 and is incremented by 1 for each new promotion. + * @notice Create a new promotion. + * @dev For sake of simplicity, `msg.sender` will be the creator of the promotion. + * @dev `_latestPromotionId` starts at 0 and is incremented by 1 for each new promotion. So the first promotion will have id 1, the second 2, etc. - @dev The transaction will revert if the amount of reward tokens provided is not equal to `_tokensPerEpoch * _numberOfEpochs`. + @dev The transaction will revert if the amount of reward tokens provided is not equal to `_tokensPerEpoch * _numberOfEpochs`. This scenario could happen if the token supplied is a fee on transfer one. - @param _token Address of the token to be distributed - @param _startTimestamp Timestamp at which the promotion starts - @param _tokensPerEpoch Number of tokens to be distributed per epoch - @param _epochDuration Duration of one epoch in seconds - @param _numberOfEpochs Number of epochs the promotion will last for - @return Id of the newly created promotion + * @param _token Address of the token to be distributed + * @param _startTimestamp Timestamp at which the promotion starts + * @param _tokensPerEpoch Number of tokens to be distributed per epoch + * @param _epochDuration Duration of one epoch in seconds + * @param _numberOfEpochs Number of epochs the promotion will last for + * @return Id of the newly created promotion */ function createPromotion( IERC20 _token, @@ -75,21 +75,21 @@ interface ITwabRewards { function destroyPromotion(uint256 _promotionId, address _to) external returns (bool); /** - @notice Extend promotion by adding more epochs. - @param _promotionId Promotion id to extend - @param _numberOfEpochs Number of epochs to add - @return true if the operation was successful + * @notice Extend promotion by adding more epochs. + * @param _promotionId Promotion id to extend + * @param _numberOfEpochs Number of epochs to add + * @return true if the operation was successful */ function extendPromotion(uint256 _promotionId, uint8 _numberOfEpochs) external returns (bool); /** - @notice Claim rewards for a given promotion and epoch. - @dev Rewards can be claimed on behalf of a user. - @dev Rewards can only be claimed for a past epoch. - @param _user Address of the user to claim rewards for - @param _promotionId Promotion id to claim rewards for - @param _epochIds Epoch ids to claim rewards for - @return Amount of rewards claimed + * @notice Claim rewards for a given promotion and epoch. + * @dev Rewards can be claimed on behalf of a user. + * @dev Rewards can only be claimed for a past epoch. + * @param _user Address of the user to claim rewards for + * @param _promotionId Promotion id to claim rewards for + * @param _epochIds Epoch ids to claim rewards for + * @return Amount of rewards claimed */ function claimRewards( address _user, @@ -98,37 +98,37 @@ interface ITwabRewards { ) external returns (uint256); /** - @notice Get settings for a specific promotion. - @param _promotionId Promotion id to get settings for - @return Promotion settings + * @notice Get settings for a specific promotion. + * @param _promotionId Promotion id to get settings for + * @return Promotion settings */ function getPromotion(uint256 _promotionId) external view returns (Promotion memory); /** - @notice Get the current epoch id of a promotion. - @dev Epoch ids and their boolean values are tightly packed and stored in a uint256, so epoch id starts at 0. - @param _promotionId Promotion id to get current epoch for - @return Epoch id + * @notice Get the current epoch id of a promotion. + * @dev Epoch ids and their boolean values are tightly packed and stored in a uint256, so epoch id starts at 0. + * @param _promotionId Promotion id to get current epoch for + * @return Epoch id */ function getCurrentEpochId(uint256 _promotionId) external view returns (uint256); /** - @notice Get the total amount of tokens left to be rewarded. - @param _promotionId Promotion id to get the total amount of tokens left to be rewarded for - @return Amount of tokens left to be rewarded + * @notice Get the total amount of tokens left to be rewarded. + * @param _promotionId Promotion id to get the total amount of tokens left to be rewarded for + * @return Amount of tokens left to be rewarded */ function getRemainingRewards(uint256 _promotionId) external view returns (uint256); /** - @notice Get amount of tokens to be rewarded for a given epoch. - @dev Rewards amount can only be retrieved for epochs that are over. - @dev Will revert if `_epochId` is over the total number of epochs or if epoch is not over. - @dev Will return 0 if the user average balance of tickets is 0. - @dev Will be 0 if user has already claimed rewards for the epoch. - @param _user Address of the user to get amount of rewards for - @param _promotionId Promotion id from which the epoch is - @param _epochIds Epoch ids to get reward amount for - @return Amount of tokens to be rewarded + * @notice Get amount of tokens to be rewarded for a given epoch. + * @dev Rewards amount can only be retrieved for epochs that are over. + * @dev Will revert if `_epochId` is over the total number of epochs or if epoch is not over. + * @dev Will return 0 if the user average balance of tickets is 0. + * @dev Will be 0 if user has already claimed rewards for the epoch. + * @param _user Address of the user to get amount of rewards for + * @param _promotionId Promotion id from which the epoch is + * @param _epochIds Epoch ids to get reward amount for + * @return Amount of tokens to be rewarded */ function getRewardsAmount( address _user, From c3774145ca0fc80d7a84c31752c92d7f703cfabc Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 22 Dec 2021 11:34:31 +0100 Subject: [PATCH 53/55] fix(TwabRewards): fix doc template return values --- contracts/TwabRewards.sol | 48 ++++++++------- contracts/interfaces/ITwabRewards.sol | 88 +++++++++++++-------------- templates/contract.hbs | 8 +-- 3 files changed, 74 insertions(+), 70 deletions(-) diff --git a/contracts/TwabRewards.sol b/contracts/TwabRewards.sol index 04791e9..54ab801 100644 --- a/contracts/TwabRewards.sol +++ b/contracts/TwabRewards.sol @@ -33,13 +33,17 @@ contract TwabRewards is ITwabRewards { /// @notice Settings of each promotion. mapping(uint256 => Promotion) internal _promotions; - /// @notice Latest recorded promotion id. - /// @dev Starts at 0 and is incremented by 1 for each new promotion. So the first promotion will have id 1, the second 2, etc. + /** + * @notice Latest recorded promotion id. + * @dev Starts at 0 and is incremented by 1 for each new promotion. So the first promotion will have id 1, the second 2, etc. + */ uint256 internal _latestPromotionId; - /// @notice Keeps track of claimed rewards per user. - /// @dev _claimedEpochs[promotionId][user] => claimedEpochs - /// @dev We pack epochs claimed by a user into a uint256. So we can't store more than 256 epochs. + /** + * @notice Keeps track of claimed rewards per user. + * @dev _claimedEpochs[promotionId][user] => claimedEpochs + * @dev We pack epochs claimed by a user into a uint256. So we can't store more than 256 epochs. + */ mapping(uint256 => mapping(address => uint256)) internal _claimedEpochs; /* ============ Events ============ */ @@ -51,12 +55,12 @@ contract TwabRewards is ITwabRewards { event PromotionCreated(uint256 indexed promotionId); /** - @notice Emitted when a promotion is ended. - @param promotionId Id of the promotion being ended - @param recipient Address of the recipient that will receive the remaining rewards - @param amount Amount of tokens transferred to the recipient - @param epochNumber Epoch number at which the promotion ended - */ + * @notice Emitted when a promotion is ended. + * @param promotionId Id of the promotion being ended + * @param recipient Address of the recipient that will receive the remaining rewards + * @param amount Amount of tokens transferred to the recipient + * @param epochNumber Epoch number at which the promotion ended + */ event PromotionEnded( uint256 indexed promotionId, address indexed recipient, @@ -355,10 +359,10 @@ contract TwabRewards is ITwabRewards { } /** - @notice Compute promotion end timestamp. - @param _promotion Promotion to compute end timestamp for - @return Promotion end timestamp - */ + * @notice Compute promotion end timestamp. + * @param _promotion Promotion to compute end timestamp for + * @return Promotion end timestamp + */ function _getPromotionEndTimestamp(Promotion memory _promotion) internal pure @@ -371,13 +375,13 @@ contract TwabRewards is ITwabRewards { } /** - @notice Get the current epoch id of a promotion. - @dev Epoch ids and their boolean values are tightly packed and stored in a uint256, so epoch id starts at 0. - @dev We return the current epoch id if the promotion has not ended. - If the current time is before the promotion start timestamp, we return 0. - Otherwise, we return the epoch id at the current timestamp. This could be greater than the number of epochs of the promotion. - @param _promotion Promotion to get current epoch for - @return Epoch id + * @notice Get the current epoch id of a promotion. + * @dev Epoch ids and their boolean values are tightly packed and stored in a uint256, so epoch id starts at 0. + * @dev We return the current epoch id if the promotion has not ended. + * If the current timestamp is before the promotion start timestamp, we return 0. + * Otherwise, we return the epoch id at the current timestamp. This could be greater than the number of epochs of the promotion. + * @param _promotion Promotion to get current epoch for + * @return Epoch id */ function _getCurrentEpochId(Promotion memory _promotion) internal view returns (uint256) { uint256 _currentEpochId; diff --git a/contracts/interfaces/ITwabRewards.sol b/contracts/interfaces/ITwabRewards.sol index 5dc2263..ccaa7c3 100644 --- a/contracts/interfaces/ITwabRewards.sol +++ b/contracts/interfaces/ITwabRewards.sol @@ -11,15 +11,15 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; */ interface ITwabRewards { /** - @notice Struct to keep track of each promotion's settings. - @param creator Addresss of the promotion creator - @param startTimestamp Timestamp at which the promotion starts - @param numberOfEpochs Number of epochs the promotion will last for - @param epochDuration Duration of one epoch in seconds - @param createdAt Timestamp at which the promotion was created - @param token Address of the token to be distributed as reward - @param tokensPerEpoch Number of tokens to be distributed per epoch - @param rewardsUnclaimed Amount of rewards that have not been claimed yet + * @notice Struct to keep track of each promotion's settings. + * @param creator Addresss of the promotion creator + * @param startTimestamp Timestamp at which the promotion starts + * @param numberOfEpochs Number of epochs the promotion will last for + * @param epochDuration Duration of one epoch in seconds + * @param createdAt Timestamp at which the promotion was created + * @param token Address of the token to be distributed as reward + * @param tokensPerEpoch Number of tokens to be distributed per epoch + * @param rewardsUnclaimed Amount of rewards that have not been claimed yet */ struct Promotion { address creator; @@ -33,19 +33,19 @@ interface ITwabRewards { } /** - * @notice Create a new promotion. - * @dev For sake of simplicity, `msg.sender` will be the creator of the promotion. - * @dev `_latestPromotionId` starts at 0 and is incremented by 1 for each new promotion. - So the first promotion will have id 1, the second 2, etc. - @dev The transaction will revert if the amount of reward tokens provided is not equal to `_tokensPerEpoch * _numberOfEpochs`. - This scenario could happen if the token supplied is a fee on transfer one. - * @param _token Address of the token to be distributed - * @param _startTimestamp Timestamp at which the promotion starts - * @param _tokensPerEpoch Number of tokens to be distributed per epoch - * @param _epochDuration Duration of one epoch in seconds - * @param _numberOfEpochs Number of epochs the promotion will last for - * @return Id of the newly created promotion - */ + * @notice Creates a new promotion. + * @dev For sake of simplicity, `msg.sender` will be the creator of the promotion. + * @dev `_latestPromotionId` starts at 0 and is incremented by 1 for each new promotion. + * So the first promotion will have id 1, the second 2, etc. + * @dev The transaction will revert if the amount of reward tokens provided is not equal to `_tokensPerEpoch * _numberOfEpochs`. + * This scenario could happen if the token supplied is a fee on transfer one. + * @param _token Address of the token to be distributed + * @param _startTimestamp Timestamp at which the promotion starts + * @param _tokensPerEpoch Number of tokens to be distributed per epoch + * @param _epochDuration Duration of one epoch in seconds + * @param _numberOfEpochs Number of epochs the promotion will last for + * @return Id of the newly created promotion + */ function createPromotion( IERC20 _token, uint64 _startTimestamp, @@ -55,30 +55,30 @@ interface ITwabRewards { ) external returns (uint256); /** - @notice End currently active promotion and send promotion tokens back to the creator. - @dev Will only send back tokens from the epochs that have not completed. - @param _promotionId Promotion id to end - @param _to Address that will receive the remaining tokens if there are any left - @return true if operation was successful + * @notice End currently active promotion and send promotion tokens back to the creator. + * @dev Will only send back tokens from the epochs that have not completed. + * @param _promotionId Promotion id to end + * @param _to Address that will receive the remaining tokens if there are any left + * @return true if operation was successful */ function endPromotion(uint256 _promotionId, address _to) external returns (bool); /** - @notice Delete an inactive promotion and send promotion tokens back to the creator. - @dev Will send back all the tokens that have not been claimed yet by users. - @dev This function will revert if the promotion is still active. - @dev This function will revert if the grace period is not over yet. - @param _promotionId Promotion id to destroy - @param _to Address that will receive the remaining tokens if there are any left - @return true if operation was successful + * @notice Delete an inactive promotion and send promotion tokens back to the creator. + * @dev Will send back all the tokens that have not been claimed yet by users. + * @dev This function will revert if the promotion is still active. + * @dev This function will revert if the grace period is not over yet. + * @param _promotionId Promotion id to destroy + * @param _to Address that will receive the remaining tokens if there are any left + * @return True if operation was successful */ function destroyPromotion(uint256 _promotionId, address _to) external returns (bool); /** * @notice Extend promotion by adding more epochs. - * @param _promotionId Promotion id to extend + * @param _promotionId Id of the promotion to extend * @param _numberOfEpochs Number of epochs to add - * @return true if the operation was successful + * @return True if the operation was successful */ function extendPromotion(uint256 _promotionId, uint8 _numberOfEpochs) external returns (bool); @@ -87,9 +87,9 @@ interface ITwabRewards { * @dev Rewards can be claimed on behalf of a user. * @dev Rewards can only be claimed for a past epoch. * @param _user Address of the user to claim rewards for - * @param _promotionId Promotion id to claim rewards for + * @param _promotionId Id of the promotion to claim rewards for * @param _epochIds Epoch ids to claim rewards for - * @return Amount of rewards claimed + * @return Total amount of rewards claimed */ function claimRewards( address _user, @@ -99,7 +99,7 @@ interface ITwabRewards { /** * @notice Get settings for a specific promotion. - * @param _promotionId Promotion id to get settings for + * @param _promotionId Id of the promotion to get settings for * @return Promotion settings */ function getPromotion(uint256 _promotionId) external view returns (Promotion memory); @@ -107,14 +107,14 @@ interface ITwabRewards { /** * @notice Get the current epoch id of a promotion. * @dev Epoch ids and their boolean values are tightly packed and stored in a uint256, so epoch id starts at 0. - * @param _promotionId Promotion id to get current epoch for - * @return Epoch id + * @param _promotionId Id of the promotion to get current epoch for + * @return Current epoch id of the promotion */ function getCurrentEpochId(uint256 _promotionId) external view returns (uint256); /** * @notice Get the total amount of tokens left to be rewarded. - * @param _promotionId Promotion id to get the total amount of tokens left to be rewarded for + * @param _promotionId Id of the promotion to get the total amount of tokens left to be rewarded for * @return Amount of tokens left to be rewarded */ function getRemainingRewards(uint256 _promotionId) external view returns (uint256); @@ -126,9 +126,9 @@ interface ITwabRewards { * @dev Will return 0 if the user average balance of tickets is 0. * @dev Will be 0 if user has already claimed rewards for the epoch. * @param _user Address of the user to get amount of rewards for - * @param _promotionId Promotion id from which the epoch is + * @param _promotionId Id of the promotion from which the epoch is * @param _epochIds Epoch ids to get reward amount for - * @return Amount of tokens to be rewarded + * @return Amount of tokens per epoch to be rewarded */ function getRewardsAmount( address _user, diff --git a/templates/contract.hbs b/templates/contract.hbs index b999dde..1e897c2 100644 --- a/templates/contract.hbs +++ b/templates/contract.hbs @@ -45,10 +45,10 @@ All members: {{members}} |`{{param}}` | {{#lookup ../args.types @index}}{{/lookup}} | {{ description }}{{/natspec.params}}{{/if}} {{#if natspec.returns}} #### Return Values: -| Name | Type | Description | -| :----------------------------- | :------------ | :--------------------------------------------------------------------------- | +| Type | Description | +| :------------ | :--------------------------------------------------------------------------- | {{#natspec.returns}} -|`{{param}}`| {{#lookup ../args.types @index}}{{/lookup}} | {{{description}}}{{/natspec.returns}}{{/if}} +| {{#lookup ../outputs.types @index}}{{/lookup}} | {{param}} {{{description}}}{{/natspec.returns}}{{/if}} {{/unless}} {{/functions}} {{#if events}} @@ -71,4 +71,4 @@ All members: {{members}} | :----------------------------- | :------------ | :--------------------------------------------- | {{#natspec.params}} |`{{param}}`| {{#lookup ../args.types @index}}{{/lookup}} | {{{description}}}{{/natspec.params}}{{/if}} -{{/events}} \ No newline at end of file +{{/events}} From 09506c46bd6ce05ee9fe9f90e57d33913191b642 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 20 Jan 2022 11:11:22 -0600 Subject: [PATCH 54/55] fix(README): update documentation --- README.md | 59 +++++++++++-------------------------------------------- 1 file changed, 11 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index cc1acc2..abf9e51 100644 --- a/README.md +++ b/README.md @@ -11,31 +11,23 @@ [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/JFBPMxv5tr) [![Twitter](https://badgen.net/badge/icon/twitter?icon=twitter&label)](https://twitter.com/PoolTogether_) -**Documention**
-https://docs.pooltogether.com +**Documentation**
+- [PrizeDistributionFactory](https://v4.docs.pooltogether.com/protocol/reference/v4-periphery/PrizeDistributionFactory) +- [PrizeFlush](https://v4.docs.pooltogether.com/protocol/reference/v4-periphery/PrizeFlush) +- [PrizeTierHistory](https://v4.docs.pooltogether.com/protocol/reference/v4-periphery/PrizeTierHistory) +- [TwabRewards](https://v4.docs.pooltogether.com/protocol/reference/v4-periphery/TwabRewards) -**Deplyoments**
-- [Ethereum](https://docs.pooltogether.com/resources/networks/ethereum) -- [Matic](https://docs.pooltogether.com/resources/networks/matic) - -# Overview -- [PrizeFlush](/contracts/PrizeFlush.sol) - -The `PrizeFlush` contract wraps multiple draw completion steps: capturing/distributing interest, and automatically transferring the captured interest to PrizeDistributor. The contract is **simple in nature** and is expeced to evolve with the V4 rollout and governance requirements. - -As the draw and prize distribution params are optimized with continual hypothesis and testing, the PoolTogether Community and Governance process can "codify" the rules for an optimal interest distribution - adding intermediary steps to fine-tuning prize models and optimal interes allocation. - -**Core and Timelock contracts:** - -- https://github.com/pooltogether/v4-core -- https://github.com/pooltogether/v4-timelocks +**Deployments**
+- [Ethereum](https://v4.docs.pooltogether.com/protocol/reference/deployments/mainnet#mainnet) +- [Polygon](https://v4.docs.pooltogether.com/protocol/reference/deployments/mainnet#polygon) +- [Avalanche](https://v4.docs.pooltogether.com/protocol/reference/deployments/mainnet#avalanche) # Getting Started The project is made available as a NPM package. ```sh -$ yarn add @pooltogether/pooltogether-contracts +$ yarn add @pooltogether/v4-periphery ``` The repo can be cloned from Github for contributions. @@ -54,13 +46,11 @@ We use [direnv](https://direnv.net/) to manage environment variables. You'll li cp .envrc.example .envrv ``` -To run fork scripts, deploy or perform any operation with a mainnet/testnet node you will need an Infura API key. - # Testing We use [Hardhat](https://hardhat.dev) and [hardhat-deploy](https://github.com/wighawag/hardhat-deploy) -To run unit & integration tests: +To run unit tests: ```sh $ yarn test @@ -71,30 +61,3 @@ To run coverage: ```sh $ yarn coverage ``` - -# Fork Testing - -Ensure your environment variables are set up. Make sure your Alchemy URL is set. Now start a local fork: - -```sh -$ yarn start-fork -``` - -Setup account impersonation and transfer eth: - -```sh -$ ./scripts/setup.sh -``` - -# Deployment - -## Deploy Locally - -Start a local node and deploy the top-level contracts: - -```bash -$ yarn start -``` - -NOTE: When you run this command it will reset the local blockchain. - From 318e5ac1f046260548ec413b68096776df029ad6 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 20 Jan 2022 11:12:14 -0600 Subject: [PATCH 55/55] 1.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6852c81..4117814 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pooltogether/v4-periphery", - "version": "1.1.0", + "version": "1.2.0", "description": "PoolTogether V4 Periphery", "main": "index.js", "license": "MIT",