diff --git a/src/PrizePool.sol b/src/PrizePool.sol index 6084b4a..415ff21 100644 --- a/src/PrizePool.sol +++ b/src/PrizePool.sol @@ -227,6 +227,9 @@ contract PrizePool is TieredLiquidityDistributor { /// @notice Records the last shutdown withdrawal for an account mapping(address vault => mapping(address user => uint24 drawId)) internal _lastShutdownWithdrawal; + /// @notice The special value for the donator address. Contributions from this address are excluded from the total odds. F2EE because it's free money! + address public constant DONATOR = 0x000000000000000000000000000000000000F2EE; + /// @notice The token that is being contributed and awarded as prizes. IERC20 public immutable prizeToken; @@ -333,7 +336,7 @@ contract PrizePool is TieredLiquidityDistributor { /// @param _prizeVault The address of the vault to contribute to /// @param _amount The amount of prize tokens to contribute /// @return The amount of available prize tokens prior to the contribution. - function contributePrizeTokens(address _prizeVault, uint256 _amount) external returns (uint256) { + function contributePrizeTokens(address _prizeVault, uint256 _amount) public returns (uint256) { uint256 _deltaBalance = prizeToken.balanceOf(address(this)) - accountedBalance(); if (_deltaBalance < _amount) { revert ContributionGTDeltaBalance(_amount, _deltaBalance); @@ -345,6 +348,13 @@ contract PrizePool is TieredLiquidityDistributor { return _deltaBalance; } + /// @notice Allows a user to donate prize tokens to the prize pool. + /// @param _amount The amount of tokens to donate. The amount should already be approved for transfer. + function donatePrizeTokens(uint256 _amount) external { + prizeToken.transferFrom(msg.sender, address(this), _amount); + contributePrizeTokens(DONATOR, _amount); + } + /// @notice Allows the Manager to allocate a reward from the reserve to a recipient. /// @param _to The address to allocate the rewards to /// @param _amount The amount of tokens for the reward @@ -578,6 +588,22 @@ contract PrizePool is TieredLiquidityDistributor { ); } + /// @notice Returns the total prize tokens donated to the prize pool + /// @param _startDrawIdInclusive Start draw id inclusive + /// @param _endDrawIdInclusive End draw id inclusive + /// @return The total prize tokens donated to the prize pool + function getDonatedBetween( + uint24 _startDrawIdInclusive, + uint24 _endDrawIdInclusive + ) external view returns (uint256) { + return + DrawAccumulatorLib.getDisbursedBetween( + _vaultAccumulator[DONATOR], + _startDrawIdInclusive, + _endDrawIdInclusive + ); + } + /// @notice Computes the expected duration prize accrual for a tier. /// @param _tier The tier to check /// @return The number of draws @@ -944,12 +970,18 @@ contract PrizePool is TieredLiquidityDistributor { uint24 _startDrawIdInclusive, uint24 _endDrawIdInclusive ) public view returns (SD59x18) { + if (_vault == DONATOR) { + return sd(0); + } + uint256 totalContributed = DrawAccumulatorLib.getDisbursedBetween( _totalAccumulator, _startDrawIdInclusive, _endDrawIdInclusive ); + uint256 totalDonated = DrawAccumulatorLib.getDisbursedBetween(_vaultAccumulator[DONATOR], _startDrawIdInclusive, _endDrawIdInclusive); + // vaultContributed / totalContributed return totalContributed != 0 @@ -961,7 +993,7 @@ contract PrizePool is TieredLiquidityDistributor { _endDrawIdInclusive ) ) - ).div(sd(SafeCast.toInt256(totalContributed))) + ).div(sd(SafeCast.toInt256(totalContributed - totalDonated))) : sd(0); } diff --git a/test/PrizePool.t.sol b/test/PrizePool.t.sol index ca216f2..f15f32a 100644 --- a/test/PrizePool.t.sol +++ b/test/PrizePool.t.sol @@ -376,6 +376,26 @@ contract PrizePoolTest is Test { prizePool.contributePrizeTokens(address(this), 100); } + function testDonatePrizeTokens() public { + prizeToken.mint(address(this), 100); + prizeToken.approve(address(prizePool), 100); + prizePool.donatePrizeTokens(100); + assertEq(prizeToken.balanceOf(address(prizePool)), 100); + assertEq(prizePool.getTotalContributedBetween(1, 1), 100); + assertEq(prizePool.getDonatedBetween(1,1), 100); + } + + function testDonatePrizeTokens_twice() public { + prizeToken.mint(address(this), 100); + prizeToken.approve(address(prizePool), 100); + prizePool.donatePrizeTokens(50); + awardDraw(winningRandomNumber); + prizePool.donatePrizeTokens(50); + assertEq(prizeToken.balanceOf(address(prizePool)), 100); + assertEq(prizePool.getTotalContributedBetween(1, 2), 100); + assertEq(prizePool.getDonatedBetween(1, 2), 100); + } + function testAccountedBalance_withdrawnReserve() public { contribute(100e18); awardDraw(1); @@ -425,6 +445,11 @@ contract PrizePoolTest is Test { assertEq(prizePool.accountedBalance(), 100e18 - prize - prize2, "accounted balance"); } + function testGetVaultPortion_fromDonator() public { + contribute(100e18, prizePool.DONATOR()); // available draw 1 + assertEq(SD59x18.unwrap(prizePool.getVaultPortion(prizePool.DONATOR(), 1, 1)), 0); + } + function testGetVaultPortion_WhenEmpty() public { assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 0, 0)), 0); } @@ -476,6 +501,23 @@ contract PrizePoolTest is Test { assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 0, 2)), 1e18); } + function testGetVaultPortion_ignoresDonations() public { + // contribute to vault1 + prizeToken.mint(address(prizePool), 100); + prizePool.contributePrizeTokens(address(vault), 100); + + // contribute to vault2 + prizeToken.mint(address(prizePool), 100); + prizePool.contributePrizeTokens(address(vault2), 100); + + prizeToken.mint(address(this), 100); + prizeToken.approve(address(prizePool), 100); + prizePool.donatePrizeTokens(100); + + assertEq(prizePool.getVaultPortion(address(vault), 1, 1).unwrap(), 0.5e18); + assertEq(prizePool.getVaultPortion(address(vault2), 1, 1).unwrap(), 0.5e18); + } + function testGetOpenDrawId() public { uint256 openDrawId = prizePool.getOpenDrawId(); assertEq(openDrawId, 1); diff --git a/test/invariants/helpers/PrizePoolFuzzHarness.sol b/test/invariants/helpers/PrizePoolFuzzHarness.sol index f50651f..a3e2884 100644 --- a/test/invariants/helpers/PrizePoolFuzzHarness.sol +++ b/test/invariants/helpers/PrizePoolFuzzHarness.sol @@ -93,6 +93,17 @@ contract PrizePoolFuzzHarness is CommonBase, StdCheats, StdUtils, CurrentTimeCon prizePool.contributePrizeTokens(_actor(actorSeed), _amount); } + function donatePrizeTokens(uint88 _amount, uint256 actorSeed) public increaseCurrentTime(_timeIncrease()) prankActor(actorSeed) { + // console2.log("contributePrizeTokens"); + address actor = _actor(actorSeed); + token.mint(address(actor), _amount); + vm.startPrank(actor); + token.approve(address(prizePool), _amount); + contributed += _amount; + prizePool.donatePrizeTokens(_amount); + vm.stopPrank(); + } + function contributeReserve(uint88 _amount, uint256 actorSeed) public increaseCurrentTime(_timeIncrease()) prankActor(actorSeed) { if (prizePool.isShutdown()) { return;