From ccceaf45f5d0a1b5b60065a3c52e792d129b7371 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 27 May 2022 18:54:20 -0500 Subject: [PATCH] feat(PrizeDistV2): add missing events --- contracts/DrawCalculatorV3.sol | 18 ++- contracts/PrizeDistributorV2.sol | 129 +++++++++++++++------ contracts/interfaces/IDrawCalculatorV3.sol | 10 +- test/PrizeDistributorV2.test.ts | 90 ++++++++------ 4 files changed, 173 insertions(+), 74 deletions(-) diff --git a/contracts/DrawCalculatorV3.sol b/contracts/DrawCalculatorV3.sol index dbb51704..f7f70dc2 100644 --- a/contracts/DrawCalculatorV3.sol +++ b/contracts/DrawCalculatorV3.sol @@ -88,14 +88,20 @@ contract DrawCalculatorV3 is IDrawCalculatorV3, Manageable { address _user, uint32[] calldata _drawIds, bytes calldata _pickIndicesForDraws - ) external view override returns (uint256[] memory prizesAwardable, bytes memory prizeCounts) { + ) external view override returns ( + uint256[] memory prizesAwardable, + bytes memory prizeCounts, + uint64[][] memory drawPickIndices + ) { uint64[][] memory _pickIndices = abi.decode(_pickIndicesForDraws, (uint64 [][])); require(_pickIndices.length == _drawIds.length, "DrawCalc/invalid-pick-indices"); // User address is hashed once. bytes32 _userRandomNumber = keccak256(abi.encodePacked(_user)); - return _calculatePrizesAwardable( + drawPickIndices = _pickIndices; + + (prizesAwardable, prizeCounts) = _calculatePrizesAwardable( _ticket, _user, _userRandomNumber, @@ -189,15 +195,19 @@ contract DrawCalculatorV3 is IDrawCalculatorV3, Manageable { bytes32 _userRandomNumber, uint32[] memory _drawIds, uint64[][] memory _pickIndicesForDraws - ) internal view returns (uint256[] memory prizesAwardable, bytes memory prizeCounts) { + ) internal view returns ( + uint256[] memory prizesAwardable, + bytes memory prizeCounts + ) { // READ list of IDrawBeacon.Draw using the drawIds from drawBuffer IDrawBeacon.Draw[] memory _draws = drawBuffer.getDraws(_drawIds); + uint256 _drawsLength = _draws.length; uint256[] memory _prizesAwardable = new uint256[](_drawIds.length); uint256[][] memory _prizeCounts = new uint256[][](_drawIds.length); // Calculate prizes awardable for each Draw passed - for (uint32 _drawIndex = 0; _drawIndex < _draws.length; _drawIndex++) { + for (uint32 _drawIndex = 0; _drawIndex < _drawsLength; _drawIndex++) { IDrawBeacon.Draw memory _draw = _draws[_drawIndex]; IPrizeConfigHistory.PrizeConfig memory _prizeConfig = prizeConfigHistory.getPrizeConfig(_draw.drawId); diff --git a/contracts/PrizeDistributorV2.sol b/contracts/PrizeDistributorV2.sol index 9bb2f3b6..37456727 100644 --- a/contracts/PrizeDistributorV2.sol +++ b/contracts/PrizeDistributorV2.sol @@ -5,13 +5,14 @@ pragma solidity 0.8.6; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@pooltogether/owner-manager-contracts/contracts/Manageable.sol"; + import "./interfaces/IDrawCalculatorV3.sol"; /** * @title PoolTogether V4 PrizeDistributorV2 * @author PoolTogether Inc Team * @notice The PrizeDistributorV2 contract holds Tickets (captured interest) and distributes tickets to users with winning draw claims. - PrizeDistributorV2 uses an external IDrawCalculatorV3 to validate a users draw claim, before awarding payouts. To prevent users + PrizeDistributorV2 uses an external IDrawCalculatorV3 to validate a users draw claim, before awarding payouts. To prevent users from reclaiming prizes, a payout history for each draw claim is mapped to user accounts. Reclaiming a draw can occur if an "optimal" prize was not included in previous claim pick indices and the new claims updated payout is greater then the previous prize distributor claim payout. @@ -24,14 +25,21 @@ contract PrizeDistributorV2 is Manageable { * @param user User address receiving draw claim payouts * @param drawId Draw id that was paid out * @param payout Payout for draw + * @param pickIndices Pick indices for draw */ - event ClaimedDraw(address indexed user, uint32 indexed drawId, uint256 payout); + event ClaimedDraw( + address indexed user, + uint32 indexed drawId, + uint256 payout, + uint64[] pickIndices + ); /** * @notice Emit when IDrawCalculatorV3 is set. + * @param caller Address who has set the new DrawCalculator * @param calculator IDrawCalculatorV3 address */ - event DrawCalculatorSet(IDrawCalculatorV3 indexed calculator); + event DrawCalculatorSet(address indexed caller, IDrawCalculatorV3 indexed calculator); /** * @notice Emit when Token is set. @@ -41,9 +49,9 @@ contract PrizeDistributorV2 is Manageable { /** * @notice Emit when ERC20 tokens are withdrawn. - * @param token ERC20 token transferred. - * @param to Address that received funds. - * @param amount Amount of tokens transferred. + * @param token ERC20 token transferred + * @param to Address that received funds + * @param amount Amount of tokens transferred */ event ERC20Withdrawn(IERC20 indexed token, address indexed to, uint256 amount); @@ -58,44 +66,64 @@ contract PrizeDistributorV2 is Manageable { /// @notice Maps users => drawId => paid out balance mapping(address => mapping(uint256 => uint256)) internal userDrawPayouts; - /// @notice The vault that stores the prize tokens - address public vault; + /// @notice The tokenVault that stores the prize tokens + address public tokenVault; - /* ============ Initialize ============ */ + /* ============ Constructor ============ */ /** - * @notice Initialize PrizeDistributorV2 smart contract. - * @param _owner Owner address - * @param _token Token address - * @param _drawCalculator IDrawCalculatorV3 address + * @notice Constructs PrizeDistributorV2 smart contract. + * @param _owner Contract owner address + * @param _token Address of the token being used to pay out prizes + * @param _drawCalculator Address of the DrawCalculatorV3 contract which computes draw payouts + * @param _tokenVault Address of the TokenVault contract that holds the `token` being used to pay out prizes */ constructor( address _owner, IERC20 _token, IDrawCalculatorV3 _drawCalculator, - address _vault + address _tokenVault ) Ownable(_owner) { + require(_owner != address(0), "PDistV2/owner-not-zero-address"); + require(address(_token) != address(0), "PDistV2/token-not-zero-address"); + _setDrawCalculator(_drawCalculator); - require(address(_token) != address(0), "PrizeDistributorV2/token-not-zero-address"); + token = _token; - vault = _vault; + tokenVault = _tokenVault; + emit TokenSet(_token); } /* ============ External Functions ============ */ + /** + * @notice Claim prize payout(s) by submitting valid drawId(s) and winning pick indice(s). The user address + is used as the "seed" phrase to generate random numbers. + * @dev The claim function is public and any wallet may execute claim on behalf of another user. + Prizes are always paid out to the designated user account and not the caller (msg.sender). + Claiming prizes is not limited to a single transaction. Reclaiming can be executed + subsequentially if an "optimal" prize was not included in previous claim pick indices. The + payout difference for the new claim is calculated during the award process and transfered to user. + * @param _ticket Address of the Ticket to claim prizes for + * @param _user Address of the user to claim rewards for. Does NOT need to be msg.sender + * @param _drawIds Draw IDs from global DrawBuffer reference + * @param _data Draws pick indices + * @return Total claim payout. May include calculations from multiple draws. + */ function claim( ITicket _ticket, address _user, uint32[] calldata _drawIds, bytes calldata _data ) external returns (uint256) { - uint256 totalPayout; - - (uint256[] memory drawPayouts, ) = drawCalculator.calculate(_ticket, _user, _drawIds, _data); // neglect the prizeCounts since we are not interested in them here + + (uint256[] memory drawPayouts, , uint64[][] memory drawPickIndices) = drawCalculator + .calculate(_ticket, _user, _drawIds, _data); uint256 drawPayoutsLength = drawPayouts.length; + for (uint256 payoutIndex = 0; payoutIndex < drawPayoutsLength; payoutIndex++) { uint32 drawId = _drawIds[payoutIndex]; uint256 payout = drawPayouts[payoutIndex]; @@ -103,7 +131,7 @@ contract PrizeDistributorV2 is Manageable { uint256 payoutDiff = 0; // helpfully short-circuit, in case the user screwed something up. - require(payout > oldPayout, "PrizeDistributorV2/zero-payout"); + require(payout > oldPayout, "PDistV2/zero-payout"); unchecked { payoutDiff = payout - oldPayout; @@ -113,7 +141,7 @@ contract PrizeDistributorV2 is Manageable { totalPayout += payoutDiff; - emit ClaimedDraw(_user, drawId, payoutDiff); + emit ClaimedDraw(_user, drawId, payoutDiff, drawPickIndices[payoutIndex]); } _awardPayout(_user, totalPayout); @@ -121,13 +149,21 @@ contract PrizeDistributorV2 is Manageable { return totalPayout; } + /** + * @notice Transfer ERC20 tokens out of contract to recipient address. + * @dev Only callable by contract owner or manager. + * @param _erc20Token Address of the ERC20 token to transfer + * @param _to Address of the recipient of the tokens + * @param _amount Amount of tokens to transfer + * @return true if operation is successful. + */ function withdrawERC20( IERC20 _erc20Token, address _to, uint256 _amount ) external onlyManagerOrOwner returns (bool) { - require(_to != address(0), "PrizeDistributorV2/recipient-not-zero-address"); - require(address(_erc20Token) != address(0), "PrizeDistributorV2/ERC20-not-zero-address"); + require(_to != address(0), "PDistV2/to-not-zero-address"); + require(address(_erc20Token) != address(0), "PDistV2/ERC20-not-zero-address"); _erc20Token.safeTransfer(_to, _amount); @@ -136,22 +172,36 @@ contract PrizeDistributorV2 is Manageable { return true; } + /** + * @notice Read global DrawCalculator address. + * @return IDrawCalculatorV3 + */ function getDrawCalculator() external view returns (IDrawCalculatorV3) { return drawCalculator; } - function getDrawPayoutBalanceOf(address _user, uint32 _drawId) - external - view - returns (uint256) - { + /** + * @notice Get the amount that a user has already been paid out for a draw + * @param _user User address + * @param _drawId Draw ID + */ + function getDrawPayoutBalanceOf(address _user, uint32 _drawId) external view returns (uint256) { return _getDrawPayoutBalanceOf(_user, _drawId); } + /** + * @notice Read global Token address. + * @return IERC20 + */ function getToken() external view returns (IERC20) { return token; } + /** + * @notice Sets DrawCalculator reference contract. + * @param _newCalculator DrawCalculator address + * @return New DrawCalculator address + */ function setDrawCalculator(IDrawCalculatorV3 _newCalculator) external onlyManagerOrOwner @@ -163,6 +213,12 @@ contract PrizeDistributorV2 is Manageable { /* ============ Internal Functions ============ */ + /** + * @notice Get payout balance of a user for a draw ID. + * @param _user Address of the user to get payout balance for + * @param _drawId Draw ID to get payout balance for + * @return Draw ID payout balance + */ function _getDrawPayoutBalanceOf(address _user, uint32 _drawId) internal view @@ -171,6 +227,12 @@ contract PrizeDistributorV2 is Manageable { return userDrawPayouts[_user][_drawId]; } + /** + * @notice Set payout balance for a user and draw ID. + * @param _user Address of the user to set payout balance for + * @param _drawId Draw ID to set payout balance for + * @param _payout Payout amount to set + */ function _setDrawPayoutBalanceOf( address _user, uint32 _drawId, @@ -184,19 +246,18 @@ contract PrizeDistributorV2 is Manageable { * @param _newCalculator IDrawCalculatorV3 address */ function _setDrawCalculator(IDrawCalculatorV3 _newCalculator) internal { - require(address(_newCalculator) != address(0), "PrizeDistributorV2/calc-not-zero"); + require(address(_newCalculator) != address(0), "PDistV2/calc-not-zero-address"); drawCalculator = _newCalculator; - emit DrawCalculatorSet(_newCalculator); + emit DrawCalculatorSet(msg.sender, _newCalculator); } /** * @notice Transfer claimed draw(s) total payout to user. - * @param _to User address - * @param _amount Transfer amount + * @param _to Address of the user to award payout to + * @param _amount Amount of `token` to transfer */ function _awardPayout(address _to, uint256 _amount) internal { - token.safeTransferFrom(vault, _to, _amount); + token.safeTransferFrom(tokenVault, _to, _amount); } - } diff --git a/contracts/interfaces/IDrawCalculatorV3.sol b/contracts/interfaces/IDrawCalculatorV3.sol index 88d3f060..c66089e7 100644 --- a/contracts/interfaces/IDrawCalculatorV3.sol +++ b/contracts/interfaces/IDrawCalculatorV3.sol @@ -21,13 +21,21 @@ interface IDrawCalculatorV3 { * @param data ABI encoded pick indices for all Draws. Expected to be winning picks. Pick indices must be less than the totalUserPicks. * @return List of awardable prize amounts ordered by drawId. * @return List of prize counts ordered by tiers. + * @return Pick indices for each drawId. */ function calculate( ITicket ticket, address user, uint32[] calldata drawIds, bytes calldata data - ) external view returns (uint256[] memory, bytes memory); + ) + external + view + returns ( + uint256[] memory, + bytes memory, + uint64[][] memory + ); /** * @notice Calculates picks for a user for Multiple Draws. diff --git a/test/PrizeDistributorV2.test.ts b/test/PrizeDistributorV2.test.ts index 69ac8edf..c344350c 100644 --- a/test/PrizeDistributorV2.test.ts +++ b/test/PrizeDistributorV2.test.ts @@ -4,7 +4,7 @@ import { utils, constants, Contract, ContractFactory, BigNumber } from 'ethers'; import { ethers, artifacts } from 'hardhat'; const { getSigners } = ethers; -const { parseEther: toWei } = utils; +const { defaultAbiCoder: encoder, parseEther: toWei } = utils; const { AddressZero } = constants; describe('PrizeDistributorV2', () => { @@ -31,49 +31,66 @@ describe('PrizeDistributorV2', () => { let IDrawCalculator = await artifacts.readArtifact('IDrawCalculatorV3'); drawCalculator = await deployMockContract(wallet1, IDrawCalculator.abi); - const PrizeDistributorV2Factory: ContractFactory = await ethers.getContractFactory('PrizeDistributorV2'); + const PrizeDistributorV2Factory: ContractFactory = await ethers.getContractFactory( + 'PrizeDistributorV2', + ); prizeDistributorV2 = await PrizeDistributorV2Factory.deploy( wallet1.address, ticket.address, drawCalculator.address, - vault.address + vault.address, ); await ticket.mint(vault.address, toWei('1000')); - await ticket.connect(vault).approve(prizeDistributorV2.address, toWei('10000000')) + await ticket.connect(vault).approve(prizeDistributorV2.address, toWei('10000000')); }); /** * @description Test claim(ITicket _ticket,address _user,uint32[] calldata _drawIds,bytes calldata _data) function * -= Expected Behavior =- * 1. calculate drawPayouts for user, ticket, drawIds and data(picks) - * FOR - * 2. update Draw payout amount for each drawId - * 3. emit ClaimedDraw event + * FOR + * 2. update Draw payout amount for each drawId + * 3. emit ClaimedDraw event * END FOR * 4. transfer total drawPayouts to user * 5. return totalPayout */ describe('claim(ITicket _ticket,address _user,uint32[] calldata _drawIds,bytes calldata _data))', () => { + let pickIndices: string; + let pickIndicesDecoded: utils.Result; + + beforeEach(() => { + pickIndices = encoder.encode(['uint64[][]'], [[['1']]]); + pickIndicesDecoded = encoder.decode(['uint64[][]'], pickIndices); + }); + it('should SUCCEED to claim and emit ClaimedDraw event', async () => { await drawCalculator.mock.calculate - .withArgs(ticket.address, wallet1.address, [1], '0x') - .returns([toWei('10')], "0x"); - await expect(prizeDistributorV2.claim(ticket.address, wallet1.address, [1], '0x')) + .withArgs(ticket.address, wallet1.address, [1], pickIndices) + .returns([toWei('10')], '0x', pickIndicesDecoded[0]); + + await expect( + prizeDistributorV2.claim(ticket.address, wallet1.address, [1], pickIndices), + ) .to.emit(prizeDistributorV2, 'ClaimedDraw') - .withArgs(wallet1.address, 1, toWei('10')); + .withArgs(wallet1.address, 1, toWei('10'), pickIndicesDecoded[0][0]); }); - it('should SUCCEED to payout the difference if user claims more', async () => { + it('should SUCCEED to payout the difference if user claims more', async () => { await drawCalculator.mock.calculate - .withArgs(ticket.address, wallet1.address, [1], '0x') - .returns([toWei('10')], "0x"); - await prizeDistributorV2.claim(ticket.address, wallet1.address, [1], '0x'); + .withArgs(ticket.address, wallet1.address, [1], pickIndices) + .returns([toWei('10')], '0x', pickIndicesDecoded[0]); + + await prizeDistributorV2.claim(ticket.address, wallet1.address, [1], pickIndices); + await drawCalculator.mock.calculate - .withArgs(ticket.address, wallet1.address, [1], '0x') - .returns([toWei('20')], "0x"); - await prizeDistributorV2.claim(ticket.address, wallet1.address, [1], '0x') + .withArgs(ticket.address, wallet1.address, [1], pickIndices) + .returns([toWei('20')], '0x', pickIndicesDecoded[0]); + + await prizeDistributorV2.claim(ticket.address, wallet1.address, [1], pickIndices); + expect(await prizeDistributorV2.getDrawPayoutBalanceOf(wallet1.address, 1)).to.equal( toWei('20'), ); @@ -81,12 +98,14 @@ describe('PrizeDistributorV2', () => { it('should REVERT on 2.update because the prize was previously claimed', async () => { await drawCalculator.mock.calculate - .withArgs(ticket.address, wallet1.address, [0], '0x') - .returns([toWei('10')], "0x"); - await prizeDistributorV2.claim(ticket.address,wallet1.address, [0], '0x'); - await expect(prizeDistributorV2.claim(ticket.address, wallet1.address, [0], '0x')).to.be.revertedWith( - 'PrizeDistributorV2/zero-payout', - ); + .withArgs(ticket.address, wallet1.address, [0], pickIndices) + .returns([toWei('10')], '0x', pickIndicesDecoded[0]); + + await prizeDistributorV2.claim(ticket.address, wallet1.address, [0], pickIndices); + + await expect( + prizeDistributorV2.claim(ticket.address, wallet1.address, [0], pickIndices), + ).to.be.revertedWith('PDistV2/zero-payout'); }); }); @@ -97,12 +116,11 @@ describe('PrizeDistributorV2', () => { * 2. update global drawCalculator variable * 3. emit DrawCalculatorSet event */ - describe('setDrawCalculator(DrawCalculatorInterface _newCalculator)', () => { - + describe('setDrawCalculator(DrawCalculatorInterface _newCalculator)', () => { it('should SUCCEED updating the drawCalculator global variable', async () => { expect(await prizeDistributorV2.setDrawCalculator(wallet2.address)) .to.emit(prizeDistributorV2, 'DrawCalculatorSet') - .withArgs(wallet2.address); + .withArgs(wallet1.address, wallet2.address); }); it('should REVERT on 1.authorized because wallet is NOT an OWNER or MANAGER', async () => { @@ -114,12 +132,11 @@ describe('PrizeDistributorV2', () => { it('should REVERT on 2.update because the drawCalculator address is NULL', async () => { await expect(prizeDistributorV2.setDrawCalculator(AddressZero)).to.be.revertedWith( - 'PrizeDistributorV2/calc-not-zero', + 'PDistV2/calc-not-zero-address', ); }); }); - /** * @description Test withdrawERC20(IERC20 _erc20Token, address _to, uint256 _amount) function * -= Expected Behavior =- @@ -127,7 +144,7 @@ describe('PrizeDistributorV2', () => { * 2. require _to address is not NULL * 3. require _erc20Token address is not NULL * 4. transfer ERC20 amount to _to address - * 5. emit ERC20Withdrawn event + * 5. emit ERC20Withdrawn event * 6. return true */ @@ -139,7 +156,9 @@ describe('PrizeDistributorV2', () => { }); it('should SUCCEED to withdraw ERC20 tokens as owner', async () => { - await expect(prizeDistributorV2.withdrawERC20(token.address, wallet1.address, withdrawAmount)) + await expect( + prizeDistributorV2.withdrawERC20(token.address, wallet1.address, withdrawAmount), + ) .to.emit(prizeDistributorV2, 'ERC20Withdrawn') .withArgs(token.address, wallet1.address, withdrawAmount); }); @@ -155,15 +174,14 @@ describe('PrizeDistributorV2', () => { it('should REVERT on 2.require because the recipient address is NULL', async () => { await expect( prizeDistributorV2.withdrawERC20(token.address, AddressZero, withdrawAmount), - ).to.be.revertedWith('PrizeDistributorV2/recipient-not-zero-address'); + ).to.be.revertedWith('PDistV2/to-not-zero-address'); }); it('should REVERT on 3.require because the ERC20 address is NULL', async () => { await expect( prizeDistributorV2.withdrawERC20(AddressZero, wallet1.address, withdrawAmount), - ).to.be.revertedWith('PrizeDistributorV2/ERC20-not-zero-address'); + ).to.be.revertedWith('PDistV2/ERC20-not-zero-address'); }); - }); describe('getDrawCalculator()', () => { @@ -174,7 +192,9 @@ describe('PrizeDistributorV2', () => { describe('getDrawPayoutBalanceOf()', () => { it('should return the user payout for draw before claiming a payout', async () => { - expect(await prizeDistributorV2.getDrawPayoutBalanceOf(wallet1.address, 0)).to.equal('0'); + expect(await prizeDistributorV2.getDrawPayoutBalanceOf(wallet1.address, 0)).to.equal( + '0', + ); }); });