From 4aef1f4c693b97925d3f985bc46ad8ca56ac352c Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 15 Jun 2023 18:15:35 -0500 Subject: [PATCH 1/8] feat(RNGRequestor): add contract --- lcov.info | 127 +++++++++- lib/openzeppelin-contracts | 2 +- lib/v5-prize-pool | 2 +- src/DrawAuction.sol | 95 ++++++-- src/DrawBeacon.sol | 475 ------------------------------------- src/RNGRequestor.sol | 367 ++++++++++++++++++++++++++++ test/DrawAuction.t.sol | 2 +- test/DrawBeacon.t.sol | 9 - test/RNGRequestor.t.sol | 349 +++++++++++++++++++++++++++ 9 files changed, 918 insertions(+), 510 deletions(-) delete mode 100644 src/DrawBeacon.sol create mode 100644 src/RNGRequestor.sol delete mode 100644 test/DrawBeacon.t.sol create mode 100644 test/RNGRequestor.t.sol diff --git a/lcov.info b/lcov.info index 820ea4b..099b8a1 100644 --- a/lcov.info +++ b/lcov.info @@ -1,10 +1,121 @@ TN: -SF:src/DrawBeacon.sol -FN:5,DrawBeacon.getDrawBeacon -FNDA:1,DrawBeacon.getDrawBeacon -FNF:1 -FNH:1 -DA:6,1 -LF:1 -LH:1 +SF:src/DrawAuction.sol +FN:55,DrawAuction.completeAndStartNextDraw +FN:71,DrawAuction.auctionDuration +FN:79,DrawAuction.prizePool +FN:87,DrawAuction.reward +FN:99,DrawAuction._reward +FNDA:0,DrawAuction.completeAndStartNextDraw +FNDA:0,DrawAuction.auctionDuration +FNDA:0,DrawAuction.prizePool +FNDA:5,DrawAuction.reward +FNDA:5,DrawAuction._reward +FNF:5 +FNH:2 +DA:56,0 +DA:58,0 +DA:59,0 +DA:61,0 +DA:72,0 +DA:80,0 +DA:88,5 +DA:100,5 +DA:102,5 +DA:103,1 +DA:106,4 +DA:107,4 +DA:109,4 +DA:110,4 +LF:14 +LH:8 +end_of_record +TN: +SF:src/RNGRequestor.sol +FN:167,RNGRequestor.startRNGRequest +FN:187,RNGRequestor.completeRNGRequest +FN:199,RNGRequestor.cancelRNGRequest +FN:216,RNGRequestor.isRNGRequested +FN:224,RNGRequestor.isRNGCompleted +FN:232,RNGRequestor.isRNGTimedOut +FN:240,RNGRequestor.canStartRNGRequest +FN:248,RNGRequestor.canCompleteRNGRequest +FN:258,RNGRequestor.getRNGLockBlock +FN:267,RNGRequestor.getRNGRequestId +FN:275,RNGRequestor.getRNGTimeout +FN:283,RNGRequestor.getRNGService +FN:295,RNGRequestor.setRNGService +FN:305,RNGRequestor.setRNGTimeout +FN:315,RNGRequestor._afterRNGComplete +FN:321,RNGRequestor._currentTime +FN:329,RNGRequestor._isRNGRequested +FN:337,RNGRequestor._isRNGCompleted +FN:345,RNGRequestor._isRNGTimedOut +FN:357,RNGRequestor._setRNGService +FN:367,RNGRequestor._setRNGTimeout +FNDA:12,RNGRequestor.startRNGRequest +FNDA:3,RNGRequestor.completeRNGRequest +FNDA:2,RNGRequestor.cancelRNGRequest +FNDA:2,RNGRequestor.isRNGRequested +FNDA:2,RNGRequestor.isRNGCompleted +FNDA:2,RNGRequestor.isRNGTimedOut +FNDA:2,RNGRequestor.canStartRNGRequest +FNDA:2,RNGRequestor.canCompleteRNGRequest +FNDA:3,RNGRequestor.getRNGLockBlock +FNDA:3,RNGRequestor.getRNGRequestId +FNDA:3,RNGRequestor.getRNGTimeout +FNDA:3,RNGRequestor.getRNGService +FNDA:2,RNGRequestor.setRNGService +FNDA:2,RNGRequestor.setRNGTimeout +FNDA:1,RNGRequestor._afterRNGComplete +FNDA:14,RNGRequestor._currentTime +FNDA:21,RNGRequestor._isRNGRequested +FNDA:5,RNGRequestor._isRNGCompleted +FNDA:4,RNGRequestor._isRNGTimedOut +FNDA:2,RNGRequestor._setRNGService +FNDA:2,RNGRequestor._setRNGTimeout +FNF:21 +FNH:21 +DA:168,11 +DA:170,11 +DA:171,1 +DA:174,11 +DA:175,11 +DA:176,11 +DA:177,11 +DA:179,11 +DA:188,1 +DA:189,1 +DA:191,1 +DA:193,1 +DA:195,1 +DA:200,2 +DA:202,1 +DA:203,1 +DA:205,1 +DA:207,1 +DA:217,2 +DA:225,2 +DA:233,2 +DA:241,2 +DA:249,2 +DA:259,3 +DA:268,3 +DA:276,3 +DA:284,3 +DA:296,2 +DA:306,2 +DA:322,14 +DA:330,21 +DA:338,5 +DA:346,4 +DA:347,1 +DA:349,3 +DA:358,2 +DA:359,1 +DA:360,1 +DA:368,2 +DA:369,1 +DA:370,1 +LF:41 +LH:41 end_of_record diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 281550b..ded8c9e 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 281550b71c3df9a83e6b80ceefc700852c287570 +Subproject commit ded8c9eedb9a03b0703b65d430e6d0076cb0e444 diff --git a/lib/v5-prize-pool b/lib/v5-prize-pool index 45a23d0..524016e 160000 --- a/lib/v5-prize-pool +++ b/lib/v5-prize-pool @@ -1 +1 @@ -Subproject commit 45a23d0d7be8fe7c6d976f3f653b6d7a4064470f +Subproject commit 524016e90f5f0ca637c8707d242a1e71f050e5db diff --git a/src/DrawAuction.sol b/src/DrawAuction.sol index 9a58c55..90805ea 100644 --- a/src/DrawAuction.sol +++ b/src/DrawAuction.sol @@ -3,45 +3,110 @@ pragma solidity 0.8.17; import { PrizePool } from "v5-prize-pool/PrizePool.sol"; +/** + * @title PoolTogether V5 DrawAuction + * @author PoolTogether Inc. Team + * @notice The DrawAuction uses an auction mechanism to incentivize the completion of the Draw. + * This mechanism relies on a linear interpolation to incentivizes anyone to start and complete the Draw. + * The first user to complete the Draw gets rewarded with the partial or full PrizePool reserve amount. + */ contract DrawAuction { - PrizePool internal prizePool; + /* ============ Variables ============ */ + /// @notice Duration of the auction in seconds. uint32 internal _auctionDuration; - constructor(PrizePool _prizePool, uint32 auctionDuration_) { - prizePool = _prizePool; + /// @notice Instance of the PrizePool to compute Draw for. + PrizePool internal _prizePool; + + /// @notice Seconds between draws. + uint32 internal _drawPeriodSeconds; + + /* ============ Custom Errors ============ */ + + /// @notice Thrown when the PrizePool address passed to the constructor is zero address. + error PrizePoolNotZeroAddress(); + + /// @notice Thrown when the Draw period seconds passed to the constructor is zero. + error DrawPeriodSecondsNotZero(); + + /* ============ Constructor ============ */ + + /** + * @notice Contract constructor. + * @dev We pass the `drawPeriodSeconds` cause the PrizePool we want to interact with may live on L2. + * @param prizePool_ Address of the prize pool + * @param drawPeriodSeconds_ Draw period in seconds + * @param auctionDuration_ Duration of the auction in seconds + */ + constructor(PrizePool prizePool_, uint32 drawPeriodSeconds_, uint32 auctionDuration_) { + _prizePool = prizePool_; + _drawPeriodSeconds = drawPeriodSeconds_; _auctionDuration = auctionDuration_; } - /// @notice Allows the Manager to complete the current prize period and starts the next one, updating the number of tiers, the winning random number, and the prize pool reserve - /// @param winningRandomNumber_ The winning random number for the current draw - function completeAndStartNextDraw(uint256 winningRandomNumber_) external { - uint256 _y = _reward(); + /* ============ External Functions ============ */ + + /** + * @notice Complete the current Draw and start the next one. + * @param winningRandomNumber_ The winning random number for the current Draw + * @return Reward amount + */ + function completeAndStartNextDraw(uint256 winningRandomNumber_) external returns (uint256) { + uint256 _rewardAmount = _reward(); - prizePool.completeAndStartNextDraw(winningRandomNumber_); + _prizePool.completeAndStartNextDraw(winningRandomNumber_); + _prizePool.withdrawReserve(msg.sender, uint104(_rewardAmount)); - prizePool.withdrawReserve(msg.sender, uint104(_y)); + return _rewardAmount; } + /* ============ Getter Functions ============ */ + + /** + * @notice Duration of the auction. + * @dev This is the time it takes for the auction to reach the PrizePool full reserve amount. + * @return Duration of the auction in seconds + */ + function auctionDuration() external view returns (uint256) { + return _auctionDuration; + } + + /** + * @notice Prize Pool instance for which the Draw is triggered. + * @return Prize Pool instance + */ + function prizePool() external view returns (PrizePool) { + return _prizePool; + } + + /** + * @notice Current reward for calling `completeAndStartNextDraw`. + * @return Reward amount + */ function reward() external view returns (uint256) { return _reward(); } + /* ============ Internal Functions ============ */ + + /** + * @notice Current reward for calling `completeAndStartNextDraw`. + * @dev The reward amount is computed via linear interpolation starting from 0 + * and increasing as the auction goes on to the full reserve amount. + * @return Reward amount + */ function _reward() internal view returns (uint256) { - uint256 _nextDrawEndsAt = prizePool.nextDrawEndsAt(); + uint256 _nextDrawEndsAt = _prizePool.nextDrawEndsAt(); if (block.timestamp < _nextDrawEndsAt) { return 0; } - uint256 _reserve = prizePool.reserve() + prizePool.reserveForNextDraw(); + uint256 _reserve = _prizePool.reserve() + _prizePool.reserveForNextDraw(); uint256 _elapsedTime = block.timestamp - _nextDrawEndsAt; return _elapsedTime >= _auctionDuration ? _reserve : (_elapsedTime * _reserve) / _auctionDuration; } - - function auctionDuration() external view returns (uint256) { - return _auctionDuration; - } } diff --git a/src/DrawBeacon.sol b/src/DrawBeacon.sol deleted file mode 100644 index f860afb..0000000 --- a/src/DrawBeacon.sol +++ /dev/null @@ -1,475 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.17; - -import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; -import { Ownable } from "owner-manager/Ownable.sol"; -import { RNGInterface } from "rng/RNGInterface.sol"; -import { PrizePool } from "v5-prize-pool/PrizePool.sol"; - -contract DrawBeacon is Ownable { - using SafeERC20 for IERC20; - - /* ============ Variables ============ */ - - /// @notice PrizePool contract address. - PrizePool internal prizePool; - - /// @notice RNG contract address. - RNGInterface internal rng; - - /// @notice Current RNG Request. - RngRequest internal rngRequest; - - /** - * @notice RNG Request Timeout. In fact, this is really a "complete draw" timeout. - * @dev If the rng completes the award can still be cancelled. - */ - uint32 internal rngTimeout; - - /// @notice Seconds between beacon period request. - uint32 internal beaconPeriodSeconds; - - /// @notice Epoch timestamp when beacon period can start. - uint64 internal beaconPeriodStartedAt; - - /** - * @notice Next Draw ID to use when creating a new draw. - * @dev Starts at 1. This way we know that no Draw has been recorded at 0. - */ - uint32 internal nextDrawId; - - /* ============ Structs ============ */ - - /** - * @notice RNG Request. - * @param id RNG request ID - * @param lockBlock Block number that the RNG request is locked - * @param requestedAt Time when RNG is requested - */ - struct RngRequest { - uint32 id; - uint32 lockBlock; - uint64 requestedAt; - } - - /* ============ Events ============ */ - - /** - * @notice Emitted when a draw has opened. - * @param startedAt Start timestamp - */ - event BeaconPeriodStarted(uint64 indexed startedAt); - - /** - * @notice Emitted when a draw has started. - * @param rngRequestId Request id - * @param rngLockBlock Block when draw becomes invalid - */ - event DrawStarted(uint32 indexed rngRequestId, uint32 rngLockBlock); - - /** - * @notice Emitted when a draw has been cancelled. - * @param rngRequestId Request id - * @param rngLockBlock Block when draw becomes invalid - */ - event DrawCancelled(uint32 indexed rngRequestId, uint32 rngLockBlock); - - /** - * @notice Emitted when a draw has been completed. - * @param randomNumber Random number generated for the draw - */ - event DrawCompleted(uint256 randomNumber); - - /** - * @notice Emitted when the drawPeriodSeconds is set. - * @param drawPeriodSeconds Time between draws in seconds - */ - event BeaconPeriodSecondsSet(uint32 drawPeriodSeconds); - - /** - * @notice Emitted when the PrizePool address is set. - * @param prizePool PrizePool address - */ - event PrizePoolSet(PrizePool indexed prizePool); - - /** - * @notice Emitted when the RNG service address is set. - * @param rngService RNG service address - */ - event RngServiceSet(RNGInterface indexed rngService); - - /** - * @notice Emitted when the draw timeout param is set. - * @param rngTimeout Draw timeout param in seconds - */ - event RngTimeoutSet(uint32 rngTimeout); - - /* ============ Modifiers ============ */ - - modifier requireDrawNotStarted() { - require( - rngRequest.lockBlock == 0 || block.number < rngRequest.lockBlock, - "DrawBeacon/rng-in-flight" - ); - _; - } - - modifier requireCanStartDraw() { - require(_isBeaconPeriodOver(), "DrawBeacon/beaconPeriod-not-over"); - require(!isRngRequested(), "DrawBeacon/rng-already-requested"); - _; - } - - modifier requireCanCompleteRngRequest() { - require(isRngRequested(), "DrawBeacon/rng-not-requested"); - require(isRngCompleted(), "DrawBeacon/rng-not-complete"); - _; - } - - /* ============ Constructor ============ */ - - /** - * @notice Deploy the DrawBeacon smart contract. - * @param _owner Address of the DrawBeacon owner - * @param _prizePool Address of the prize pool - * @param _rng Address of the RNG service - * @param _nextDrawId Draw ID at which the DrawBeacon will start. Can't be inferior to 1. - * @param _beaconPeriodStart The starting timestamp of the beacon period - * @param _beaconPeriodSeconds The duration of the beacon period in seconds - * @param _rngTimeout Time in seconds before a draw can be cancelled - */ - constructor( - address _owner, - PrizePool _prizePool, - RNGInterface _rng, - uint32 _nextDrawId, - uint64 _beaconPeriodStart, - uint32 _beaconPeriodSeconds, - uint32 _rngTimeout - ) Ownable(_owner) { - require(_beaconPeriodStart > 0, "DrawBeacon/beacon-period-gt-zero"); - require(_nextDrawId >= 1, "DrawBeacon/next-draw-id-gte-one"); - - beaconPeriodStartedAt = _beaconPeriodStart; - nextDrawId = _nextDrawId; - - _setBeaconPeriodSeconds(_beaconPeriodSeconds); - _setPrizePool(_prizePool); - _setRngService(_rng); - _setRngTimeout(_rngTimeout); - } - - /* ============ Public Functions ============ */ - - /** - * @notice Returns whether the random number request has completed or not. - * @return True if a random number request has completed, false otherwise. - */ - function isRngCompleted() public view returns (bool) { - return rng.isRequestComplete(rngRequest.id); - } - - /** - * @notice Returns whether a random number has been requested or not. - * @return True if a random number has been requested, false otherwise. - */ - function isRngRequested() public view returns (bool) { - return rngRequest.id != 0; - } - - /** - * @notice Returns whether the random number request has timed out or not. - * @return True if a random number request has timed out, false otherwise. - */ - function isRngTimedOut() public view returns (bool) { - if (rngRequest.requestedAt == 0) { - return false; - } else { - return rngTimeout + rngRequest.requestedAt < _currentTime(); - } - } - - /* ============ External Functions ============ */ - - /** - * @notice Returns whether a Draw can be started or not. - * @return True if a Draw can be started, false otherwise. - */ - function canStartDraw() external view returns (bool) { - return _isBeaconPeriodOver() && !isRngRequested(); - } - - /** - * @notice Returns whether a Draw can be completed or not. - * @return True if a Draw can be completed, false otherwise. - */ - function canCompleteDraw() external view returns (bool) { - return isRngRequested() && isRngCompleted(); - } - - /** - * @notice Calculates the next beacon start time, assuming all beacon periods have occurred between the last and now. - * @return The next beacon period start time. - */ - function calculateNextBeaconPeriodStartTimeFromCurrentTime() external view returns (uint64) { - return - _calculateNextBeaconPeriodStartTime( - beaconPeriodStartedAt, - beaconPeriodSeconds, - _currentTime() - ); - } - - /** - * @notice Calculates when the next beacon period will start. - * @param _time Timestamp to use as the current time - * @return Timestamp at which the next beacon period will start. - */ - function calculateNextBeaconPeriodStartTime(uint64 _time) external view returns (uint64) { - return _calculateNextBeaconPeriodStartTime(beaconPeriodStartedAt, beaconPeriodSeconds, _time); - } - - /// @notice Can be called by anyone to cancel the draw request if the RNG has timed out. - function cancelDraw() external { - require(isRngTimedOut(), "DrawBeacon/rng-not-timedout"); - uint32 requestId = rngRequest.id; - uint32 lockBlock = rngRequest.lockBlock; - delete rngRequest; - emit DrawCancelled(requestId, lockBlock); - } - - /// @notice Completes the Draw (RNG) request and award the PrizePool. - function completeDraw() external requireCanCompleteRngRequest { - uint256 _randomNumber = rng.randomNumber(rngRequest.id); - uint64 _beaconPeriodStartedAt = beaconPeriodStartedAt; - uint32 _beaconPeriodSeconds = beaconPeriodSeconds; - uint64 _time = _currentTime(); - - uint32 _lastCompletedDrawId = prizePool.completeAndStartNextDraw(_randomNumber); - - // To avoid clock drift, we should calculate the start time based on the previous period start time. - uint64 _nextBeaconPeriodStartedAt = _calculateNextBeaconPeriodStartTime( - _beaconPeriodStartedAt, - _beaconPeriodSeconds, - _time - ); - - beaconPeriodStartedAt = _nextBeaconPeriodStartedAt; - nextDrawId = _lastCompletedDrawId + 1; - - // Reset the rngRequest state so Beacon period can start again. - delete rngRequest; - - emit DrawCompleted(_randomNumber); - emit BeaconPeriodStarted(_nextBeaconPeriodStartedAt); - } - - /** - * @notice Returns the number of seconds remaining until the beacon period can be complete. - * @return The number of seconds remaining until the beacon period can be complete. - */ - function beaconPeriodRemainingSeconds() external view returns (uint64) { - return _beaconPeriodRemainingSeconds(); - } - - /** - * @notice Returns the timestamp at which the beacon period ends - * @return The timestamp at which the beacon period ends. - */ - function beaconPeriodEndAt() external view returns (uint64) { - return _beaconPeriodEndAt(); - } - - function getBeaconPeriodSeconds() external view returns (uint32) { - return beaconPeriodSeconds; - } - - function getBeaconPeriodStartedAt() external view returns (uint64) { - return beaconPeriodStartedAt; - } - - function getNextDrawId() external view returns (uint32) { - return nextDrawId; - } - - /** - * @notice Returns the block number that the current RNG request has been locked to. - * @return The block number that the RNG request is locked to. - */ - function getLastRngLockBlock() external view returns (uint32) { - return rngRequest.lockBlock; - } - - function getLastRngRequestId() external view returns (uint32) { - return rngRequest.id; - } - - function getRngService() external view returns (RNGInterface) { - return rng; - } - - function getRngTimeout() external view returns (uint32) { - return rngTimeout; - } - - /** - * @notice Returns whether the beacon period is over or not. - * @return True if the beacon period is over, false otherwise. - */ - function isBeaconPeriodOver() external view returns (bool) { - return _isBeaconPeriodOver(); - } - - /** - * @notice Starts the Draw process by starting random number request. The previous beacon period must have ended. - * @dev If the RNG Service request a `feeToken` for payment, - * the RNG-Request-Fee is expected to be held within this contract before calling this function. - */ - function startDraw() external requireCanStartDraw { - (address feeToken, uint256 requestFee) = rng.getRequestFee(); - - if (feeToken != address(0) && requestFee > 0) { - IERC20(feeToken).safeIncreaseAllowance(address(rng), requestFee); - } - - (uint32 requestId, uint32 lockBlock) = rng.requestRandomNumber(); - rngRequest.id = requestId; - rngRequest.lockBlock = lockBlock; - rngRequest.requestedAt = _currentTime(); - - emit DrawStarted(requestId, lockBlock); - } - - /** - * @notice Allows the owner to set the beacon period in seconds. - * @param _beaconPeriodSeconds The new beacon period in seconds. Must be greater than zero. - */ - function setBeaconPeriodSeconds( - uint32 _beaconPeriodSeconds - ) external onlyOwner requireDrawNotStarted { - _setBeaconPeriodSeconds(_beaconPeriodSeconds); - } - - /** - * @notice Sets the PrizePool that will compute the Draw. - * @param _prizePool Address of the new PrizePool - */ - function setPrizePool(PrizePool _prizePool) external onlyOwner requireDrawNotStarted { - _setPrizePool(_prizePool); - } - - /** - * @notice Sets the RNG service that the Prize Strategy is connected to. - * @param _rngService The address of the new RNG service interface - */ - function setRngService(RNGInterface _rngService) external onlyOwner requireDrawNotStarted { - _setRngService(_rngService); - } - - /** - * @notice Allows the owner to set the RNG request timeout in seconds. This is the time that must elapsed before the RNG request can be cancelled and the pool unlocked. - * @param _rngTimeout The RNG request timeout in seconds - */ - function setRngTimeout(uint32 _rngTimeout) external onlyOwner requireDrawNotStarted { - _setRngTimeout(_rngTimeout); - } - - /* ============ Internal Functions ============ */ - - /** - * @notice Calculates when the next beacon period will start - * @param _beaconPeriodStartedAt The timestamp at which the beacon period started - * @param _beaconPeriodSeconds The duration of the beacon period in seconds - * @param _time The timestamp to use as the current time - * @return The timestamp at which the next beacon period will start. - */ - function _calculateNextBeaconPeriodStartTime( - uint64 _beaconPeriodStartedAt, - uint32 _beaconPeriodSeconds, - uint64 _time - ) internal pure returns (uint64) { - uint64 elapsedPeriods = (_time - _beaconPeriodStartedAt) / _beaconPeriodSeconds; - return _beaconPeriodStartedAt + (elapsedPeriods * _beaconPeriodSeconds); - } - - /** - * @notice Returns the current timestamp. - * @return The current timestamp. - */ - function _currentTime() internal view virtual returns (uint64) { - return uint64(block.timestamp); - } - - /** - * @notice Returns the timestamp at which the beacon period ends. - * @return The timestamp at which the beacon period ends. - */ - function _beaconPeriodEndAt() internal view returns (uint64) { - return beaconPeriodStartedAt + beaconPeriodSeconds; - } - - /** - * @notice Returns the number of seconds remaining until the prize can be awarded. - * @return The number of seconds remaining until the prize can be awarded. - */ - function _beaconPeriodRemainingSeconds() internal view returns (uint64) { - uint64 endAt = _beaconPeriodEndAt(); - uint64 time = _currentTime(); - - if (endAt <= time) { - return 0; - } - - return endAt - time; - } - - /** - * @notice Returns whether the beacon period is over or not. - * @return True if the beacon period is over, false otherwise. - */ - function _isBeaconPeriodOver() internal view returns (bool) { - return _beaconPeriodEndAt() <= _currentTime(); - } - - /** - * @notice Sets the beacon period in seconds. - * @param _beaconPeriodSeconds New beacon period in seconds. Must be greater than zero. - */ - function _setBeaconPeriodSeconds(uint32 _beaconPeriodSeconds) internal { - require(_beaconPeriodSeconds > 0, "DrawBeacon/beacon-period-gt-zero"); - beaconPeriodSeconds = _beaconPeriodSeconds; - - emit BeaconPeriodSecondsSet(_beaconPeriodSeconds); - } - - /** - * @notice Sets the PrizePool that will compute the Draw. - * @param _prizePool Address of the new PrizePool - */ - function _setPrizePool(PrizePool _prizePool) internal { - require(address(_prizePool) != address(0), "DrawBeacon/PP-not-zero-address"); - prizePool = _prizePool; - emit PrizePoolSet(_prizePool); - } - - /** - * @notice Sets the RNG service that the Prize Strategy is connected to - * @param _rng Address of the new RNG service - */ - function _setRngService(RNGInterface _rng) internal { - require(address(_rng) != address(0), "DrawBeacon/rng-not-zero-address"); - rng = _rng; - emit RngServiceSet(_rng); - } - - /** - * @notice Sets the RNG request timeout in seconds. This is the time that must elapse before the RNG request can be cancelled and the pool unlocked. - * @param _rngTimeout RNG request timeout in seconds - */ - function _setRngTimeout(uint32 _rngTimeout) internal { - require(_rngTimeout > 60, "DrawBeacon/rng-timeout-gt-60s"); - rngTimeout = _rngTimeout; - emit RngTimeoutSet(_rngTimeout); - } -} diff --git a/src/RNGRequestor.sol b/src/RNGRequestor.sol new file mode 100644 index 0000000..9502cf2 --- /dev/null +++ b/src/RNGRequestor.sol @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import { Ownable } from "owner-manager/Ownable.sol"; +import { RNGInterface } from "rng/RNGInterface.sol"; +import { PrizePool } from "v5-prize-pool/PrizePool.sol"; + +/** + * @title PoolTogether V5 RNGRequestor + * @author PoolTogether Inc. Team + * @notice The RNGRequestor allows anyone to request a RNG using the RNG service set. + * This contract can be inherited by other contracts and use the `_afterRNGComplete` hook + * to make use of the random number generated by the RNG. + */ +contract RNGRequestor is Ownable { + using SafeERC20 for IERC20; + + /* ============ Structs ============ */ + + /** + * @notice RNG Request. + * @param id RNG request ID + * @param lockBlock Block number at which the RNG request is locked + * @param requestedAt Time at which the RNG was requested + */ + struct RNGRequest { + uint32 id; + uint32 lockBlock; + uint64 requestedAt; + } + + /* ============ Variables ============ */ + + /// @notice RNG instance. + RNGInterface internal _rng; + + /// @notice Current RNG Request. + RNGRequest internal _rngRequest; + + /** + * @notice RNG Request Timeout. + * @dev If the RNG request timeouts, anyone can call `cancelRNGRequest` to cancel the pending the request. + */ + uint32 internal _rngTimeout; + + /* ============ Custom Errors ============ */ + + /// @notice Thrown when the RNG address passed to the setter function is zero address. + error RNGNotZeroAddress(); + + /** + * @notice Thrown when the RNG Timeout passed to the setter function is lower than 60 seconds. + * @param rngTimeout RNG Timeout value + */ + error RNGTimeoutLT60Seconds(uint32 rngTimeout); + + /// @notice Thrown if the RNG has not timed out. + error RNGHasNotTimedout(); + + /// @notice Thrown if a RNG request is currently being processed. + error RNGInProgress(); + + /// @notice Thrown if the next Draw has not finished yet. + error NextDrawNotFinished(); + + /** + * @notice Thrown if the RNG has been requested and is in progress. + * @param requestId ID of the RNG request + */ + error RNGRequested(uint32 requestId); + + /// @notice Thrown if calling `completeRNGRequest` and the RNG has not been requested. + error RNGNotRequested(); + + /** + * @notice Thrown if calling `completeRNGRequest` and the RNG request has not been completed yet. + * @param requestId ID of the RNG request + */ + error RNGNotCompleted(uint32 requestId); + + /* ============ Events ============ */ + + /** + * @notice Emitted when an RNG request has started. + * @param rngRequestId ID of the RNG request + * @param rngLockBlock Block when RNG request becomes invalid + */ + event RNGRequestStarted(uint32 indexed rngRequestId, uint32 rngLockBlock); + + /** + * @notice Emitted when an RNG request has been completed. + * @param rngRequestId ID of the RNG request + * @param randomNumber Random number generated + */ + event RNGRequestCompleted(uint32 indexed rngRequestId, uint256 randomNumber); + + /** + * @notice Emitted when an RNG request has been cancelled. + * @param rngRequestId ID of the RNG request + * @param rngLockBlock Block when RNG request became invalid + */ + event RNGRequestCancelled(uint32 indexed rngRequestId, uint32 rngLockBlock); + + /** + * @notice Emitted when the RNG service address is set. + * @param rngService RNG service address + */ + event RNGServiceSet(RNGInterface indexed rngService); + + /** + * @notice Emitted when the RNG timeout is set. + * @param rngTimeout RNG timeout in seconds + */ + event RNGTimeoutSet(uint32 rngTimeout); + + /* ============ Constructor ============ */ + + /** + * @notice Deploy the RNGRequestor smart contract. + * @param rng_ Address of the RNG service + * @param rngTimeout_ Time in seconds before an RNG request can be cancelled + * @param _owner Address of the RNGRequestor owner + */ + constructor(RNGInterface rng_, uint32 rngTimeout_, address _owner) Ownable(_owner) { + _setRNGService(rng_); + _setRNGTimeout(rngTimeout_); + } + + /* ============ Modifiers ============ */ + + /// @notice Reverts if an RNG request is in progress. + modifier requireRNGNotInProgress() { + if (_rngRequest.lockBlock != 0 || _rngRequest.lockBlock > block.number) revert RNGInProgress(); + _; + } + + /** + * @notice Reverts if an RNG request has been requested. + * @dev This RNG request could be in progress, completed or have timed out. + */ + modifier requireCanStartRNGRequest() { + if (_isRNGRequested()) revert RNGRequested(_rngRequest.id); + _; + } + + /// @notice Reverts if an RNG request has not been requested or has not completed yet. + modifier requireCanCompleteRNGRequest() { + if (!_isRNGRequested()) revert RNGNotRequested(); + if (!_isRNGCompleted()) revert RNGNotCompleted(_rngRequest.id); + _; + } + + /* ============ External Functions ============ */ + + /** + * @notice Starts the RNG Request. + * @dev Will revert if an RNG request has already been requested. + * @dev If the RNG Service request a `feeToken` for payment, + * the RNG-Request-Fee is expected to be held within this contract before calling this function. + */ + function startRNGRequest() external requireCanStartRNGRequest { + (address _feeToken, uint256 _requestFee) = _rng.getRequestFee(); + + if (_feeToken != address(0) && _requestFee > 0) { + IERC20(_feeToken).safeIncreaseAllowance(address(_rng), _requestFee); + } + + (uint32 _requestId, uint32 _lockBlock) = _rng.requestRandomNumber(); + _rngRequest.id = _requestId; + _rngRequest.lockBlock = _lockBlock; + _rngRequest.requestedAt = _currentTime(); + + emit RNGRequestStarted(_requestId, _lockBlock); + } + + /** + * @notice Completes the RNG request. + * @dev Will revert if no RNG has been requested or if the RNG request has not completed yet. + */ + function completeRNGRequest() external requireCanCompleteRNGRequest { + uint32 _rngRequestId = _rngRequest.id; + uint256 _randomNumber = _rng.randomNumber(_rngRequestId); + + delete _rngRequest; + + _afterRNGComplete(_randomNumber); + + emit RNGRequestCompleted(_rngRequestId, _randomNumber); + } + + /// @notice Can be called by anyone to cancel the RNG request if it has timed out. + function cancelRNGRequest() external { + if (!_isRNGTimedOut()) revert RNGHasNotTimedout(); + + uint32 _requestId = _rngRequest.id; + uint32 _lockBlock = _rngRequest.lockBlock; + + delete _rngRequest; + + emit RNGRequestCancelled(_requestId, _lockBlock); + } + + /* ============ State Functions ============ */ + + /** + * @notice Returns whether an RNG request has been requested or not. + * @return True if an RNG request has been requested, false otherwise. + */ + function isRNGRequested() external view returns (bool) { + return _isRNGRequested(); + } + + /** + * @notice Returns whether the RNG request has completed or not. + * @return True if the RNG request has completed, false otherwise. + */ + function isRNGCompleted() external view returns (bool) { + return _isRNGCompleted(); + } + + /** + * @notice Returns whether the RNG request has timed out or not. + * @return True if the RNG request has timed out, false otherwise. + */ + function isRNGTimedOut() external view returns (bool) { + return _isRNGTimedOut(); + } + + /** + * @notice Returns whether the RNG request can be started or not. + * @return True if the RNG request can be started, false otherwise. + */ + function canStartRNGRequest() external view returns (bool) { + return !_isRNGRequested(); + } + + /** + * @notice Returns whether the RNG request can be completed or not. + * @return True if the RNG request can be completed, false otherwise. + */ + function canCompleteRNGRequest() external view returns (bool) { + return _isRNGRequested() && _isRNGCompleted(); + } + + /* ============ Getter Functions ============ */ + + /** + * @notice Returns the block number at which the current RNG request has been locked to. + * @return The block number at which the RNG request is locked to. + */ + function getRNGLockBlock() external view returns (uint32) { + return _rngRequest.lockBlock; + } + + /** + * @notice Returns the ID of the current RNG request. + * @dev Will return 0 if there is no RNG request in progress. + * @return ID of the current RNG request + */ + function getRNGRequestId() external view returns (uint32) { + return _rngRequest.id; + } + + /** + * @notice Returns the RNG timeout in seconds. + * @return RNG timeout in seconds + */ + function getRNGTimeout() external view returns (uint32) { + return _rngTimeout; + } + + /** + * @notice Returns the RNG service used to generate random numbers. + * @return RNG service instance + */ + function getRNGService() external view returns (RNGInterface) { + return _rng; + } + + /* ============ Setter Functions ============ */ + + /** + * @notice Sets the RNG service used to generate random numbers. + * @dev Only callable by the owner. + * @dev Will revert if an RNG request is in progress. + * @param _rngService Address of the new RNG service + */ + function setRNGService(RNGInterface _rngService) external onlyOwner requireRNGNotInProgress { + _setRNGService(_rngService); + } + + /** + * @notice Allows the owner to set the RNG request timeout in seconds. This is the time that must elapsed before the RNG request can be cancelled. + * @dev Only callable by the owner. + * @dev Will revert if an RNG request is in progress. + * @param rngTimeout_ RNG request timeout in seconds + */ + function setRNGTimeout(uint32 rngTimeout_) external onlyOwner requireRNGNotInProgress { + _setRNGTimeout(rngTimeout_); + } + + /* ============ Internal Functions ============ */ + + /** + * @notice Hook called after the RNG request has completed. + * @param _randomNumber The random number that was generated + */ + function _afterRNGComplete(uint256 _randomNumber) internal {} + + /** + * @notice Returns the current timestamp. + * @return The current timestamp. + */ + function _currentTime() internal view virtual returns (uint64) { + return uint64(block.timestamp); + } + + /** + * @notice Returns whether an RNG has been requested or not. + * @return True if an RNG has been requested, false otherwise. + */ + function _isRNGRequested() internal view returns (bool) { + return _rngRequest.id != 0; + } + + /** + * @notice Returns whether the RNG request has completed or not. + * @return True if the RNG request has completed, false otherwise. + */ + function _isRNGCompleted() internal view returns (bool) { + return _rng.isRequestComplete(_rngRequest.id); + } + + /** + * @notice Returns whether the RNG request has timed out or not. + * @return True if the RNG request has timed out, false otherwise. + */ + function _isRNGTimedOut() internal view returns (bool) { + if (_rngRequest.requestedAt == 0) { + return false; + } else { + return _rngTimeout + _rngRequest.requestedAt < _currentTime(); + } + } + + /** + * @notice Sets the RNG service used to generate random numbers. + * @param rng_ Address of the new RNG service + */ + function _setRNGService(RNGInterface rng_) internal { + if (address(rng_) == address(0)) revert RNGNotZeroAddress(); + _rng = rng_; + emit RNGServiceSet(rng_); + } + + /** + * @notice Sets the RNG request timeout in seconds. This is the time that must elapse before the RNG request can be cancelled. + * @param rngTimeout_ RNG request timeout in seconds + */ + function _setRNGTimeout(uint32 rngTimeout_) internal { + if (rngTimeout_ < 60) revert RNGTimeoutLT60Seconds(rngTimeout_); + _rngTimeout = rngTimeout_; + emit RNGTimeoutSet(rngTimeout_); + } +} diff --git a/test/DrawAuction.t.sol b/test/DrawAuction.t.sol index 798c1cb..b9a3d91 100644 --- a/test/DrawAuction.t.sol +++ b/test/DrawAuction.t.sol @@ -29,7 +29,7 @@ contract DrawAuctionTest is Test { SD1x18.wrap(0.9e18) // alpha ); - _drawAuction = new DrawAuction(_prizePool, _auctionDuration); + _drawAuction = new DrawAuction(_prizePool, 86400, _auctionDuration); vm.warp(0); diff --git a/test/DrawBeacon.t.sol b/test/DrawBeacon.t.sol deleted file mode 100644 index 915fe40..0000000 --- a/test/DrawBeacon.t.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.17; - -import "forge-std/Test.sol"; -import { console2 } from "forge-std/console2.sol"; - -import { DrawBeacon } from "../src/DrawBeacon.sol"; - -contract DrawBeaconTest is Test {} diff --git a/test/RNGRequestor.t.sol b/test/RNGRequestor.t.sol new file mode 100644 index 0000000..1ee007a --- /dev/null +++ b/test/RNGRequestor.t.sol @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; +import { console2 } from "forge-std/console2.sol"; + +import { ERC20Mock } from "openzeppelin/mocks/ERC20Mock.sol"; +import { RNGInterface } from "rng/RNGInterface.sol"; + +import { RNGRequestor } from "src/RNGRequestor.sol"; + +contract RNGRequestorTest is Test { + /* ============ Events ============ */ + + event RNGServiceSet(RNGInterface indexed rngService); + event RNGTimeoutSet(uint32 rngTimeout); + event RNGRequestStarted(uint32 indexed rngRequestId, uint32 rngLockBlock); + event RNGRequestCompleted(uint32 indexed rngRequestId, uint256 randomNumber); + event RNGRequestCancelled(uint32 indexed rngRequestId, uint32 rngLockBlock); + + /* ============ Variables ============ */ + + RNGInterface public rng; + RNGRequestor public rngRequestor; + uint32 public rngTimeOut; + + ERC20Mock public feeToken; + uint256 public feeAmount; + + function setUp() public { + feeToken = new ERC20Mock(); + feeAmount = 2e18; + + rng = RNGInterface(address(1)); + rngTimeOut = 1 hours; + + rngRequestor = new RNGRequestor(rng, rngTimeOut, address(this)); + } + + /* ============ Constructor ============ */ + + function testConstructor() public { + assertEq(address(rngRequestor.getRNGService()), address(rng)); + assertEq(rngRequestor.getRNGTimeout(), rngTimeOut); + assertEq(rngRequestor.owner(), address(this)); + } + + /* ============ Methods ============ */ + + /* ============ startRNGRequest ============ */ + function testStartRNGRequest() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockGetRequestFee(address(0), 0); + _mockRequestRandomNumber(_requestId, _lockBlock); + + vm.expectEmit(); + emit RNGRequestStarted(_requestId, _lockBlock); + + rngRequestor.startRNGRequest(); + + assertEq(rngRequestor.getRNGLockBlock(), _lockBlock); + assertEq(rngRequestor.getRNGRequestId(), _requestId); + } + + // @TODO Test with ChainlinkVRFV2 direct LINK transfer contact + function testStartRNGRequestWithFeeToken() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockGetRequestFee(address(feeToken), feeAmount); + _mockRequestRandomNumber(_requestId, _lockBlock); + + vm.expectEmit(); + emit RNGRequestStarted(_requestId, _lockBlock); + + rngRequestor.startRNGRequest(); + + assertEq(rngRequestor.getRNGLockBlock(), _lockBlock); + assertEq(rngRequestor.getRNGRequestId(), _requestId); + } + + function testStartRNGRequestFailRNGRequested() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockGetRequestFee(address(0), 0); + _mockRequestRandomNumber(_requestId, _lockBlock); + + rngRequestor.startRNGRequest(); + + vm.expectRevert(abi.encodeWithSelector(RNGRequestor.RNGRequested.selector, _requestId)); + + rngRequestor.startRNGRequest(); + } + + /* ============ completeRNGRequest ============ */ + function testCompleteRNGRequest() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + uint256 _randomNumber = 123456789; + + _mockGetRequestFee(address(0), 0); + _mockRequestRandomNumber(_requestId, _lockBlock); + + rngRequestor.startRNGRequest(); + + _mockIsRequestComplete(_requestId, true); + _mockRandomNumber(_requestId, _randomNumber); + + vm.expectEmit(); + emit RNGRequestCompleted(_requestId, _randomNumber); + + rngRequestor.completeRNGRequest(); + } + + function testCompleteRNGRequestFailRNGNotRequested() public { + vm.expectRevert(abi.encodeWithSelector(RNGRequestor.RNGNotRequested.selector)); + + rngRequestor.completeRNGRequest(); + } + + function testCompleteRNGRequestFailRNGNotCompleted() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockGetRequestFee(address(0), 0); + _mockRequestRandomNumber(_requestId, _lockBlock); + + rngRequestor.startRNGRequest(); + + _mockIsRequestComplete(_requestId, false); + + vm.expectRevert(abi.encodeWithSelector(RNGRequestor.RNGNotCompleted.selector, _requestId)); + + rngRequestor.completeRNGRequest(); + } + + /* ============ cancelRNGRequest ============ */ + function testCancelRNGRequest() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockGetRequestFee(address(0), 0); + _mockRequestRandomNumber(_requestId, _lockBlock); + + rngRequestor.startRNGRequest(); + + vm.warp(2 hours); + + vm.expectEmit(); + emit RNGRequestCancelled(_requestId, _lockBlock); + + rngRequestor.cancelRNGRequest(); + } + + function testCancelRNGRequestFail() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockGetRequestFee(address(0), 0); + _mockRequestRandomNumber(_requestId, _lockBlock); + + rngRequestor.startRNGRequest(); + + vm.expectRevert(abi.encodeWithSelector(RNGRequestor.RNGHasNotTimedout.selector)); + + rngRequestor.cancelRNGRequest(); + } + + /* ============ State Functions ============ */ + + /* ============ isRNGRequested ============ */ + function testIsRNGRequestedDefaultState() public { + assertEq(rngRequestor.isRNGRequested(), false); + } + + function testIsRNGRequestedActiveState() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockGetRequestFee(address(0), 0); + _mockRequestRandomNumber(_requestId, _lockBlock); + + vm.expectEmit(); + emit RNGRequestStarted(_requestId, _lockBlock); + + rngRequestor.startRNGRequest(); + + assertEq(rngRequestor.isRNGRequested(), true); + } + + /* ============ isRNGCompleted ============ */ + function testIsRNGCompletedDefaultState() public { + _mockIsRequestComplete(uint32(0), false); + assertEq(rngRequestor.isRNGCompleted(), false); + } + + function testIsRNGCompletedActiveState() public { + _mockIsRequestComplete(uint32(0), true); + assertEq(rngRequestor.isRNGCompleted(), true); + } + + /* ============ isRNGTimedOut ============ */ + function testIsRNGTimedOutDefaultState() public { + assertEq(rngRequestor.isRNGTimedOut(), false); + } + + function testIsRNGTimedOutActiveState() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockGetRequestFee(address(0), 0); + _mockRequestRandomNumber(_requestId, _lockBlock); + + rngRequestor.startRNGRequest(); + + vm.warp(2 hours); + assertEq(rngRequestor.isRNGTimedOut(), true); + } + + /* ============ canStartRNGRequest ============ */ + function testCanStartRNGRequestDefaultState() public { + assertEq(rngRequestor.canStartRNGRequest(), true); + } + + function testCanStartRNGRequestActiveState() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockGetRequestFee(address(0), 0); + _mockRequestRandomNumber(_requestId, _lockBlock); + + rngRequestor.startRNGRequest(); + + assertEq(rngRequestor.canStartRNGRequest(), false); + } + + /* ============ canCompleteRNGRequest ============ */ + function testCanCompleteRNGRequestDefaultState() public { + assertEq(rngRequestor.canCompleteRNGRequest(), false); + } + + function testCanCompleteRNGRequestActiveState() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockGetRequestFee(address(0), 0); + _mockRequestRandomNumber(_requestId, _lockBlock); + _mockIsRequestComplete(_requestId, true); + + rngRequestor.startRNGRequest(); + + assertEq(rngRequestor.canCompleteRNGRequest(), true); + } + + /* ============ Getter Functions ============ */ + function testgetRNGLockBlock() public { + assertEq(rngRequestor.getRNGLockBlock(), 0); + } + + function testGetRNGRequestId() public { + assertEq(rngRequestor.getRNGRequestId(), 0); + } + + function testGetRNGTimeout() public { + assertEq(rngRequestor.getRNGTimeout(), rngTimeOut); + } + + function testGetRNGService() public { + assertEq(address(rngRequestor.getRNGService()), address(rng)); + } + + /* ============ Setter Functions ============ */ + + /* ============ setRNGService ============ */ + function testSetRNGService() public { + RNGInterface _newRNGService = RNGInterface(address(2)); + + vm.expectEmit(); + emit RNGServiceSet(_newRNGService); + + rngRequestor.setRNGService(_newRNGService); + + assertEq(address(rngRequestor.getRNGService()), address(_newRNGService)); + } + + function testSetRNGServiceFail() public { + vm.expectRevert(abi.encodeWithSelector(RNGRequestor.RNGNotZeroAddress.selector)); + + rngRequestor.setRNGService(RNGInterface(address(0))); + } + + /* ============ setRNGService ============ */ + function testSetRNGTimeout() public { + uint32 _newRNGTimeout = 2 hours; + + vm.expectEmit(); + emit RNGTimeoutSet(_newRNGTimeout); + + rngRequestor.setRNGTimeout(_newRNGTimeout); + + assertEq(rngRequestor.getRNGTimeout(), _newRNGTimeout); + } + + function testSetRNGTimeoutFail() public { + uint32 _newRNGTimeout = 0; + + vm.expectRevert( + abi.encodeWithSelector(RNGRequestor.RNGTimeoutLT60Seconds.selector, _newRNGTimeout) + ); + + rngRequestor.setRNGTimeout(_newRNGTimeout); + } + + /* ============ Mock Functions ============ */ + function _mockGetRequestFee(address _feeToken, uint256 _requestFee) internal { + vm.mockCall( + address(rng), + abi.encodeWithSelector(RNGInterface.getRequestFee.selector), + abi.encode(_feeToken, _requestFee) + ); + } + + function _mockRequestRandomNumber(uint32 _requestId, uint32 _lockBlock) internal { + vm.mockCall( + address(rng), + abi.encodeWithSelector(RNGInterface.requestRandomNumber.selector), + abi.encode(_requestId, _lockBlock) + ); + } + + function _mockRandomNumber(uint32 _requestId, uint256 _randomNumber) internal { + vm.mockCall( + address(rng), + abi.encodeWithSelector(RNGInterface.randomNumber.selector, _requestId), + abi.encode(_randomNumber) + ); + } + + function _mockIsRequestComplete(uint32 _requestId, bool _isRequestComplete) internal { + vm.mockCall( + address(rng), + abi.encodeWithSelector(RNGInterface.isRequestComplete.selector, _requestId), + abi.encode(_isRequestComplete) + ); + } +} From 4cd6b1455a191c4f2362526c5d5049931b50f7bc Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 21 Jun 2023 18:20:42 -0500 Subject: [PATCH 2/8] feat(libraries): add libs for modularity --- lcov.info | 194 +++++++++++++++++---------- lib/openzeppelin-contracts | 2 +- lib/v5-prize-pool | 2 +- remappings.txt | 1 + src/DrawAuction.sol | 133 +++++++++--------- src/RNGRequestor.sol | 20 ++- src/auctions/Auction.sol | 113 ++++++++++++++++ src/auctions/TwoStepsAuction.sol | 50 +++++++ src/libraries/AuctionLib.sol | 20 +++ src/libraries/RewardLib.sol | 84 ++++++++++++ test/DrawAuction.t.sol | 223 ++++++++++++++++++++++++------- test/RNGRequestor.t.sol | 121 +++++------------ test/helpers/Helpers.t.sol | 102 ++++++++++++++ 13 files changed, 798 insertions(+), 267 deletions(-) create mode 100644 src/auctions/Auction.sol create mode 100644 src/auctions/TwoStepsAuction.sol create mode 100644 src/libraries/AuctionLib.sol create mode 100644 src/libraries/RewardLib.sol create mode 100644 test/helpers/Helpers.t.sol diff --git a/lcov.info b/lcov.info index 099b8a1..4ae8a35 100644 --- a/lcov.info +++ b/lcov.info @@ -1,37 +1,21 @@ TN: SF:src/DrawAuction.sol -FN:55,DrawAuction.completeAndStartNextDraw -FN:71,DrawAuction.auctionDuration FN:79,DrawAuction.prizePool -FN:87,DrawAuction.reward -FN:99,DrawAuction._reward -FNDA:0,DrawAuction.completeAndStartNextDraw -FNDA:0,DrawAuction.auctionDuration -FNDA:0,DrawAuction.prizePool -FNDA:5,DrawAuction.reward -FNDA:5,DrawAuction._reward -FNF:5 -FNH:2 -DA:56,0 -DA:58,0 -DA:59,0 -DA:61,0 -DA:72,0 -DA:80,0 -DA:88,5 -DA:100,5 -DA:102,5 -DA:103,1 -DA:106,4 -DA:107,4 -DA:109,4 -DA:110,4 -LF:14 -LH:8 +FN:88,DrawAuction.reward +FN:96,DrawAuction._afterAuctionEnds +FNDA:1,DrawAuction.prizePool +FNDA:11,DrawAuction.reward +FNDA:2,DrawAuction._afterAuctionEnds +FNF:3 +FNH:3 +DA:80,1 +DA:89,11 +LF:2 +LH:2 end_of_record TN: SF:src/RNGRequestor.sol -FN:167,RNGRequestor.startRNGRequest +FN:165,RNGRequestor.startRNGRequest FN:187,RNGRequestor.completeRNGRequest FN:199,RNGRequestor.cancelRNGRequest FN:216,RNGRequestor.isRNGRequested @@ -45,15 +29,16 @@ FN:275,RNGRequestor.getRNGTimeout FN:283,RNGRequestor.getRNGService FN:295,RNGRequestor.setRNGService FN:305,RNGRequestor.setRNGTimeout -FN:315,RNGRequestor._afterRNGComplete -FN:321,RNGRequestor._currentTime -FN:329,RNGRequestor._isRNGRequested -FN:337,RNGRequestor._isRNGCompleted -FN:345,RNGRequestor._isRNGTimedOut -FN:357,RNGRequestor._setRNGService -FN:367,RNGRequestor._setRNGTimeout -FNDA:12,RNGRequestor.startRNGRequest -FNDA:3,RNGRequestor.completeRNGRequest +FN:315,RNGRequestor._afterRNGStart +FN:322,RNGRequestor._afterRNGComplete +FN:328,RNGRequestor._currentTime +FN:336,RNGRequestor._isRNGRequested +FN:344,RNGRequestor._isRNGCompleted +FN:352,RNGRequestor._isRNGTimedOut +FN:364,RNGRequestor._setRNGService +FN:374,RNGRequestor._setRNGTimeout +FNDA:16,RNGRequestor.startRNGRequest +FNDA:5,RNGRequestor.completeRNGRequest FNDA:2,RNGRequestor.cancelRNGRequest FNDA:2,RNGRequestor.isRNGRequested FNDA:2,RNGRequestor.isRNGCompleted @@ -66,28 +51,30 @@ FNDA:3,RNGRequestor.getRNGTimeout FNDA:3,RNGRequestor.getRNGService FNDA:2,RNGRequestor.setRNGService FNDA:2,RNGRequestor.setRNGTimeout +FNDA:11,RNGRequestor._afterRNGStart FNDA:1,RNGRequestor._afterRNGComplete -FNDA:14,RNGRequestor._currentTime -FNDA:21,RNGRequestor._isRNGRequested -FNDA:5,RNGRequestor._isRNGCompleted +FNDA:18,RNGRequestor._currentTime +FNDA:27,RNGRequestor._isRNGRequested +FNDA:7,RNGRequestor._isRNGCompleted FNDA:4,RNGRequestor._isRNGTimedOut FNDA:2,RNGRequestor._setRNGService FNDA:2,RNGRequestor._setRNGTimeout -FNF:21 -FNH:21 -DA:168,11 -DA:170,11 -DA:171,1 -DA:174,11 -DA:175,11 -DA:176,11 -DA:177,11 -DA:179,11 -DA:188,1 -DA:189,1 -DA:191,1 -DA:193,1 -DA:195,1 +FNF:22 +FNH:22 +DA:166,15 +DA:168,15 +DA:169,1 +DA:172,15 +DA:173,15 +DA:174,15 +DA:175,15 +DA:177,15 +DA:179,15 +DA:188,3 +DA:189,3 +DA:191,3 +DA:193,3 +DA:195,3 DA:200,2 DA:202,1 DA:203,1 @@ -104,18 +91,87 @@ DA:276,3 DA:284,3 DA:296,2 DA:306,2 -DA:322,14 -DA:330,21 -DA:338,5 -DA:346,4 -DA:347,1 -DA:349,3 -DA:358,2 -DA:359,1 -DA:360,1 -DA:368,2 -DA:369,1 -DA:370,1 -LF:41 -LH:41 +DA:329,18 +DA:337,27 +DA:345,7 +DA:353,4 +DA:354,1 +DA:356,3 +DA:365,2 +DA:366,1 +DA:367,1 +DA:375,2 +DA:376,1 +DA:377,1 +LF:42 +LH:42 +end_of_record +TN: +SF:src/auctions/Auction.sol +FN:68,Auction.auctionDuration +FN:72,Auction.getPhase +FN:84,Auction._afterAuctionEnds +FN:88,Auction._getPhase +FN:94,Auction._setPhase +FNDA:1,Auction.auctionDuration +FNDA:0,Auction.getPhase +FNDA:0,Auction._afterAuctionEnds +FNDA:2,Auction._getPhase +FNDA:6,Auction._setPhase +FNF:5 +FNH:3 +DA:69,1 +DA:73,0 +DA:89,2 +DA:100,6 +DA:107,6 +DA:109,6 +DA:111,6 +LF:7 +LH:6 +end_of_record +TN: +SF:src/auctions/TwoStepsAuction.sol +FN:37,TwoStepsAuction._afterRNGStart +FN:46,TwoStepsAuction._afterRNGComplete +FNDA:4,TwoStepsAuction._afterRNGStart +FNDA:2,TwoStepsAuction._afterRNGComplete +FNF:2 +FNH:2 +DA:38,4 +DA:47,2 +DA:48,2 +LF:3 +LH:3 +end_of_record +TN: +SF:src/libraries/RewardLib.sol +FN:20,RewardLib.getReward +FNDA:11,RewardLib.getReward +FNF:1 +FNH:1 +DA:27,11 +DA:29,11 +DA:30,11 +DA:31,11 +DA:32,11 +DA:35,11 +DA:36,4 +DA:39,7 +DA:40,7 +DA:42,7 +DA:47,4 +DA:48,4 +DA:49,4 +DA:53,3 +DA:57,3 +DA:58,1 +DA:66,2 +DA:72,6 +DA:76,6 +DA:77,2 +DA:80,4 +DA:82,4 +LF:22 +LH:22 end_of_record diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index ded8c9e..e50c24f 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit ded8c9eedb9a03b0703b65d430e6d0076cb0e444 +Subproject commit e50c24f5839db17f46991478384bfda14acfb830 diff --git a/lib/v5-prize-pool b/lib/v5-prize-pool index 524016e..773b1b0 160000 --- a/lib/v5-prize-pool +++ b/lib/v5-prize-pool @@ -1 +1 @@ -Subproject commit 524016e90f5f0ca637c8707d242a1e71f050e5db +Subproject commit 773b1b0d36efa9eb9c06826783996cb5beefd17f diff --git a/remappings.txt b/remappings.txt index 170c1e1..ecfce42 100644 --- a/remappings.txt +++ b/remappings.txt @@ -7,3 +7,4 @@ rng/=lib/pooltogether-rng-contracts/contracts/ v5-prize-pool/=lib/v5-prize-pool/src/ src/=src/ +test/=test/ diff --git a/src/DrawAuction.sol b/src/DrawAuction.sol index 90805ea..8a57e54 100644 --- a/src/DrawAuction.sol +++ b/src/DrawAuction.sol @@ -2,6 +2,11 @@ pragma solidity 0.8.17; import { PrizePool } from "v5-prize-pool/PrizePool.sol"; +import { console2 } from "forge-std/Test.sol"; + +import { Auction } from "src/auctions/Auction.sol"; +import { TwoStepsAuction, RNGInterface } from "src/auctions/TwoStepsAuction.sol"; +import { RewardLib } from "src/libraries/RewardLib.sol"; /** * @title PoolTogether V5 DrawAuction @@ -10,68 +15,63 @@ import { PrizePool } from "v5-prize-pool/PrizePool.sol"; * This mechanism relies on a linear interpolation to incentivizes anyone to start and complete the Draw. * The first user to complete the Draw gets rewarded with the partial or full PrizePool reserve amount. */ -contract DrawAuction { +contract DrawAuction is TwoStepsAuction { /* ============ Variables ============ */ - /// @notice Duration of the auction in seconds. - uint32 internal _auctionDuration; - /// @notice Instance of the PrizePool to compute Draw for. PrizePool internal _prizePool; - /// @notice Seconds between draws. - uint32 internal _drawPeriodSeconds; - /* ============ Custom Errors ============ */ /// @notice Thrown when the PrizePool address passed to the constructor is zero address. error PrizePoolNotZeroAddress(); - /// @notice Thrown when the Draw period seconds passed to the constructor is zero. - error DrawPeriodSecondsNotZero(); + /* ============ Events ============ */ + + /** + * @notice Emitted when a Draw auction phase has completed. + * @param phaseId Id of the phase + * @param caller Address of the caller + */ + event DrawAuctionPhaseCompleted(uint256 indexed phaseId, address indexed caller); + + /** + * @notice Emitted when a Draw auction has completed and rewards have been distributed. + * @param phaseIds Ids of the phases that were rewarded + * @param rewardRecipients Addresses of the rewards recipients per phase id + * @param rewardAmounts Amounts of rewards distributed per phase id + */ + event DrawAuctionRewardsDistributed( + uint8[] phaseIds, + address[] rewardRecipients, + uint256[] rewardAmounts + ); /* ============ Constructor ============ */ /** * @notice Contract constructor. - * @dev We pass the `drawPeriodSeconds` cause the PrizePool we want to interact with may live on L2. + * @param rng_ Address of the RNG service + * @param rngTimeout_ Time in seconds before an RNG request can be cancelled * @param prizePool_ Address of the prize pool - * @param drawPeriodSeconds_ Draw period in seconds + * @param _auctionPhases Number of auction phases * @param auctionDuration_ Duration of the auction in seconds + * @param _owner Address of the DrawAuction owner */ - constructor(PrizePool prizePool_, uint32 drawPeriodSeconds_, uint32 auctionDuration_) { + constructor( + RNGInterface rng_, + uint32 rngTimeout_, + PrizePool prizePool_, + uint8 _auctionPhases, + uint256 auctionDuration_, + address _owner + ) TwoStepsAuction(rng_, rngTimeout_, _auctionPhases, auctionDuration_, _owner) { + if (address(prizePool_) == address(0)) revert PrizePoolNotZeroAddress(); _prizePool = prizePool_; - _drawPeriodSeconds = drawPeriodSeconds_; - _auctionDuration = auctionDuration_; - } - - /* ============ External Functions ============ */ - - /** - * @notice Complete the current Draw and start the next one. - * @param winningRandomNumber_ The winning random number for the current Draw - * @return Reward amount - */ - function completeAndStartNextDraw(uint256 winningRandomNumber_) external returns (uint256) { - uint256 _rewardAmount = _reward(); - - _prizePool.completeAndStartNextDraw(winningRandomNumber_); - _prizePool.withdrawReserve(msg.sender, uint104(_rewardAmount)); - - return _rewardAmount; } /* ============ Getter Functions ============ */ - /** - * @notice Duration of the auction. - * @dev This is the time it takes for the auction to reach the PrizePool full reserve amount. - * @return Duration of the auction in seconds - */ - function auctionDuration() external view returns (uint256) { - return _auctionDuration; - } - /** * @notice Prize Pool instance for which the Draw is triggered. * @return Prize Pool instance @@ -81,32 +81,45 @@ contract DrawAuction { } /** - * @notice Current reward for calling `completeAndStartNextDraw`. + * @notice Current reward for completing the Auction phase. + * @param _phaseId ID of the phase to get reward for (i.e. 0 for `startRNGRequest` or 1 for `completeRNGRequest`) * @return Reward amount */ - function reward() external view returns (uint256) { - return _reward(); + function reward(uint8 _phaseId) external view returns (uint256) { + return RewardLib.getReward(_phases, _phaseId, _prizePool, _auctionDuration); } - /* ============ Internal Functions ============ */ - /** - * @notice Current reward for calling `completeAndStartNextDraw`. - * @dev The reward amount is computed via linear interpolation starting from 0 - * and increasing as the auction goes on to the full reserve amount. - * @return Reward amount + * @notice Hook called after the RNG request has completed. + * @param _randomNumber The random number that was generated */ - function _reward() internal view returns (uint256) { - uint256 _nextDrawEndsAt = _prizePool.nextDrawEndsAt(); - - if (block.timestamp < _nextDrawEndsAt) { - return 0; - } - - uint256 _reserve = _prizePool.reserve() + _prizePool.reserveForNextDraw(); - uint256 _elapsedTime = block.timestamp - _nextDrawEndsAt; - - return - _elapsedTime >= _auctionDuration ? _reserve : (_elapsedTime * _reserve) / _auctionDuration; + function _afterAuctionEnds(uint256 _randomNumber) internal override { + // Phase memory _startRNGPhase = _getPhase(0); + // Phase memory _completeRNGPhase = _setPhase(1, _startRNGPhase.startTime, uint64(block.timestamp), _rewardRecipient); + // uint256 _startRNGRewardAmount = _reward(_startRNGPhase); + // console2.log("_startRNGRewardAmount", _startRNGRewardAmount); + // uint256 _completeRNGRewardAmount = _reward(_completeRNGPhase); + // _prizePool.completeAndStartNextDraw(_randomNumber); + // if (_startRNGPhase.recipient == _completeRNGPhase.recipient) { + // _prizePool.withdrawReserve(_startRNGPhase.recipient, uint104(_startRNGRewardAmount + _completeRNGRewardAmount)); + // } else { + // _prizePool.withdrawReserve(_startRNGPhase.recipient, uint104(_startRNGRewardAmount)); + // _prizePool.withdrawReserve(_completeRNGPhase.recipient, uint104(_completeRNGRewardAmount)); + // } + // uint8[] memory _phaseIds = new uint8[](2); + // _phaseIds[0] = _startRNGPhase.id; + // _phaseIds[1] = _completeRNGPhase.id; + // address[] memory _rewardRecipients = new address[](2); + // _rewardRecipients[0] = _startRNGPhase.recipient; + // _rewardRecipients[1] = _completeRNGPhase.recipient; + // uint256[] memory _rewardAmounts = new uint256[](2); + // _rewardAmounts[0] = _startRNGRewardAmount; + // _rewardAmounts[1] = _completeRNGRewardAmount; + // emit DrawAuctionPhaseCompleted(1, msg.sender); + // emit DrawAuctionRewardsDistributed( + // _phaseIds, + // _rewardRecipients, + // _rewardAmounts + // ); } } diff --git a/src/RNGRequestor.sol b/src/RNGRequestor.sol index 9502cf2..bdb5ee0 100644 --- a/src/RNGRequestor.sol +++ b/src/RNGRequestor.sol @@ -10,6 +10,7 @@ import { PrizePool } from "v5-prize-pool/PrizePool.sol"; /** * @title PoolTogether V5 RNGRequestor * @author PoolTogether Inc. Team + * TODO: rephrase doc to explain how DrawAuction inherit from this contract * @notice The RNGRequestor allows anyone to request a RNG using the RNG service set. * This contract can be inherited by other contracts and use the `_afterRNGComplete` hook * to make use of the random number generated by the RNG. @@ -159,8 +160,9 @@ contract RNGRequestor is Ownable { * @dev Will revert if an RNG request has already been requested. * @dev If the RNG Service request a `feeToken` for payment, * the RNG-Request-Fee is expected to be held within this contract before calling this function. + * @param _rewardRecipient Address that will receive the auction reward for starting the RNG request */ - function startRNGRequest() external requireCanStartRNGRequest { + function startRNGRequest(address _rewardRecipient) external requireCanStartRNGRequest { (address _feeToken, uint256 _requestFee) = _rng.getRequestFee(); if (_feeToken != address(0) && _requestFee > 0) { @@ -172,20 +174,23 @@ contract RNGRequestor is Ownable { _rngRequest.lockBlock = _lockBlock; _rngRequest.requestedAt = _currentTime(); + _afterRNGStart(_rewardRecipient); + emit RNGRequestStarted(_requestId, _lockBlock); } /** * @notice Completes the RNG request. * @dev Will revert if no RNG has been requested or if the RNG request has not completed yet. + * @param _rewardRecipient Address that will receive the auction reward for completing the RNG request */ - function completeRNGRequest() external requireCanCompleteRNGRequest { + function completeRNGRequest(address _rewardRecipient) external requireCanCompleteRNGRequest { uint32 _rngRequestId = _rngRequest.id; uint256 _randomNumber = _rng.randomNumber(_rngRequestId); delete _rngRequest; - _afterRNGComplete(_randomNumber); + _afterRNGComplete(_randomNumber, _rewardRecipient); emit RNGRequestCompleted(_rngRequestId, _randomNumber); } @@ -303,11 +308,18 @@ contract RNGRequestor is Ownable { /* ============ Internal Functions ============ */ + /** + * @notice Hook called after the RNG request has started. + * @param _rewardRecipient Address that will receive the auction reward for starting the RNG request + */ + function _afterRNGStart(address _rewardRecipient) internal virtual {} + /** * @notice Hook called after the RNG request has completed. * @param _randomNumber The random number that was generated + * @param _rewardRecipient Address that will receive the auction reward for completing the RNG request */ - function _afterRNGComplete(uint256 _randomNumber) internal {} + function _afterRNGComplete(uint256 _randomNumber, address _rewardRecipient) internal virtual {} /** * @notice Returns the current timestamp. diff --git a/src/auctions/Auction.sol b/src/auctions/Auction.sol new file mode 100644 index 0000000..00b6ec4 --- /dev/null +++ b/src/auctions/Auction.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { AuctionLib } from "src/libraries/AuctionLib.sol"; + +contract Auction { + /* ============ Variables ============ */ + + /// @notice Duration of the auction in seconds. + uint256 internal _auctionDuration; + + /// @notice Array storing phases per id in ascending order. + AuctionLib.Phase[] internal _phases; + + /* ============ Custom Errors ============ */ + + /// @notice Thrown when the auction duration passed to the constructor is zero. + error AuctionDurationNotZero(); + + /// @notice Thrown when the number of auction phases passed to the constructor is zero. + error AuctionPhasesNotZero(); + + /* ============ Events ============ */ + + /** + @notice Emitted when a phase is set. + @param phaseId Id of the phase + @param startTime Start time of the phase + @param endTime End time of the phase + @param recipient Recipient of the phase reward + */ + event PhaseSet( + uint8 indexed phaseId, + uint64 startTime, + uint64 endTime, + address indexed recipient + ); + + /* ============ Constructor ============ */ + + /** + * @notice Contract constructor. + * @param _auctionPhases Number of auction phases + * @param auctionDuration_ Duration of the auction in seconds + */ + constructor(uint8 _auctionPhases, uint256 auctionDuration_) { + if (_auctionPhases == 0) revert AuctionPhasesNotZero(); + if (auctionDuration_ == 0) revert AuctionDurationNotZero(); + + for (uint8 i = 0; i < _auctionPhases; i++) { + _phases.push( + AuctionLib.Phase({ id: i, startTime: uint64(0), endTime: uint64(0), recipient: address(0) }) + ); + } + + _auctionDuration = auctionDuration_; + } + + /* ============ External Functions ============ */ + + /* ============ Getters ============ */ + + /** + * @notice Duration of the auction. + * @dev This is the time it takes for the auction to reach the PrizePool full reserve amount. + * @return Duration of the auction in seconds + */ + function auctionDuration() external view returns (uint256) { + return _auctionDuration; + } + + function getPhase(uint256 _phaseId) external view returns (AuctionLib.Phase memory) { + return _getPhase(_phaseId); + } + + /* ============ Internal Functions ============ */ + + /* ============ Hooks ============ */ + + /** + * @notice Hook called after the auction has ended. + * @param _randomNumber The random number that was generated + */ + function _afterAuctionEnds(uint256 _randomNumber) internal virtual {} + + /* ============ Getters ============ */ + + function _getPhase(uint256 _phaseId) internal view returns (AuctionLib.Phase memory) { + return _phases[_phaseId]; + } + + /* ============ Setters ============ */ + + function _setPhase( + uint8 _phaseId, + uint64 _startTime, + uint64 _endTime, + address _recipient + ) internal returns (AuctionLib.Phase memory) { + AuctionLib.Phase memory _phase = AuctionLib.Phase({ + id: _phaseId, + startTime: _startTime, + endTime: _endTime, + recipient: _recipient + }); + + _phases[_phaseId] = _phase; + + emit PhaseSet(_phaseId, _startTime, _endTime, _recipient); + + return _phase; + } +} diff --git a/src/auctions/TwoStepsAuction.sol b/src/auctions/TwoStepsAuction.sol new file mode 100644 index 0000000..445bc3d --- /dev/null +++ b/src/auctions/TwoStepsAuction.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { Auction } from "src/auctions/Auction.sol"; +import { RNGRequestor, RNGInterface } from "src/RNGRequestor.sol"; + +contract TwoStepsAuction is Auction, RNGRequestor { + /* ============ Constructor ============ */ + + /** + * @notice Contract constructor. + * @param rng_ Address of the RNG service + * @param rngTimeout_ Time in seconds before an RNG request can be cancelled + * @param _auctionPhases Number of auction phases + * @param auctionDuration_ Duration of the auction in seconds + * @param _owner Address of the DrawAuction owner + */ + constructor( + RNGInterface rng_, + uint32 rngTimeout_, + uint8 _auctionPhases, + uint256 auctionDuration_, + address _owner + ) Auction(_auctionPhases, auctionDuration_) RNGRequestor(rng_, rngTimeout_, _owner) {} + + /* ============ Internal Functions ============ */ + + /* ============ Hooks ============ */ + + /** + * @notice Hook called after the RNG request has started. + * @dev The auction is not aware of the PrizePool contract, so startTime is set to 0. + * Since the first phase of the auction starts when the draw has ended, + * we can derive the actual startTime by calling PrizePool.nextDrawEndsAt() when computing the reward. + * @param _rewardRecipient Address that will receive the auction reward for starting the RNG request + */ + function _afterRNGStart(address _rewardRecipient) internal override { + _setPhase(0, 0, uint64(block.timestamp), _rewardRecipient); + } + + /** + * @notice Hook called after the RNG request has completed. + * @param _randomNumber The random number that was generated + * @param _rewardRecipient Address that will receive the auction reward for completing the RNG request + */ + function _afterRNGComplete(uint256 _randomNumber, address _rewardRecipient) internal override { + _setPhase(1, _getPhase(0).endTime, uint64(block.timestamp), _rewardRecipient); + _afterAuctionEnds(_randomNumber); + } +} diff --git a/src/libraries/AuctionLib.sol b/src/libraries/AuctionLib.sol new file mode 100644 index 0000000..0e26498 --- /dev/null +++ b/src/libraries/AuctionLib.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +contract AuctionLib { + /* ============ Structs ============ */ + + /** + * @notice Struct representing the phase of an auction. + * @param id Id of the phase + * @param startTime Start time of the phase + * @param endTime End time of the phase + * @param recipient Recipient of the phase reward + */ + struct Phase { + uint8 id; + uint64 startTime; + uint64 endTime; + address recipient; + } +} diff --git a/src/libraries/RewardLib.sol b/src/libraries/RewardLib.sol new file mode 100644 index 0000000..153aa8b --- /dev/null +++ b/src/libraries/RewardLib.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { PrizePool } from "v5-prize-pool/PrizePool.sol"; + +import { AuctionLib } from "src/libraries/AuctionLib.sol"; + +import { console2 } from "forge-std/console2.sol"; + +library RewardLib { + /** + * @notice Current reward for completing the Auction phase. + * @dev The reward amount is computed via linear interpolation starting from 0 + * and increasing as the auction goes on to the full reserve amount. + * @param _phaseId ID of the phase to get reward for + * @param _prizePool Address of the Prize Pool to get auction reward for + * @param _auctionDuration Duration of the auction in seconds + * @return Reward amount + */ + function getReward( + AuctionLib.Phase[] memory _phases, + uint8 _phaseId, + PrizePool _prizePool, + uint256 _auctionDuration + ) internal view returns (uint256) { + // TODO: which value would nextDrawEndsAt return if the draw has been awarded? + uint64 _nextDrawEndsAt = _prizePool.nextDrawEndsAt(); + + console2.log("_reward block.timestamp", block.timestamp); + console2.log("_reward _nextDrawEndsAt", _nextDrawEndsAt); + console2.log("_reward _prizePool.nextDrawStartsAt()", _prizePool.nextDrawStartsAt()); + console2.log("_reward periodDiff", _nextDrawEndsAt - _prizePool.nextDrawStartsAt()); + + // If the Draw has not ended yet, we return 0 + if (block.timestamp <= _nextDrawEndsAt) { + return 0; + } + + AuctionLib.Phase memory _phase = _phases[_phaseId]; + uint256 _elapsedTime; + + if (_phase.id == 0) { + // Elapsed time between the timestamp at which the Draw ended and the first phase end time + // End time will be block.timestamp if this phase has not been triggered yet + // Is unchecked since block.timestamp can't be lower than nextDrawEndsAt + unchecked { + _elapsedTime = (_phase.endTime != 0 ? _phase.endTime : block.timestamp) - _nextDrawEndsAt; + console2.log("_reward _phase.startTime", _phase.startTime); + console2.log("_reward _elapsedTime", _elapsedTime); + } + } else { + // Retrieve the previous phase + AuctionLib.Phase memory _previousPhase = _phases[_phase.id - 1]; + + // If the previous phase has not been triggered, + // we return 0 cause we can't compute reward for this phase + if (_previousPhase.endTime == 0) { + return 0; + } + + // Elapsed time between this phase startTime and endTime + // If startTime is different than 0, it means that this phase has been recorded and endTime is also set + // End time will be block.timestamp if this phase has not been triggered yet + // Is unchecked since block.timestamp can't be lower than previousPhase.endTime + unchecked { + _elapsedTime = _phase.startTime != 0 + ? _phase.endTime - _phase.startTime + : block.timestamp - _previousPhase.endTime; + } + } + + uint256 _reserve = _prizePool.reserve() + _prizePool.reserveForNextDraw(); + + // We can't award more than the available reserve + // TODO: look at _nextDrawEndsAt in the PrizePool to figure out if the auction duration could be extended + if (_elapsedTime >= _auctionDuration) { + return _reserve; + } + + console2.log("_elapsedTime", _elapsedTime); + + return (_elapsedTime * _reserve) / _auctionDuration; + } +} diff --git a/test/DrawAuction.t.sol b/test/DrawAuction.t.sol index b9a3d91..811e0ed 100644 --- a/test/DrawAuction.t.sol +++ b/test/DrawAuction.t.sol @@ -3,80 +3,207 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; import { console2 } from "forge-std/console2.sol"; +import { ERC20Mock } from "openzeppelin/mocks/ERC20Mock.sol"; -import { UD2x18, SD1x18, IERC20, PrizePool, TieredLiquidityDistributor, TwabController } from "v5-prize-pool/PrizePool.sol"; +import { UD2x18, SD1x18, ConstructorParams, PrizePool, TieredLiquidityDistributor, TwabController } from "v5-prize-pool/PrizePool.sol"; import { DrawAuction } from "src/DrawAuction.sol"; +import { Helpers, RNGInterface } from "./helpers/Helpers.t.sol"; -contract DrawAuctionTest is Test { - DrawAuction internal _drawAuction; - PrizePool internal _prizePool; +contract DrawAuctionTest is Helpers { + /* ============ Events ============ */ + event DrawAuctionCompleted(address indexed caller, uint256 rewardAmount); - uint32 internal _auctionDuration = 3 hours; + /* ============ Variables ============ */ - function setUp() public { - _prizePool = new PrizePool( - IERC20(address(0)), - TwabController(address(0)), - uint32(365), - 1 days, - uint64(block.timestamp), - uint8(2), // minimum number of tiers - 100, - 10, - 10, - UD2x18.wrap(0.9e18), // claim threshold of 90% - SD1x18.wrap(0.9e18) // alpha - ); + DrawAuction public drawAuction; + ERC20Mock public prizeToken; + PrizePool public prizePool; + RNGInterface public rng; - _drawAuction = new DrawAuction(_prizePool, 86400, _auctionDuration); + uint32 public auctionDuration = 3 hours; + uint32 public rngTimeOut = 1 hours; + uint32 public drawPeriodSeconds = 1 days; + uint256 public randomNumber = 123456789; + address public recipient = address(this); + function setUp() public { vm.warp(0); - vm.mockCall( - address(_prizePool), - abi.encodeWithSelector(TieredLiquidityDistributor.reserve.selector), - abi.encode(100e18) + prizeToken = new ERC20Mock(); + console2.log("drawPeriodSeconds", drawPeriodSeconds); + + prizePool = new PrizePool( + ConstructorParams({ + prizeToken: prizeToken, + twabController: TwabController(address(0)), + drawManager: address(0), + grandPrizePeriodDraws: uint32(365), + drawPeriodSeconds: drawPeriodSeconds, + firstDrawStartsAt: uint64(block.timestamp), + numberOfTiers: uint8(3), // minimum number of tiers + tierShares: 100, + canaryShares: 10, + reserveShares: 10, + claimExpansionThreshold: UD2x18.wrap(0.9e18), // claim threshold of 90% + smoothing: SD1x18.wrap(0.9e18) // alpha + }) ); - vm.mockCall( - address(_prizePool), - abi.encodeWithSelector(PrizePool.reserveForNextDraw.selector), - abi.encode(100e18) - ); + rng = RNGInterface(address(1)); - vm.mockCall( - address(_prizePool), - abi.encodeWithSelector(PrizePool.nextDrawEndsAt.selector), - abi.encode(1 days) - ); + drawAuction = new DrawAuction(rng, rngTimeOut, prizePool, 2, auctionDuration, address(this)); + + prizePool.setDrawManager(address(drawAuction)); + } + + /* ============ Getter Functions ============ */ + + function testAuctionDuration() public { + assertEq(drawAuction.auctionDuration(), auctionDuration); + } + + function testPrizePool() public { + assertEq(address(drawAuction.prizePool()), address(prizePool)); + } + + /* ============ State Functions ============ */ + + /* ============ reward ============ */ + + function testRewardBeforeDrawEnds() public { + assertEq(drawAuction.reward(0), 0); + assertEq(drawAuction.reward(1), 0); } - function testRewardBeforeTime() public { - assertEq(_drawAuction.reward(), 0); + function testRewardAtDrawEnds() public { + vm.warp(drawPeriodSeconds); + + assertEq(drawAuction.reward(0), 0); + assertEq(drawAuction.reward(1), 0); } - function testRewardAtTime0() public { - vm.warp(1 days); + /* ============ Half Time ============ */ + /* ============ Phase 0 ============ */ + function testPhase0RewardAtHalfTime() public { + vm.warp(drawPeriodSeconds + (auctionDuration / 2)); - assertEq(_drawAuction.reward(), 0); + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + assertEq(drawAuction.reward(0), _reserveAmount / 2); } - function testRewardAtHalfTime() public { - vm.warp(1 days + _auctionDuration / 2); + function testPhase0TriggeredRewardAtHalfTime() public { + vm.warp(drawPeriodSeconds + (auctionDuration / 2)); + + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + _mockStartRNGRequest(address(rng), address(0), 0, uint32(1), uint32(block.number)); + drawAuction.startRNGRequest(recipient); + + vm.warp(drawPeriodSeconds + (auctionDuration)); + + assertEq(drawAuction.reward(0), _reserveAmount / 2); + } - assertEq(_drawAuction.reward(), 100e18); + /* ============ Phase 1 ============ */ + function testPhase1RewardAtHalfTimePhase0NotTriggered() public { + vm.warp(drawPeriodSeconds + (auctionDuration / 2)); + assertEq(drawAuction.reward(1), 0); } - function testRewardAtFullTime() public { - vm.warp(1 days + _auctionDuration); + function testPhase1RewardAtHalfTimePhase0Triggered() public { + uint256 _startRNGRequestTime = drawPeriodSeconds + (auctionDuration / 4); // drawPeriodSeconds + 45 minutes + + vm.warp(_startRNGRequestTime); + + _mockStartRNGRequest(address(rng), address(0), 0, uint32(1), uint32(block.number)); + drawAuction.startRNGRequest(recipient); + + vm.warp(drawPeriodSeconds + (auctionDuration / 2)); - assertEq(_drawAuction.reward(), 200e18); + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + assertEq( + drawAuction.reward(1), + _computeReward(block.timestamp - _startRNGRequestTime, _reserveAmount, auctionDuration) + ); } - function testRewardAfterTime() public { - vm.warp(2 days); + function testPhase1TriggeredRewardAtHalfTime() public { + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + uint256 _startRNGRequestTime = drawPeriodSeconds + (auctionDuration / 4); // drawPeriodSeconds + 45 minutes + + vm.warp(_startRNGRequestTime); + + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); + drawAuction.startRNGRequest(recipient); + + vm.warp(drawPeriodSeconds + (auctionDuration / 2)); + + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + assertEq( + drawAuction.reward(1), + _computeReward(block.timestamp - _startRNGRequestTime, _reserveAmount, auctionDuration) + ); + + _mockCompleteRNGRequest(address(rng), _requestId, randomNumber); + + drawAuction.completeRNGRequest(recipient); + } + + /* ============ At or Aftter auction ends ============ */ + + function testRewardAtAuctionEnd() public { + vm.warp(drawPeriodSeconds + auctionDuration); + + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + assertEq(drawAuction.reward(0), 200e18); + } + + function testRewardAfterAuctionEnd() public { + vm.warp(drawPeriodSeconds + drawPeriodSeconds / 2); + + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + assertEq(drawAuction.reward(0), _reserveAmount); + } + + /* ============ _afterRNGComplete ============ */ + + function testAfterRNGComplete() public { + console2.log("accountedBalance before", prizePool.accountedBalance()); + uint256 _reserveAmount = 200e18; + + prizeToken.mint(address(prizePool), _reserveAmount * 2); + prizePool.contributePrizeTokens(address(2), _reserveAmount * 2); + + console2.log("accountedBalance after", prizePool.accountedBalance()); + + vm.warp(drawPeriodSeconds + auctionDuration); + + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + // uint256 _rewardAmount = drawAuction.reward(0); + + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); + + drawAuction.startRNGRequest(recipient); + + _mockCompleteRNGRequest(address(rng), _requestId, randomNumber); + + // vm.expectEmit(); + // emit DrawAuctionCompleted(address(this), _rewardAmount); - assertEq(_drawAuction.reward(), 200e18); + drawAuction.completeRNGRequest(recipient); } } diff --git a/test/RNGRequestor.t.sol b/test/RNGRequestor.t.sol index 1ee007a..79ddf93 100644 --- a/test/RNGRequestor.t.sol +++ b/test/RNGRequestor.t.sol @@ -1,15 +1,12 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.17; -import "forge-std/Test.sol"; -import { console2 } from "forge-std/console2.sol"; - import { ERC20Mock } from "openzeppelin/mocks/ERC20Mock.sol"; -import { RNGInterface } from "rng/RNGInterface.sol"; import { RNGRequestor } from "src/RNGRequestor.sol"; +import { Helpers, RNGInterface } from "./helpers/Helpers.t.sol"; -contract RNGRequestorTest is Test { +contract RNGRequestorTest is Helpers { /* ============ Events ============ */ event RNGServiceSet(RNGInterface indexed rngService); @@ -22,17 +19,17 @@ contract RNGRequestorTest is Test { RNGInterface public rng; RNGRequestor public rngRequestor; - uint32 public rngTimeOut; + uint32 public rngTimeOut = 1 hours; ERC20Mock public feeToken; uint256 public feeAmount; + address public recipient = address(this); function setUp() public { feeToken = new ERC20Mock(); feeAmount = 2e18; rng = RNGInterface(address(1)); - rngTimeOut = 1 hours; rngRequestor = new RNGRequestor(rng, rngTimeOut, address(this)); } @@ -52,30 +49,28 @@ contract RNGRequestorTest is Test { uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - _mockGetRequestFee(address(0), 0); - _mockRequestRandomNumber(_requestId, _lockBlock); + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); vm.expectEmit(); emit RNGRequestStarted(_requestId, _lockBlock); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); assertEq(rngRequestor.getRNGLockBlock(), _lockBlock); assertEq(rngRequestor.getRNGRequestId(), _requestId); } - // @TODO Test with ChainlinkVRFV2 direct LINK transfer contact + // @TODO Test with ChainlinkVRFV2 direct LINK transfer contract function testStartRNGRequestWithFeeToken() public { uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - _mockGetRequestFee(address(feeToken), feeAmount); - _mockRequestRandomNumber(_requestId, _lockBlock); + _mockStartRNGRequest(address(rng), address(feeToken), feeAmount, _requestId, _lockBlock); vm.expectEmit(); emit RNGRequestStarted(_requestId, _lockBlock); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); assertEq(rngRequestor.getRNGLockBlock(), _lockBlock); assertEq(rngRequestor.getRNGRequestId(), _requestId); @@ -85,14 +80,13 @@ contract RNGRequestorTest is Test { uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - _mockGetRequestFee(address(0), 0); - _mockRequestRandomNumber(_requestId, _lockBlock); + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); vm.expectRevert(abi.encodeWithSelector(RNGRequestor.RNGRequested.selector, _requestId)); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); } /* ============ completeRNGRequest ============ */ @@ -101,40 +95,38 @@ contract RNGRequestorTest is Test { uint32 _lockBlock = uint32(block.number); uint256 _randomNumber = 123456789; - _mockGetRequestFee(address(0), 0); - _mockRequestRandomNumber(_requestId, _lockBlock); + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); - _mockIsRequestComplete(_requestId, true); - _mockRandomNumber(_requestId, _randomNumber); + _mockIsRequestComplete(address(rng), _requestId, true); + _mockRandomNumber(address(rng), _requestId, _randomNumber); vm.expectEmit(); emit RNGRequestCompleted(_requestId, _randomNumber); - rngRequestor.completeRNGRequest(); + rngRequestor.completeRNGRequest(recipient); } function testCompleteRNGRequestFailRNGNotRequested() public { vm.expectRevert(abi.encodeWithSelector(RNGRequestor.RNGNotRequested.selector)); - rngRequestor.completeRNGRequest(); + rngRequestor.completeRNGRequest(recipient); } function testCompleteRNGRequestFailRNGNotCompleted() public { uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - _mockGetRequestFee(address(0), 0); - _mockRequestRandomNumber(_requestId, _lockBlock); + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); - _mockIsRequestComplete(_requestId, false); + _mockIsRequestComplete(address(rng), _requestId, false); vm.expectRevert(abi.encodeWithSelector(RNGRequestor.RNGNotCompleted.selector, _requestId)); - rngRequestor.completeRNGRequest(); + rngRequestor.completeRNGRequest(recipient); } /* ============ cancelRNGRequest ============ */ @@ -142,10 +134,9 @@ contract RNGRequestorTest is Test { uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - _mockGetRequestFee(address(0), 0); - _mockRequestRandomNumber(_requestId, _lockBlock); + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); vm.warp(2 hours); @@ -159,10 +150,9 @@ contract RNGRequestorTest is Test { uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - _mockGetRequestFee(address(0), 0); - _mockRequestRandomNumber(_requestId, _lockBlock); + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); vm.expectRevert(abi.encodeWithSelector(RNGRequestor.RNGHasNotTimedout.selector)); @@ -180,25 +170,24 @@ contract RNGRequestorTest is Test { uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - _mockGetRequestFee(address(0), 0); - _mockRequestRandomNumber(_requestId, _lockBlock); + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); vm.expectEmit(); emit RNGRequestStarted(_requestId, _lockBlock); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); assertEq(rngRequestor.isRNGRequested(), true); } /* ============ isRNGCompleted ============ */ function testIsRNGCompletedDefaultState() public { - _mockIsRequestComplete(uint32(0), false); + _mockIsRequestComplete(address(rng), uint32(0), false); assertEq(rngRequestor.isRNGCompleted(), false); } function testIsRNGCompletedActiveState() public { - _mockIsRequestComplete(uint32(0), true); + _mockIsRequestComplete(address(rng), uint32(0), true); assertEq(rngRequestor.isRNGCompleted(), true); } @@ -211,10 +200,9 @@ contract RNGRequestorTest is Test { uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - _mockGetRequestFee(address(0), 0); - _mockRequestRandomNumber(_requestId, _lockBlock); + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); vm.warp(2 hours); assertEq(rngRequestor.isRNGTimedOut(), true); @@ -229,10 +217,9 @@ contract RNGRequestorTest is Test { uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - _mockGetRequestFee(address(0), 0); - _mockRequestRandomNumber(_requestId, _lockBlock); + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); assertEq(rngRequestor.canStartRNGRequest(), false); } @@ -246,11 +233,10 @@ contract RNGRequestorTest is Test { uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - _mockGetRequestFee(address(0), 0); - _mockRequestRandomNumber(_requestId, _lockBlock); - _mockIsRequestComplete(_requestId, true); + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); + _mockIsRequestComplete(address(rng), _requestId, true); - rngRequestor.startRNGRequest(); + rngRequestor.startRNGRequest(recipient); assertEq(rngRequestor.canCompleteRNGRequest(), true); } @@ -313,37 +299,4 @@ contract RNGRequestorTest is Test { rngRequestor.setRNGTimeout(_newRNGTimeout); } - - /* ============ Mock Functions ============ */ - function _mockGetRequestFee(address _feeToken, uint256 _requestFee) internal { - vm.mockCall( - address(rng), - abi.encodeWithSelector(RNGInterface.getRequestFee.selector), - abi.encode(_feeToken, _requestFee) - ); - } - - function _mockRequestRandomNumber(uint32 _requestId, uint32 _lockBlock) internal { - vm.mockCall( - address(rng), - abi.encodeWithSelector(RNGInterface.requestRandomNumber.selector), - abi.encode(_requestId, _lockBlock) - ); - } - - function _mockRandomNumber(uint32 _requestId, uint256 _randomNumber) internal { - vm.mockCall( - address(rng), - abi.encodeWithSelector(RNGInterface.randomNumber.selector, _requestId), - abi.encode(_randomNumber) - ); - } - - function _mockIsRequestComplete(uint32 _requestId, bool _isRequestComplete) internal { - vm.mockCall( - address(rng), - abi.encodeWithSelector(RNGInterface.isRequestComplete.selector, _requestId), - abi.encode(_isRequestComplete) - ); - } } diff --git a/test/helpers/Helpers.t.sol b/test/helpers/Helpers.t.sol new file mode 100644 index 0000000..5419265 --- /dev/null +++ b/test/helpers/Helpers.t.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; + +import { PrizePool, TieredLiquidityDistributor } from "v5-prize-pool/PrizePool.sol"; +import { RNGInterface } from "rng/RNGInterface.sol"; + +contract Helpers is Test { + /* ============ Mock Functions ============ */ + + /* ============ RNGRequestor ============ */ + function _mockGetRequestFee(address _rng, address _feeToken, uint256 _requestFee) internal { + vm.mockCall( + _rng, + abi.encodeWithSelector(RNGInterface.getRequestFee.selector), + abi.encode(_feeToken, _requestFee) + ); + } + + function _mockRequestRandomNumber(address _rng, uint32 _requestId, uint32 _lockBlock) internal { + vm.mockCall( + _rng, + abi.encodeWithSelector(RNGInterface.requestRandomNumber.selector), + abi.encode(_requestId, _lockBlock) + ); + } + + function _mockStartRNGRequest( + address _rng, + address _feeToken, + uint256 _requestFee, + uint32 _requestId, + uint32 _lockBlock + ) internal { + _mockGetRequestFee(_rng, _feeToken, _requestFee); + _mockRequestRandomNumber(_rng, _requestId, _lockBlock); + } + + function _mockIsRequestComplete( + address _rng, + uint32 _requestId, + bool _isRequestComplete + ) internal { + vm.mockCall( + _rng, + abi.encodeWithSelector(RNGInterface.isRequestComplete.selector, _requestId), + abi.encode(_isRequestComplete) + ); + } + + function _mockRandomNumber(address _rng, uint32 _requestId, uint256 _randomNumber) internal { + vm.mockCall( + _rng, + abi.encodeWithSelector(RNGInterface.randomNumber.selector, _requestId), + abi.encode(_randomNumber) + ); + } + + function _mockCompleteRNGRequest( + address _rng, + uint32 _requestId, + uint256 _randomNumber + ) internal { + _mockIsRequestComplete(_rng, _requestId, true); + _mockRandomNumber(_rng, _requestId, _randomNumber); + } + + /* ============ PrizePool ============ */ + function _mockReserve(address _prizePool, uint256 _amount) internal { + vm.mockCall( + _prizePool, + abi.encodeWithSelector(TieredLiquidityDistributor.reserve.selector), + abi.encode(_amount) + ); + } + + function _mockReserveForNextDraw(address _prizePool, uint256 _amount) internal { + vm.mockCall( + _prizePool, + abi.encodeWithSelector(PrizePool.reserveForNextDraw.selector), + abi.encode(_amount) + ); + } + + function _mockReserves(address _prizePool, uint256 _reserveAmount) internal { + uint256 _amount = _reserveAmount / 2; + + _mockReserve(_prizePool, _amount); + _mockReserveForNextDraw(_prizePool, _amount); + } + + /* ============ Computations ============ */ + + function _computeReward( + uint256 _elapsedTime, + uint256 _reserve, + uint256 _auctionDuration + ) internal pure returns (uint256) { + return (_elapsedTime * _reserve) / _auctionDuration; + } +} From 678c5cd6a67aba59ef53947340c0b7177be10f85 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 22 Jun 2023 19:06:59 -0500 Subject: [PATCH 3/8] feat(contracts): add unit tests --- lcov.info | 302 +++++++++++++----------- src/DrawAuction.sol | 98 ++++---- src/RNGRequestor.sol | 1 - src/auctions/Auction.sol | 67 +++++- src/auctions/TwoStepsAuction.sol | 5 +- src/libraries/RewardLib.sol | 143 ++++++----- test/DrawAuction.t.sol | 135 +++++++---- test/auctions/Auctions.t.sol | 104 ++++++++ test/auctions/TwoStepsAuction.t.sol | 47 ++++ test/harness/AuctionHarness.sol | 20 ++ test/harness/RewardLibHarness.sol | 49 ++++ test/harness/TwoStepsAuctionHarness.sol | 22 ++ test/helpers/Helpers.t.sol | 23 +- test/libraries/RewardLib.t.sol | 251 ++++++++++++++++++++ 14 files changed, 967 insertions(+), 300 deletions(-) create mode 100644 test/auctions/Auctions.t.sol create mode 100644 test/auctions/TwoStepsAuction.t.sol create mode 100644 test/harness/AuctionHarness.sol create mode 100644 test/harness/RewardLibHarness.sol create mode 100644 test/harness/TwoStepsAuctionHarness.sol create mode 100644 test/libraries/RewardLib.t.sol diff --git a/lcov.info b/lcov.info index 4ae8a35..1a02388 100644 --- a/lcov.info +++ b/lcov.info @@ -1,44 +1,65 @@ TN: SF:src/DrawAuction.sol -FN:79,DrawAuction.prizePool -FN:88,DrawAuction.reward -FN:96,DrawAuction._afterAuctionEnds +FN:60,DrawAuction.prizePool +FN:69,DrawAuction.reward +FN:81,DrawAuction._afterAuctionEnds FNDA:1,DrawAuction.prizePool -FNDA:11,DrawAuction.reward -FNDA:2,DrawAuction._afterAuctionEnds +FNDA:7,DrawAuction.reward +FNDA:3,DrawAuction._afterAuctionEnds FNF:3 FNH:3 -DA:80,1 -DA:89,11 -LF:2 -LH:2 +DA:61,1 +DA:70,7 +DA:82,3 +DA:83,3 +DA:85,3 +DA:86,3 +DA:87,3 +DA:89,3 +DA:91,3 +DA:93,3 +DA:95,3 +DA:96,3 +DA:98,3 +DA:99,2 +DA:101,1 +DA:102,1 +DA:105,3 +DA:106,3 +DA:107,3 +DA:109,3 +DA:110,3 +DA:111,3 +DA:113,3 +LF:23 +LH:23 end_of_record TN: SF:src/RNGRequestor.sol -FN:165,RNGRequestor.startRNGRequest -FN:187,RNGRequestor.completeRNGRequest -FN:199,RNGRequestor.cancelRNGRequest -FN:216,RNGRequestor.isRNGRequested -FN:224,RNGRequestor.isRNGCompleted -FN:232,RNGRequestor.isRNGTimedOut -FN:240,RNGRequestor.canStartRNGRequest -FN:248,RNGRequestor.canCompleteRNGRequest -FN:258,RNGRequestor.getRNGLockBlock -FN:267,RNGRequestor.getRNGRequestId -FN:275,RNGRequestor.getRNGTimeout -FN:283,RNGRequestor.getRNGService -FN:295,RNGRequestor.setRNGService -FN:305,RNGRequestor.setRNGTimeout -FN:315,RNGRequestor._afterRNGStart -FN:322,RNGRequestor._afterRNGComplete -FN:328,RNGRequestor._currentTime -FN:336,RNGRequestor._isRNGRequested -FN:344,RNGRequestor._isRNGCompleted -FN:352,RNGRequestor._isRNGTimedOut -FN:364,RNGRequestor._setRNGService -FN:374,RNGRequestor._setRNGTimeout -FNDA:16,RNGRequestor.startRNGRequest -FNDA:5,RNGRequestor.completeRNGRequest +FN:164,RNGRequestor.startRNGRequest +FN:186,RNGRequestor.completeRNGRequest +FN:198,RNGRequestor.cancelRNGRequest +FN:215,RNGRequestor.isRNGRequested +FN:223,RNGRequestor.isRNGCompleted +FN:231,RNGRequestor.isRNGTimedOut +FN:239,RNGRequestor.canStartRNGRequest +FN:247,RNGRequestor.canCompleteRNGRequest +FN:257,RNGRequestor.getRNGLockBlock +FN:266,RNGRequestor.getRNGRequestId +FN:274,RNGRequestor.getRNGTimeout +FN:282,RNGRequestor.getRNGService +FN:294,RNGRequestor.setRNGService +FN:304,RNGRequestor.setRNGTimeout +FN:314,RNGRequestor._afterRNGStart +FN:321,RNGRequestor._afterRNGComplete +FN:327,RNGRequestor._currentTime +FN:335,RNGRequestor._isRNGRequested +FN:343,RNGRequestor._isRNGCompleted +FN:351,RNGRequestor._isRNGTimedOut +FN:363,RNGRequestor._setRNGService +FN:373,RNGRequestor._setRNGTimeout +FNDA:17,RNGRequestor.startRNGRequest +FNDA:6,RNGRequestor.completeRNGRequest FNDA:2,RNGRequestor.cancelRNGRequest FNDA:2,RNGRequestor.isRNGRequested FNDA:2,RNGRequestor.isRNGCompleted @@ -53,125 +74,142 @@ FNDA:2,RNGRequestor.setRNGService FNDA:2,RNGRequestor.setRNGTimeout FNDA:11,RNGRequestor._afterRNGStart FNDA:1,RNGRequestor._afterRNGComplete -FNDA:18,RNGRequestor._currentTime -FNDA:27,RNGRequestor._isRNGRequested -FNDA:7,RNGRequestor._isRNGCompleted +FNDA:19,RNGRequestor._currentTime +FNDA:29,RNGRequestor._isRNGRequested +FNDA:8,RNGRequestor._isRNGCompleted FNDA:4,RNGRequestor._isRNGTimedOut FNDA:2,RNGRequestor._setRNGService FNDA:2,RNGRequestor._setRNGTimeout FNF:22 FNH:22 -DA:166,15 -DA:168,15 -DA:169,1 -DA:172,15 -DA:173,15 -DA:174,15 -DA:175,15 -DA:177,15 -DA:179,15 -DA:188,3 -DA:189,3 -DA:191,3 -DA:193,3 -DA:195,3 -DA:200,2 +DA:165,16 +DA:167,16 +DA:168,1 +DA:171,16 +DA:172,16 +DA:173,16 +DA:174,16 +DA:176,16 +DA:178,16 +DA:187,4 +DA:188,4 +DA:190,4 +DA:192,4 +DA:194,4 +DA:199,2 +DA:201,1 DA:202,1 -DA:203,1 -DA:205,1 -DA:207,1 -DA:217,2 -DA:225,2 -DA:233,2 -DA:241,2 -DA:249,2 -DA:259,3 -DA:268,3 -DA:276,3 -DA:284,3 -DA:296,2 -DA:306,2 -DA:329,18 -DA:337,27 -DA:345,7 -DA:353,4 -DA:354,1 -DA:356,3 -DA:365,2 +DA:204,1 +DA:206,1 +DA:216,2 +DA:224,2 +DA:232,2 +DA:240,2 +DA:248,2 +DA:258,3 +DA:267,3 +DA:275,3 +DA:283,3 +DA:295,2 +DA:305,2 +DA:328,19 +DA:336,29 +DA:344,8 +DA:352,4 +DA:353,1 +DA:355,3 +DA:364,2 +DA:365,1 DA:366,1 -DA:367,1 -DA:375,2 +DA:374,2 +DA:375,1 DA:376,1 -DA:377,1 LF:42 LH:42 end_of_record TN: SF:src/auctions/Auction.sol -FN:68,Auction.auctionDuration -FN:72,Auction.getPhase -FN:84,Auction._afterAuctionEnds -FN:88,Auction._getPhase -FN:94,Auction._setPhase -FNDA:1,Auction.auctionDuration -FNDA:0,Auction.getPhase -FNDA:0,Auction._afterAuctionEnds -FNDA:2,Auction._getPhase -FNDA:6,Auction._setPhase -FNF:5 -FNH:3 -DA:69,1 -DA:73,0 -DA:89,2 -DA:100,6 -DA:107,6 -DA:109,6 -DA:111,6 -LF:7 -LH:6 +FN:104,Auction.getPhase +FN:116,Auction._afterAuctionEnds +FN:124,Auction._getPhases +FN:133,Auction._getPhase +FN:147,Auction._setPhase +FN:87,Auction.auctionDuration +FN:95,Auction.getPhases +FNDA:1,Auction.getPhase +FNDA:1,Auction._afterAuctionEnds +FNDA:1,Auction._getPhases +FNDA:11,Auction._getPhase +FNDA:14,Auction._setPhase +FNDA:2,Auction.auctionDuration +FNDA:1,Auction.getPhases +FNF:7 +FNH:7 +DA:88,2 +DA:96,1 +DA:105,1 +DA:125,1 +DA:134,11 +DA:153,14 +DA:160,14 +DA:162,14 +DA:164,14 +LF:9 +LH:9 end_of_record TN: SF:src/auctions/TwoStepsAuction.sol FN:37,TwoStepsAuction._afterRNGStart -FN:46,TwoStepsAuction._afterRNGComplete -FNDA:4,TwoStepsAuction._afterRNGStart -FNDA:2,TwoStepsAuction._afterRNGComplete +FN:47,TwoStepsAuction._afterRNGComplete +FNDA:6,TwoStepsAuction._afterRNGStart +FNDA:4,TwoStepsAuction._afterRNGComplete FNF:2 FNH:2 -DA:38,4 -DA:47,2 -DA:48,2 -LF:3 -LH:3 +DA:38,6 +DA:39,6 +DA:48,4 +DA:49,4 +DA:51,4 +LF:5 +LH:5 end_of_record TN: SF:src/libraries/RewardLib.sol -FN:20,RewardLib.getReward -FNDA:11,RewardLib.getReward -FNF:1 -FNH:1 -DA:27,11 -DA:29,11 -DA:30,11 -DA:31,11 -DA:32,11 -DA:35,11 -DA:36,4 -DA:39,7 -DA:40,7 -DA:42,7 -DA:47,4 -DA:48,4 -DA:49,4 -DA:53,3 -DA:57,3 -DA:58,1 -DA:66,2 -DA:72,6 -DA:76,6 -DA:77,2 -DA:80,4 -DA:82,4 -LF:22 -LH:22 +FN:24,RewardLib.rewards +FN:55,RewardLib.reward +FN:83,RewardLib._reward +FNDA:3,RewardLib.rewards +FNDA:22,RewardLib.reward +FNDA:28,RewardLib._reward +FNF:3 +FNH:3 +DA:29,3 +DA:30,3 +DA:32,3 +DA:34,3 +DA:35,3 +DA:37,3 +DA:38,6 +DA:41,3 +DA:60,22 +DA:61,22 +DA:63,22 +DA:65,22 +DA:91,28 +DA:92,5 +DA:95,23 +DA:96,23 +DA:100,23 +DA:101,14 +DA:106,23 +DA:107,2 +DA:111,21 +DA:112,4 +DA:118,21 +DA:119,3 +DA:122,21 +DA:123,21 +DA:125,21 +LF:27 +LH:27 end_of_record diff --git a/src/DrawAuction.sol b/src/DrawAuction.sol index 8a57e54..e21de71 100644 --- a/src/DrawAuction.sol +++ b/src/DrawAuction.sol @@ -2,9 +2,8 @@ pragma solidity 0.8.17; import { PrizePool } from "v5-prize-pool/PrizePool.sol"; -import { console2 } from "forge-std/Test.sol"; -import { Auction } from "src/auctions/Auction.sol"; +import { Auction, AuctionLib } from "src/auctions/Auction.sol"; import { TwoStepsAuction, RNGInterface } from "src/auctions/TwoStepsAuction.sol"; import { RewardLib } from "src/libraries/RewardLib.sol"; @@ -19,34 +18,13 @@ contract DrawAuction is TwoStepsAuction { /* ============ Variables ============ */ /// @notice Instance of the PrizePool to compute Draw for. - PrizePool internal _prizePool; + PrizePool internal immutable _prizePool; /* ============ Custom Errors ============ */ /// @notice Thrown when the PrizePool address passed to the constructor is zero address. error PrizePoolNotZeroAddress(); - /* ============ Events ============ */ - - /** - * @notice Emitted when a Draw auction phase has completed. - * @param phaseId Id of the phase - * @param caller Address of the caller - */ - event DrawAuctionPhaseCompleted(uint256 indexed phaseId, address indexed caller); - - /** - * @notice Emitted when a Draw auction has completed and rewards have been distributed. - * @param phaseIds Ids of the phases that were rewarded - * @param rewardRecipients Addresses of the rewards recipients per phase id - * @param rewardAmounts Amounts of rewards distributed per phase id - */ - event DrawAuctionRewardsDistributed( - uint8[] phaseIds, - address[] rewardRecipients, - uint256[] rewardAmounts - ); - /* ============ Constructor ============ */ /** @@ -63,13 +41,15 @@ contract DrawAuction is TwoStepsAuction { uint32 rngTimeout_, PrizePool prizePool_, uint8 _auctionPhases, - uint256 auctionDuration_, + uint32 auctionDuration_, address _owner ) TwoStepsAuction(rng_, rngTimeout_, _auctionPhases, auctionDuration_, _owner) { if (address(prizePool_) == address(0)) revert PrizePoolNotZeroAddress(); _prizePool = prizePool_; } + /* ============ External Functions ============ */ + /* ============ Getter Functions ============ */ /** @@ -81,45 +61,49 @@ contract DrawAuction is TwoStepsAuction { } /** - * @notice Current reward for completing the Auction phase. - * @param _phaseId ID of the phase to get reward for (i.e. 0 for `startRNGRequest` or 1 for `completeRNGRequest`) + * @notice Reward for completing the Auction phase. + * @param _phase Phase to get reward for * @return Reward amount */ - function reward(uint8 _phaseId) external view returns (uint256) { - return RewardLib.getReward(_phases, _phaseId, _prizePool, _auctionDuration); + function reward(AuctionLib.Phase calldata _phase) external view returns (uint256) { + return RewardLib.reward(_phase, _prizePool, _auctionDuration); } + /* ============ Internal Functions ============ */ + + /* ============ Hooks ============ */ + /** - * @notice Hook called after the RNG request has completed. + * @notice Hook called after the auction has ended. * @param _randomNumber The random number that was generated */ function _afterAuctionEnds(uint256 _randomNumber) internal override { - // Phase memory _startRNGPhase = _getPhase(0); - // Phase memory _completeRNGPhase = _setPhase(1, _startRNGPhase.startTime, uint64(block.timestamp), _rewardRecipient); - // uint256 _startRNGRewardAmount = _reward(_startRNGPhase); - // console2.log("_startRNGRewardAmount", _startRNGRewardAmount); - // uint256 _completeRNGRewardAmount = _reward(_completeRNGPhase); - // _prizePool.completeAndStartNextDraw(_randomNumber); - // if (_startRNGPhase.recipient == _completeRNGPhase.recipient) { - // _prizePool.withdrawReserve(_startRNGPhase.recipient, uint104(_startRNGRewardAmount + _completeRNGRewardAmount)); - // } else { - // _prizePool.withdrawReserve(_startRNGPhase.recipient, uint104(_startRNGRewardAmount)); - // _prizePool.withdrawReserve(_completeRNGPhase.recipient, uint104(_completeRNGRewardAmount)); - // } - // uint8[] memory _phaseIds = new uint8[](2); - // _phaseIds[0] = _startRNGPhase.id; - // _phaseIds[1] = _completeRNGPhase.id; - // address[] memory _rewardRecipients = new address[](2); - // _rewardRecipients[0] = _startRNGPhase.recipient; - // _rewardRecipients[1] = _completeRNGPhase.recipient; - // uint256[] memory _rewardAmounts = new uint256[](2); - // _rewardAmounts[0] = _startRNGRewardAmount; - // _rewardAmounts[1] = _completeRNGRewardAmount; - // emit DrawAuctionPhaseCompleted(1, msg.sender); - // emit DrawAuctionRewardsDistributed( - // _phaseIds, - // _rewardRecipients, - // _rewardAmounts - // ); + AuctionLib.Phase memory _startRNGPhase = _getPhase(0); + AuctionLib.Phase memory _completeRNGPhase = _getPhase(1); + + AuctionLib.Phase[] memory _auctionPhases = new AuctionLib.Phase[](2); + _auctionPhases[0] = _startRNGPhase; + _auctionPhases[1] = _completeRNGPhase; + + uint256[] memory _rewards = RewardLib.rewards(_auctionPhases, _prizePool, _auctionDuration); + + _prizePool.completeAndStartNextDraw(_randomNumber); + + if (_startRNGPhase.recipient == _completeRNGPhase.recipient) { + _prizePool.withdrawReserve(_startRNGPhase.recipient, uint104(_rewards[0] + _rewards[1])); + } else { + _prizePool.withdrawReserve(_startRNGPhase.recipient, uint104(_rewards[0])); + _prizePool.withdrawReserve(_completeRNGPhase.recipient, uint104(_rewards[1])); + } + + uint8[] memory _phaseIds = new uint8[](2); + _phaseIds[0] = _startRNGPhase.id; + _phaseIds[1] = _completeRNGPhase.id; + + address[] memory _rewardRecipients = new address[](2); + _rewardRecipients[0] = _startRNGPhase.recipient; + _rewardRecipients[1] = _completeRNGPhase.recipient; + + emit AuctionRewardsDistributed(_phaseIds, _rewardRecipients, _rewards); } } diff --git a/src/RNGRequestor.sol b/src/RNGRequestor.sol index bdb5ee0..f176a1d 100644 --- a/src/RNGRequestor.sol +++ b/src/RNGRequestor.sol @@ -5,7 +5,6 @@ import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; import { Ownable } from "owner-manager/Ownable.sol"; import { RNGInterface } from "rng/RNGInterface.sol"; -import { PrizePool } from "v5-prize-pool/PrizePool.sol"; /** * @title PoolTogether V5 RNGRequestor diff --git a/src/auctions/Auction.sol b/src/auctions/Auction.sol index 00b6ec4..289c203 100644 --- a/src/auctions/Auction.sol +++ b/src/auctions/Auction.sol @@ -6,12 +6,12 @@ import { AuctionLib } from "src/libraries/AuctionLib.sol"; contract Auction { /* ============ Variables ============ */ - /// @notice Duration of the auction in seconds. - uint256 internal _auctionDuration; - /// @notice Array storing phases per id in ascending order. AuctionLib.Phase[] internal _phases; + /// @notice Duration of the auction in seconds. + uint32 internal immutable _auctionDuration; + /* ============ Custom Errors ============ */ /// @notice Thrown when the auction duration passed to the constructor is zero. @@ -29,13 +29,32 @@ contract Auction { @param endTime End time of the phase @param recipient Recipient of the phase reward */ - event PhaseSet( + event AuctionPhaseSet( uint8 indexed phaseId, uint64 startTime, uint64 endTime, address indexed recipient ); + /** + * @notice Emitted when an auction phase has completed. + * @param phaseId Id of the phase + * @param caller Address of the caller + */ + event AuctionPhaseCompleted(uint256 indexed phaseId, address indexed caller); + + /** + * @notice Emitted when an auction has completed and rewards have been distributed. + * @param phaseIds Ids of the phases + * @param rewardRecipients Addresses of the rewards recipients per phase id + * @param rewardAmounts Amounts of rewards distributed per phase id + */ + event AuctionRewardsDistributed( + uint8[] phaseIds, + address[] rewardRecipients, + uint256[] rewardAmounts + ); + /* ============ Constructor ============ */ /** @@ -43,7 +62,7 @@ contract Auction { * @param _auctionPhases Number of auction phases * @param auctionDuration_ Duration of the auction in seconds */ - constructor(uint8 _auctionPhases, uint256 auctionDuration_) { + constructor(uint8 _auctionPhases, uint32 auctionDuration_) { if (_auctionPhases == 0) revert AuctionPhasesNotZero(); if (auctionDuration_ == 0) revert AuctionDurationNotZero(); @@ -65,10 +84,23 @@ contract Auction { * @dev This is the time it takes for the auction to reach the PrizePool full reserve amount. * @return Duration of the auction in seconds */ - function auctionDuration() external view returns (uint256) { + function auctionDuration() external view returns (uint64) { return _auctionDuration; } + /** + * @notice Get phases. + * @return Phases + */ + function getPhases() external view returns (AuctionLib.Phase[] memory) { + return _getPhases(); + } + + /** + * @notice Get phase by id. + * @param _phaseId Id of the phase + * @return Phase + */ function getPhase(uint256 _phaseId) external view returns (AuctionLib.Phase memory) { return _getPhase(_phaseId); } @@ -85,12 +117,33 @@ contract Auction { /* ============ Getters ============ */ + /** + * @notice Get phases. + * @return Phases + */ + function _getPhases() internal view returns (AuctionLib.Phase[] memory) { + return _phases; + } + + /** + * @notice Get phase by id. + * @param _phaseId Id of the phase + * @return Phase + */ function _getPhase(uint256 _phaseId) internal view returns (AuctionLib.Phase memory) { return _phases[_phaseId]; } /* ============ Setters ============ */ + /** + * @notice Set phase. + * @param _phaseId Id of the phase + * @param _startTime Start time of the phase + * @param _endTime End time of the phase + * @param _recipient Recipient of the phase reward + * @return Phase + */ function _setPhase( uint8 _phaseId, uint64 _startTime, @@ -106,7 +159,7 @@ contract Auction { _phases[_phaseId] = _phase; - emit PhaseSet(_phaseId, _startTime, _endTime, _recipient); + emit AuctionPhaseSet(_phaseId, _startTime, _endTime, _recipient); return _phase; } diff --git a/src/auctions/TwoStepsAuction.sol b/src/auctions/TwoStepsAuction.sol index 445bc3d..f234336 100644 --- a/src/auctions/TwoStepsAuction.sol +++ b/src/auctions/TwoStepsAuction.sol @@ -19,7 +19,7 @@ contract TwoStepsAuction is Auction, RNGRequestor { RNGInterface rng_, uint32 rngTimeout_, uint8 _auctionPhases, - uint256 auctionDuration_, + uint32 auctionDuration_, address _owner ) Auction(_auctionPhases, auctionDuration_) RNGRequestor(rng_, rngTimeout_, _owner) {} @@ -36,6 +36,7 @@ contract TwoStepsAuction is Auction, RNGRequestor { */ function _afterRNGStart(address _rewardRecipient) internal override { _setPhase(0, 0, uint64(block.timestamp), _rewardRecipient); + emit AuctionPhaseCompleted(0, msg.sender); } /** @@ -45,6 +46,8 @@ contract TwoStepsAuction is Auction, RNGRequestor { */ function _afterRNGComplete(uint256 _randomNumber, address _rewardRecipient) internal override { _setPhase(1, _getPhase(0).endTime, uint64(block.timestamp), _rewardRecipient); + emit AuctionPhaseCompleted(1, msg.sender); + _afterAuctionEnds(_randomNumber); } } diff --git a/src/libraries/RewardLib.sol b/src/libraries/RewardLib.sol index 153aa8b..f902fb7 100644 --- a/src/libraries/RewardLib.sol +++ b/src/libraries/RewardLib.sol @@ -5,80 +5,115 @@ import { PrizePool } from "v5-prize-pool/PrizePool.sol"; import { AuctionLib } from "src/libraries/AuctionLib.sol"; -import { console2 } from "forge-std/console2.sol"; - library RewardLib { + /* ============ Internal Functions ============ */ + /** - * @notice Current reward for completing the Auction phase. + * @notice Reward for completing the Auction. * @dev The reward amount is computed via linear interpolation starting from 0 * and increasing as the auction goes on to the full reserve amount. - * @param _phaseId ID of the phase to get reward for + * @dev Only computes reward for the recorded phases passed to the function. + * To compute the current reward for a specific phase, use the `reward` function. + * @param _phases Phases to get reward for * @param _prizePool Address of the Prize Pool to get auction reward for * @param _auctionDuration Duration of the auction in seconds - * @return Reward amount + * @return Rewards ordered by phase ID */ - function getReward( + function rewards( AuctionLib.Phase[] memory _phases, - uint8 _phaseId, PrizePool _prizePool, - uint256 _auctionDuration + uint32 _auctionDuration + ) internal view returns (uint256[] memory) { + uint64 _auctionStart = _prizePool.nextDrawEndsAt(); + uint64 _auctionEnd = _auctionStart + _auctionDuration; + + uint256 _reserve = _prizePool.reserve() + _prizePool.reserveForNextDraw(); + + uint256 _phasesLength = _phases.length; + uint256[] memory _rewards = new uint256[](_phasesLength); + + for (uint256 i; i < _phasesLength; i++) { + _rewards[i] = _reward(_phases[i], _reserve, _auctionStart, _auctionEnd, _auctionDuration); + } + + return _rewards; + } + + /** + * @notice Reward for completing the Auction phase. + * @dev The reward amount is computed via linear interpolation starting from 0 + * and increasing as the auction goes on to the full reserve amount. + * @dev This implementation assumes that phases are run sequentially, i.e. timestamps should not overlap. + * This is to avoid overdistributing the reserve. + * @param _phase Phase to get reward for + * @param _prizePool Address of the Prize Pool to get auction reward for + * @param _auctionDuration Duration of the auction in seconds + * @return Reward amount + */ + function reward( + AuctionLib.Phase memory _phase, + PrizePool _prizePool, + uint32 _auctionDuration ) internal view returns (uint256) { - // TODO: which value would nextDrawEndsAt return if the draw has been awarded? - uint64 _nextDrawEndsAt = _prizePool.nextDrawEndsAt(); + uint64 _auctionStart = _prizePool.nextDrawEndsAt(); + uint64 _auctionEnd = _auctionStart + _auctionDuration; + + uint256 _reserve = _prizePool.reserve() + _prizePool.reserveForNextDraw(); - console2.log("_reward block.timestamp", block.timestamp); - console2.log("_reward _nextDrawEndsAt", _nextDrawEndsAt); - console2.log("_reward _prizePool.nextDrawStartsAt()", _prizePool.nextDrawStartsAt()); - console2.log("_reward periodDiff", _nextDrawEndsAt - _prizePool.nextDrawStartsAt()); + return _reward(_phase, _reserve, _auctionStart, _auctionEnd, _auctionDuration); + } + + /* ============ Private Functions ============ */ - // If the Draw has not ended yet, we return 0 - if (block.timestamp <= _nextDrawEndsAt) { + /** + * @notice Reward for completing the Auction phase. + * @dev The reward amount is computed via linear interpolation starting from 0 + * and increasing as the auction goes on to the full reserve amount. + * @dev This implementation assumes that phases are run sequentially, i.e. timestamps should not overlap. + * This is to avoid overdistributing the reserve. + * @param _phase Phase to get reward for + * @param _reserve Reserve amount + * @param _auctionStart Auction start time + * @param _auctionEnd Auction end time + * @param _auctionDuration Duration of the auction in seconds + * @return Reward amount + */ + function _reward( + AuctionLib.Phase memory _phase, + uint256 _reserve, + uint64 _auctionStart, + uint64 _auctionEnd, + uint32 _auctionDuration + ) private view returns (uint256) { + // If the auction has not started yet, we return 0 + if (block.timestamp <= _auctionStart) { return 0; } - AuctionLib.Phase memory _phase = _phases[_phaseId]; - uint256 _elapsedTime; - - if (_phase.id == 0) { - // Elapsed time between the timestamp at which the Draw ended and the first phase end time - // End time will be block.timestamp if this phase has not been triggered yet - // Is unchecked since block.timestamp can't be lower than nextDrawEndsAt - unchecked { - _elapsedTime = (_phase.endTime != 0 ? _phase.endTime : block.timestamp) - _nextDrawEndsAt; - console2.log("_reward _phase.startTime", _phase.startTime); - console2.log("_reward _elapsedTime", _elapsedTime); - } - } else { - // Retrieve the previous phase - AuctionLib.Phase memory _previousPhase = _phases[_phase.id - 1]; - - // If the previous phase has not been triggered, - // we return 0 cause we can't compute reward for this phase - if (_previousPhase.endTime == 0) { - return 0; - } - - // Elapsed time between this phase startTime and endTime - // If startTime is different than 0, it means that this phase has been recorded and endTime is also set - // End time will be block.timestamp if this phase has not been triggered yet - // Is unchecked since block.timestamp can't be lower than previousPhase.endTime - unchecked { - _elapsedTime = _phase.startTime != 0 - ? _phase.endTime - _phase.startTime - : block.timestamp - _previousPhase.endTime; - } + // Since the Auction contract is not aware of the PrizePool contract, + // the first phase start time is set to 0, so we need to set it here instead + if (_phase.id == 0 && _phase.startTime == 0) { + _phase.startTime = _auctionStart; } - uint256 _reserve = _prizePool.reserve() + _prizePool.reserveForNextDraw(); + // If the phase was started before the start of the auction + // or after the end of the auction, no reward should be distributed + if (_phase.startTime < _auctionStart || _phase.startTime > _auctionEnd) { + return 0; + } - // We can't award more than the available reserve - // TODO: look at _nextDrawEndsAt in the PrizePool to figure out if the auction duration could be extended - if (_elapsedTime >= _auctionDuration) { - return _reserve; + // If the phase end time has not been set, we use the current time + if (_phase.endTime == 0) { + _phase.endTime = uint64(block.timestamp); } - console2.log("_elapsedTime", _elapsedTime); + // If the phase was started before the end of the auction, + // but completed after, the end time should be the auction end time + // to avoid overdistributing the reserve + if (_phase.endTime > _auctionEnd) { + _phase.endTime = _auctionEnd; + } - return (_elapsedTime * _reserve) / _auctionDuration; + return ((_phase.endTime - _phase.startTime) * _reserve) / _auctionDuration; } } diff --git a/test/DrawAuction.t.sol b/test/DrawAuction.t.sol index 811e0ed..d293978 100644 --- a/test/DrawAuction.t.sol +++ b/test/DrawAuction.t.sol @@ -1,18 +1,16 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.17; -import "forge-std/Test.sol"; -import { console2 } from "forge-std/console2.sol"; import { ERC20Mock } from "openzeppelin/mocks/ERC20Mock.sol"; import { UD2x18, SD1x18, ConstructorParams, PrizePool, TieredLiquidityDistributor, TwabController } from "v5-prize-pool/PrizePool.sol"; import { DrawAuction } from "src/DrawAuction.sol"; -import { Helpers, RNGInterface } from "./helpers/Helpers.t.sol"; +import { AuctionLib } from "src/libraries/AuctionLib.sol"; +import { Helpers, RNGInterface } from "test/helpers/Helpers.t.sol"; contract DrawAuctionTest is Helpers { /* ============ Events ============ */ - event DrawAuctionCompleted(address indexed caller, uint256 rewardAmount); /* ============ Variables ============ */ @@ -31,7 +29,6 @@ contract DrawAuctionTest is Helpers { vm.warp(0); prizeToken = new ERC20Mock(); - console2.log("drawPeriodSeconds", drawPeriodSeconds); prizePool = new PrizePool( ConstructorParams({ @@ -69,33 +66,24 @@ contract DrawAuctionTest is Helpers { /* ============ State Functions ============ */ - /* ============ reward ============ */ - - function testRewardBeforeDrawEnds() public { - assertEq(drawAuction.reward(0), 0); - assertEq(drawAuction.reward(1), 0); - } - - function testRewardAtDrawEnds() public { - vm.warp(drawPeriodSeconds); - - assertEq(drawAuction.reward(0), 0); - assertEq(drawAuction.reward(1), 0); - } - + /* ============ Reward ============ */ /* ============ Half Time ============ */ /* ============ Phase 0 ============ */ function testPhase0RewardAtHalfTime() public { - vm.warp(drawPeriodSeconds + (auctionDuration / 2)); + uint64 _warpTimestamp = drawPeriodSeconds + (auctionDuration / 2); + vm.warp(_warpTimestamp); uint256 _reserveAmount = 200e18; _mockReserves(address(prizePool), _reserveAmount); - assertEq(drawAuction.reward(0), _reserveAmount / 2); + AuctionLib.Phase memory _phase = _getPhase(0, uint64(0), _warpTimestamp, address(this)); + + assertEq(drawAuction.reward(_phase), _reserveAmount / 2); } function testPhase0TriggeredRewardAtHalfTime() public { - vm.warp(drawPeriodSeconds + (auctionDuration / 2)); + uint64 _warpTimestamp = drawPeriodSeconds + (auctionDuration / 2); + vm.warp(_warpTimestamp); uint256 _reserveAmount = 200e18; _mockReserves(address(prizePool), _reserveAmount); @@ -105,18 +93,23 @@ contract DrawAuctionTest is Helpers { vm.warp(drawPeriodSeconds + (auctionDuration)); - assertEq(drawAuction.reward(0), _reserveAmount / 2); + AuctionLib.Phase memory _phase = _getPhase(0, uint64(0), _warpTimestamp, address(this)); + + assertEq(drawAuction.reward(_phase), _reserveAmount / 2); } /* ============ Phase 1 ============ */ function testPhase1RewardAtHalfTimePhase0NotTriggered() public { - vm.warp(drawPeriodSeconds + (auctionDuration / 2)); - assertEq(drawAuction.reward(1), 0); + uint64 _warpTimestamp = drawPeriodSeconds + (auctionDuration / 2); + vm.warp(_warpTimestamp); + + AuctionLib.Phase memory _phase = _getPhase(0, uint64(0), _warpTimestamp, address(this)); + + assertEq(drawAuction.reward(_phase), 0); } function testPhase1RewardAtHalfTimePhase0Triggered() public { - uint256 _startRNGRequestTime = drawPeriodSeconds + (auctionDuration / 4); // drawPeriodSeconds + 45 minutes - + uint64 _startRNGRequestTime = drawPeriodSeconds + (auctionDuration / 4); // drawPeriodSeconds + 45 minutes vm.warp(_startRNGRequestTime); _mockStartRNGRequest(address(rng), address(0), 0, uint32(1), uint32(block.number)); @@ -127,16 +120,27 @@ contract DrawAuctionTest is Helpers { uint256 _reserveAmount = 200e18; _mockReserves(address(prizePool), _reserveAmount); + AuctionLib.Phase memory _phase = _getPhase(0, uint64(0), _startRNGRequestTime, address(this)); + assertEq( - drawAuction.reward(1), - _computeReward(block.timestamp - _startRNGRequestTime, _reserveAmount, auctionDuration) + drawAuction.reward(_phase), + _computeReward( + uint64(block.timestamp - _startRNGRequestTime), + _reserveAmount, + auctionDuration + ) ); } function testPhase1TriggeredRewardAtHalfTime() public { uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - uint256 _startRNGRequestTime = drawPeriodSeconds + (auctionDuration / 4); // drawPeriodSeconds + 45 minutes + uint64 _startRNGRequestTime = drawPeriodSeconds + (auctionDuration / 4); // drawPeriodSeconds + 45 minutes + + uint256 _reserveAmount = 200e18; + + prizeToken.mint(address(prizePool), _reserveAmount * 125); + prizePool.contributePrizeTokens(address(2), _reserveAmount * 125); vm.warp(_startRNGRequestTime); @@ -145,65 +149,104 @@ contract DrawAuctionTest is Helpers { vm.warp(drawPeriodSeconds + (auctionDuration / 2)); - uint256 _reserveAmount = 200e18; _mockReserves(address(prizePool), _reserveAmount); + uint64 _phaseEndTime = uint64(block.timestamp); + + AuctionLib.Phase memory _phase = _getPhase( + 1, + _startRNGRequestTime, + _phaseEndTime, + address(this) + ); + assertEq( - drawAuction.reward(1), - _computeReward(block.timestamp - _startRNGRequestTime, _reserveAmount, auctionDuration) + drawAuction.reward(_phase), + _computeReward(_phaseEndTime - _startRNGRequestTime, _reserveAmount, auctionDuration) ); _mockCompleteRNGRequest(address(rng), _requestId, randomNumber); drawAuction.completeRNGRequest(recipient); + + assertEq(prizeToken.balanceOf(recipient), _reserveAmount / 2); } - /* ============ At or Aftter auction ends ============ */ + /* ============ At or After auction ends ============ */ function testRewardAtAuctionEnd() public { - vm.warp(drawPeriodSeconds + auctionDuration); + uint64 _warpTimestamp = drawPeriodSeconds + auctionDuration; + vm.warp(_warpTimestamp); uint256 _reserveAmount = 200e18; _mockReserves(address(prizePool), _reserveAmount); - assertEq(drawAuction.reward(0), 200e18); + AuctionLib.Phase memory _phase = _getPhase(0, uint64(0), _warpTimestamp, address(this)); + + assertEq(drawAuction.reward(_phase), 200e18); } function testRewardAfterAuctionEnd() public { + uint64 _warpTimestamp = drawPeriodSeconds + drawPeriodSeconds / 2; vm.warp(drawPeriodSeconds + drawPeriodSeconds / 2); uint256 _reserveAmount = 200e18; _mockReserves(address(prizePool), _reserveAmount); - assertEq(drawAuction.reward(0), _reserveAmount); + AuctionLib.Phase memory _phase = _getPhase(0, uint64(0), _warpTimestamp, address(this)); + + assertEq(drawAuction.reward(_phase), _reserveAmount); } /* ============ _afterRNGComplete ============ */ function testAfterRNGComplete() public { - console2.log("accountedBalance before", prizePool.accountedBalance()); uint256 _reserveAmount = 200e18; - prizeToken.mint(address(prizePool), _reserveAmount * 2); - prizePool.contributePrizeTokens(address(2), _reserveAmount * 2); - - console2.log("accountedBalance after", prizePool.accountedBalance()); + prizeToken.mint(address(prizePool), _reserveAmount * 110); + prizePool.contributePrizeTokens(address(2), _reserveAmount * 110); - vm.warp(drawPeriodSeconds + auctionDuration); + vm.warp(drawPeriodSeconds + auctionDuration / 2); uint32 _requestId = uint32(1); uint32 _lockBlock = uint32(block.number); - // uint256 _rewardAmount = drawAuction.reward(0); _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); drawAuction.startRNGRequest(recipient); + vm.warp(drawPeriodSeconds + auctionDuration); + _mockCompleteRNGRequest(address(rng), _requestId, randomNumber); - // vm.expectEmit(); - // emit DrawAuctionCompleted(address(this), _rewardAmount); + drawAuction.completeRNGRequest(recipient); + + assertEq(prizeToken.balanceOf(recipient), _reserveAmount / 2); + } + + function testAfterRNGCompleteDifferentRecipient() public { + uint256 _reserveAmount = 200e18; + + prizeToken.mint(address(prizePool), _reserveAmount * 110); + prizePool.contributePrizeTokens(address(2), _reserveAmount * 110); + + vm.warp(drawPeriodSeconds + auctionDuration / 2); + + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); + + address _secondRecipient = address(3); + drawAuction.startRNGRequest(_secondRecipient); + + vm.warp(drawPeriodSeconds + auctionDuration); + + _mockCompleteRNGRequest(address(rng), _requestId, randomNumber); drawAuction.completeRNGRequest(recipient); + + assertEq(prizeToken.balanceOf(recipient), _reserveAmount / 4); + assertEq(prizeToken.balanceOf(_secondRecipient), _reserveAmount / 4); } } diff --git a/test/auctions/Auctions.t.sol b/test/auctions/Auctions.t.sol new file mode 100644 index 0000000..92201fa --- /dev/null +++ b/test/auctions/Auctions.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; + +import { AuctionHarness, AuctionLib } from "test/harness/AuctionHarness.sol"; + +contract AuctionTest is Test { + /* ============ Events ============ */ + event AuctionPhaseSet( + uint8 indexed phaseId, + uint64 startTime, + uint64 endTime, + address indexed recipient + ); + + /* ============ Variables ============ */ + AuctionHarness public auction; + + uint8 public auctionPhases = 2; + uint32 public auctionDuration = 3 hours; + + /* ============ SetUp ============ */ + function setUp() public { + auction = new AuctionHarness(auctionPhases, auctionDuration); + } + + /* ============ Getter Functions ============ */ + + function testAuctionDuration() public { + assertEq(auction.auctionDuration(), auctionDuration); + } + + function testGetPhases() public { + uint8 _firstPhaseId = 0; + uint64 _startTime = uint64(block.timestamp); + address _recipient = address(this); + + vm.warp(auctionDuration / 2); + uint64 _endTime = uint64(block.timestamp); + + auction.setPhase(_firstPhaseId, _startTime, _endTime, _recipient); + + vm.warp(auctionDuration); + uint8 _secondPhaseId = 1; + uint64 _secondPhaseEndTime = uint64(block.timestamp); + + auction.setPhase(_secondPhaseId, _endTime, _secondPhaseEndTime, _recipient); + + AuctionLib.Phase[] memory _phases = auction.getPhases(); + AuctionLib.Phase memory _firstPhase = _phases[0]; + + assertEq(_firstPhase.id, _firstPhaseId); + assertEq(_firstPhase.startTime, _startTime); + assertEq(_firstPhase.endTime, _endTime); + assertEq(_firstPhase.recipient, _recipient); + + AuctionLib.Phase memory _secondPhase = _phases[1]; + + assertEq(_secondPhase.id, _secondPhaseId); + assertEq(_secondPhase.startTime, _endTime); + assertEq(_secondPhase.endTime, _secondPhaseEndTime); + assertEq(_secondPhase.recipient, _recipient); + } + + function testGetPhase() public { + uint8 _phaseId = 0; + uint64 _startTime = uint64(block.timestamp); + address _recipient = address(this); + + vm.warp(auctionDuration / 2); + uint64 _endTime = uint64(block.timestamp); + + auction.setPhase(_phaseId, _startTime, _endTime, _recipient); + + AuctionLib.Phase memory _phase = auction.getPhase(_phaseId); + + assertEq(_phase.id, _phaseId); + assertEq(_phase.startTime, _startTime); + assertEq(_phase.endTime, _endTime); + assertEq(_phase.recipient, _recipient); + } + + /* ============ Setter Functions ============ */ + + function testSetPhase() public { + uint8 _phaseId = 0; + uint64 _startTime = uint64(block.timestamp); + address _recipient = address(this); + + vm.warp(auctionDuration / 2); + uint64 _endTime = uint64(block.timestamp); + + vm.expectEmit(); + emit AuctionPhaseSet(_phaseId, _startTime, _endTime, _recipient); + + AuctionLib.Phase memory _phase = auction.setPhase(_phaseId, _startTime, _endTime, _recipient); + + assertEq(_phase.id, _phaseId); + assertEq(_phase.startTime, _startTime); + assertEq(_phase.endTime, _endTime); + assertEq(_phase.recipient, _recipient); + } +} diff --git a/test/auctions/TwoStepsAuction.t.sol b/test/auctions/TwoStepsAuction.t.sol new file mode 100644 index 0000000..2e70255 --- /dev/null +++ b/test/auctions/TwoStepsAuction.t.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; + +import { TwoStepsAuctionHarness, RNGInterface } from "test/harness/TwoStepsAuctionHarness.sol"; + +contract TwoStepsAuctionTest is Test { + /* ============ Events ============ */ + event AuctionPhaseCompleted(uint256 indexed phaseId, address indexed caller); + + /* ============ Variables ============ */ + TwoStepsAuctionHarness public auction; + RNGInterface public rng; + + uint32 public rngTimeout = 1 hours; + uint8 public auctionPhases = 2; + uint32 public auctionDuration = 3 hours; + + /* ============ SetUp ============ */ + function setUp() public { + rng = RNGInterface(address(1)); + auction = new TwoStepsAuctionHarness( + rng, + rngTimeout, + auctionPhases, + auctionDuration, + address(this) + ); + } + + /* ============ Hooks ============ */ + + function testAfterRNGStart() public { + vm.expectEmit(); + emit AuctionPhaseCompleted(0, address(this)); + + auction.afterRNGStart(address(this)); + } + + function testAfterRNGComplete() public { + vm.expectEmit(); + emit AuctionPhaseCompleted(1, address(this)); + + auction.afterRNGComplete(123456789, address(this)); + } +} diff --git a/test/harness/AuctionHarness.sol b/test/harness/AuctionHarness.sol new file mode 100644 index 0000000..b929865 --- /dev/null +++ b/test/harness/AuctionHarness.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { Auction, AuctionLib } from "src/auctions/Auction.sol"; + +contract AuctionHarness is Auction { + constructor( + uint8 _auctionPhases, + uint32 auctionDuration_ + ) Auction(_auctionPhases, auctionDuration_) {} + + function setPhase( + uint8 _phaseId, + uint64 _startTime, + uint64 _endTime, + address _recipient + ) external returns (AuctionLib.Phase memory) { + return _setPhase(_phaseId, _startTime, _endTime, _recipient); + } +} diff --git a/test/harness/RewardLibHarness.sol b/test/harness/RewardLibHarness.sol new file mode 100644 index 0000000..6800727 --- /dev/null +++ b/test/harness/RewardLibHarness.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { RewardLib, AuctionLib, PrizePool } from "src/libraries/RewardLib.sol"; + +contract RewardLibHarness { + PrizePool internal _prizePool; + uint32 internal _auctionDuration; + AuctionLib.Phase[] internal _phases; + + constructor(PrizePool prizePool_, uint8 _auctionPhases, uint32 auctionDuration_) { + _prizePool = prizePool_; + + for (uint8 i = 0; i < _auctionPhases; i++) { + _phases.push( + AuctionLib.Phase({ id: i, startTime: uint64(0), endTime: uint64(0), recipient: address(0) }) + ); + } + + _auctionDuration = auctionDuration_; + } + + function reward(uint8 _phaseId) external view returns (uint256) { + AuctionLib.Phase memory _phase = this.getPhase(_phaseId); + return RewardLib.reward(_phase, _prizePool, _auctionDuration); + } + + function setPhase( + uint8 _phaseId, + uint64 _startTime, + uint64 _endTime, + address _recipient + ) external returns (AuctionLib.Phase memory) { + AuctionLib.Phase memory _phase = AuctionLib.Phase({ + id: _phaseId, + startTime: _startTime, + endTime: _endTime, + recipient: _recipient + }); + + _phases[_phaseId] = _phase; + + return _phase; + } + + function getPhase(uint8 _phaseId) external view returns (AuctionLib.Phase memory) { + return _phases[_phaseId]; + } +} diff --git a/test/harness/TwoStepsAuctionHarness.sol b/test/harness/TwoStepsAuctionHarness.sol new file mode 100644 index 0000000..3035c3f --- /dev/null +++ b/test/harness/TwoStepsAuctionHarness.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { TwoStepsAuction, RNGInterface } from "src/auctions/TwoStepsAuction.sol"; + +contract TwoStepsAuctionHarness is TwoStepsAuction { + constructor( + RNGInterface rng_, + uint32 rngTimeout_, + uint8 _auctionPhases, + uint32 auctionDuration_, + address _owner + ) TwoStepsAuction(rng_, rngTimeout_, _auctionPhases, auctionDuration_, _owner) {} + + function afterRNGStart(address _rewardRecipient) external { + _afterRNGStart(_rewardRecipient); + } + + function afterRNGComplete(uint256 _randomNumber, address _rewardRecipient) external { + _afterRNGComplete(_randomNumber, _rewardRecipient); + } +} diff --git a/test/helpers/Helpers.t.sol b/test/helpers/Helpers.t.sol index 5419265..4628941 100644 --- a/test/helpers/Helpers.t.sol +++ b/test/helpers/Helpers.t.sol @@ -6,6 +6,8 @@ import "forge-std/Test.sol"; import { PrizePool, TieredLiquidityDistributor } from "v5-prize-pool/PrizePool.sol"; import { RNGInterface } from "rng/RNGInterface.sol"; +import { AuctionLib } from "src/libraries/AuctionLib.sol"; + contract Helpers is Test { /* ============ Mock Functions ============ */ @@ -93,10 +95,27 @@ contract Helpers is Test { /* ============ Computations ============ */ function _computeReward( - uint256 _elapsedTime, + uint64 _elapsedTime, uint256 _reserve, - uint256 _auctionDuration + uint32 _auctionDuration ) internal pure returns (uint256) { return (_elapsedTime * _reserve) / _auctionDuration; } + + /* ============ Getters ============ */ + + function _getPhase( + uint8 _phaseId, + uint64 _startTime, + uint64 _endTime, + address _recipient + ) internal pure returns (AuctionLib.Phase memory) { + return + AuctionLib.Phase({ + id: _phaseId, + startTime: _startTime, + endTime: _endTime, + recipient: _recipient + }); + } } diff --git a/test/libraries/RewardLib.t.sol b/test/libraries/RewardLib.t.sol new file mode 100644 index 0000000..ba4c5c5 --- /dev/null +++ b/test/libraries/RewardLib.t.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { ERC20Mock } from "openzeppelin/mocks/ERC20Mock.sol"; + +import { UD2x18, SD1x18, ConstructorParams, PrizePool, TieredLiquidityDistributor, TwabController } from "v5-prize-pool/PrizePool.sol"; + +import { Helpers, RNGInterface } from "test/helpers/Helpers.t.sol"; +import { RewardLibHarness } from "test/harness/RewardLibHarness.sol"; + +contract RewardLibTest is Helpers { + /* ============ Variables ============ */ + RewardLibHarness public rewardLib; + PrizePool public prizePool; + ERC20Mock public prizeToken; + + uint32 public auctionDuration = 3 hours; + uint32 public drawPeriodSeconds = 1 days; + address public recipient = address(this); + + /* ============ SetUp ============ */ + function setUp() public { + vm.warp(0); + + prizePool = new PrizePool( + ConstructorParams({ + prizeToken: prizeToken, + twabController: TwabController(address(0)), + drawManager: address(0), + grandPrizePeriodDraws: uint32(365), + drawPeriodSeconds: drawPeriodSeconds, + firstDrawStartsAt: uint64(block.timestamp), + numberOfTiers: uint8(3), // minimum number of tiers + tierShares: 100, + canaryShares: 10, + reserveShares: 10, + claimExpansionThreshold: UD2x18.wrap(0.9e18), // claim threshold of 90% + smoothing: SD1x18.wrap(0.9e18) // alpha + }) + ); + + rewardLib = new RewardLibHarness(prizePool, 2, auctionDuration); + } + + /* ============ Reward ============ */ + + /* ============ Before or at Draw ends (default state) ============ */ + function testRewardBeforeDrawEnds() public { + assertEq(block.timestamp, 0); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds); + + assertEq(rewardLib.reward(0), 0); + assertEq(rewardLib.reward(1), 0); + } + + function testRewardAtDrawEnds() public { + vm.warp(drawPeriodSeconds); + + assertEq(block.timestamp, drawPeriodSeconds); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds); + + assertEq(rewardLib.reward(0), 0); + assertEq(rewardLib.reward(1), 0); + } + + /* ============ Half Time ============ */ + /* ============ Phase 0 ============ */ + function testPhase0RewardAtHalfTime() public { + uint256 _warpTimestamp = drawPeriodSeconds + (auctionDuration / 2); + vm.warp(_warpTimestamp); + + assertEq(block.timestamp, _warpTimestamp); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds); + + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + assertEq(rewardLib.reward(0), _reserveAmount / 2); + } + + function testPhase0RewardSetAtHalfTime() public { + uint256 _warpTimestamp = drawPeriodSeconds + (auctionDuration / 2); + vm.warp(_warpTimestamp); + + assertEq(block.timestamp, _warpTimestamp); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds); + + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + uint8 _phaseId = 0; + rewardLib.setPhase(_phaseId, drawPeriodSeconds, uint64(_warpTimestamp), recipient); + + vm.warp(drawPeriodSeconds + auctionDuration); + + assertEq(rewardLib.reward(_phaseId), _reserveAmount / 2); + } + + /* ============ At or After auction ends ============ */ + /* ============ Phase 0 ============ */ + function testPhase0RewardAtAuctionEnd() public { + uint256 _warpTimestamp = drawPeriodSeconds + auctionDuration; + vm.warp(_warpTimestamp); + + assertEq(block.timestamp, _warpTimestamp); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds); + + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + assertEq(rewardLib.reward(0), _reserveAmount); + } + + function testPhase0RewardSetAtAuctionEnd() public { + uint256 _warpTimestamp = drawPeriodSeconds + auctionDuration; + vm.warp(_warpTimestamp); + + assertEq(block.timestamp, _warpTimestamp); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds); + + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + uint8 _phaseId = 0; + rewardLib.setPhase(_phaseId, drawPeriodSeconds, uint64(_warpTimestamp), recipient); + + vm.warp(drawPeriodSeconds + auctionDuration * 2); + + assertEq(rewardLib.reward(_phaseId), _reserveAmount); + } + + function testPhase0RewardAfterAuctionEnd() public { + uint256 _warpTimestamp = drawPeriodSeconds + auctionDuration * 2; + vm.warp(_warpTimestamp); + + assertEq(block.timestamp, _warpTimestamp); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds); + + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + assertEq(rewardLib.reward(0), _reserveAmount); + } + + function testPhase0RewardSetAfterAuctionEnd() public { + uint256 _warpTimestamp = drawPeriodSeconds + auctionDuration * 2; + vm.warp(_warpTimestamp); + + assertEq(block.timestamp, _warpTimestamp); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds); + + uint256 _reserveAmount = 200e18; + _mockReserves(address(prizePool), _reserveAmount); + + uint8 _phaseId = 0; + rewardLib.setPhase(_phaseId, drawPeriodSeconds, uint64(_warpTimestamp), recipient); + + vm.warp(drawPeriodSeconds + drawPeriodSeconds / 2); + + assertEq(rewardLib.reward(0), _reserveAmount); + } + + /* ============ At or After draw period (end of second draw) ============ */ + /* ============ Phase 0 ============ */ + function testPhase0RewardAtDrawPeriodEnd() public { + uint256 _warpTimestamp = drawPeriodSeconds * 2; + vm.warp(_warpTimestamp); + + assertEq(block.timestamp, _warpTimestamp); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds * 2); + + uint256 _reserveAmount = 400e18; + _mockReserves(address(prizePool), _reserveAmount); + + // Auction has restarted for new draw, so reward should be 0 + assertEq(rewardLib.reward(0), 0); + } + + function testPhase0RewardSetAtDrawPeriodEnd() public { + uint256 _warpTimestamp = drawPeriodSeconds * 2; + vm.warp(_warpTimestamp); + + assertEq(block.timestamp, _warpTimestamp); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds * 2); + + uint256 _reserveAmount = 400e18; + _mockReserves(address(prizePool), _reserveAmount); + + uint8 _phaseId = 0; + rewardLib.setPhase(_phaseId, drawPeriodSeconds, uint64(_warpTimestamp), recipient); + + vm.warp(drawPeriodSeconds * 2 + auctionDuration); + + // Recorded phase start time is before the beginning of the auction, so reward should be 0 + assertEq(rewardLib.reward(0), 0); + } + + function testPhase0RewardAfterDrawPeriodEnd() public { + uint256 _warpTimestamp = drawPeriodSeconds * 2 + (auctionDuration / 2); + vm.warp(_warpTimestamp); + + assertEq(block.timestamp, _warpTimestamp); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds * 2); + + uint256 _reserveAmount = 400e18; + _mockReserves(address(prizePool), _reserveAmount); + + // A new auction has started, so reward should be half the reserve amount + assertEq(rewardLib.reward(0), _reserveAmount / 2); + } + + function testPhase0RewardSetAfterDrawPeriodEnd() public { + uint256 _warpTimestamp = drawPeriodSeconds * 2 + (auctionDuration / 2); + vm.warp(_warpTimestamp); + + assertEq(block.timestamp, _warpTimestamp); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds * 2); + + uint256 _reserveAmount = 400e18; + _mockReserves(address(prizePool), _reserveAmount); + + uint8 _phaseId = 0; + rewardLib.setPhase(_phaseId, drawPeriodSeconds, uint64(_warpTimestamp), recipient); + + vm.warp(drawPeriodSeconds * 2 + auctionDuration); + + // A new auction has started but since the recorded start time + // is before the start of the auction, reward should be 0 + assertEq(rewardLib.reward(0), 0); + } + + function testPhase0RewardSetStartTime0AfterDrawPeriodEnd() public { + uint256 _warpTimestamp = drawPeriodSeconds * 2 + (auctionDuration / 2); + vm.warp(_warpTimestamp); + + assertEq(block.timestamp, _warpTimestamp); + assertEq(prizePool.nextDrawEndsAt(), drawPeriodSeconds * 2); + + uint256 _reserveAmount = 400e18; + _mockReserves(address(prizePool), _reserveAmount); + + uint8 _phaseId = 0; + rewardLib.setPhase(_phaseId, 0, uint64(_warpTimestamp), recipient); + + vm.warp(drawPeriodSeconds * 2 + auctionDuration); + + // A new auction has started, since the recorded start time is 0, + // we set it to the auction start time, so reward should be half the reserve amount + assertEq(rewardLib.reward(0), _reserveAmount / 2); + } +} From f01109c77b8429380ebb0115257cfd071f4014d6 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 23 Jun 2023 18:40:03 -0500 Subject: [PATCH 4/8] fix(Auction): pass phases to _afterAuctionEnds --- src/DrawAuction.sol | 18 +++++++++--------- src/auctions/Auction.sol | 8 ++++++-- src/auctions/TwoStepsAuction.sol | 17 +++++++++++++---- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/DrawAuction.sol b/src/DrawAuction.sol index e21de71..6d994d8 100644 --- a/src/DrawAuction.sol +++ b/src/DrawAuction.sol @@ -75,20 +75,20 @@ contract DrawAuction is TwoStepsAuction { /** * @notice Hook called after the auction has ended. - * @param _randomNumber The random number that was generated + * @param _auctionPhases Array of auction phases + * @param _randomNumber Random number generated by the RNG service */ - function _afterAuctionEnds(uint256 _randomNumber) internal override { - AuctionLib.Phase memory _startRNGPhase = _getPhase(0); - AuctionLib.Phase memory _completeRNGPhase = _getPhase(1); - - AuctionLib.Phase[] memory _auctionPhases = new AuctionLib.Phase[](2); - _auctionPhases[0] = _startRNGPhase; - _auctionPhases[1] = _completeRNGPhase; - + function _afterAuctionEnds( + AuctionLib.Phase[] memory _auctionPhases, + uint256 _randomNumber + ) internal override { uint256[] memory _rewards = RewardLib.rewards(_auctionPhases, _prizePool, _auctionDuration); _prizePool.completeAndStartNextDraw(_randomNumber); + AuctionLib.Phase memory _startRNGPhase = _auctionPhases[0]; + AuctionLib.Phase memory _completeRNGPhase = _auctionPhases[1]; + if (_startRNGPhase.recipient == _completeRNGPhase.recipient) { _prizePool.withdrawReserve(_startRNGPhase.recipient, uint104(_rewards[0] + _rewards[1])); } else { diff --git a/src/auctions/Auction.sol b/src/auctions/Auction.sol index 289c203..6df37c2 100644 --- a/src/auctions/Auction.sol +++ b/src/auctions/Auction.sol @@ -111,9 +111,13 @@ contract Auction { /** * @notice Hook called after the auction has ended. - * @param _randomNumber The random number that was generated + * @param _auctionPhases Array of auction phases + * @param _randomNumber Random number generated by the RNG service */ - function _afterAuctionEnds(uint256 _randomNumber) internal virtual {} + function _afterAuctionEnds( + AuctionLib.Phase[] memory _auctionPhases, + uint256 _randomNumber + ) internal virtual {} /* ============ Getters ============ */ diff --git a/src/auctions/TwoStepsAuction.sol b/src/auctions/TwoStepsAuction.sol index f234336..82f785c 100644 --- a/src/auctions/TwoStepsAuction.sol +++ b/src/auctions/TwoStepsAuction.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.17; -import { Auction } from "src/auctions/Auction.sol"; +import { Auction, AuctionLib } from "src/auctions/Auction.sol"; import { RNGRequestor, RNGInterface } from "src/RNGRequestor.sol"; contract TwoStepsAuction is Auction, RNGRequestor { @@ -41,13 +41,22 @@ contract TwoStepsAuction is Auction, RNGRequestor { /** * @notice Hook called after the RNG request has completed. - * @param _randomNumber The random number that was generated + * @param _randomNumber Random number generated by the RNG service * @param _rewardRecipient Address that will receive the auction reward for completing the RNG request */ function _afterRNGComplete(uint256 _randomNumber, address _rewardRecipient) internal override { - _setPhase(1, _getPhase(0).endTime, uint64(block.timestamp), _rewardRecipient); + AuctionLib.Phase memory _completeRNGPhase = _setPhase( + 1, + _getPhase(0).endTime, + uint64(block.timestamp), + _rewardRecipient + ); emit AuctionPhaseCompleted(1, msg.sender); - _afterAuctionEnds(_randomNumber); + AuctionLib.Phase[] memory _auctionPhases = new AuctionLib.Phase[](2); + _auctionPhases[0] = _getPhase(0); + _auctionPhases[1] = _completeRNGPhase; + + _afterAuctionEnds(_auctionPhases, _randomNumber); } } From a2af693aa8950443f5e08c41b30cc650c10a84e9 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 23 Jun 2023 18:26:45 -0500 Subject: [PATCH 5/8] feat(contracts): add DrawAuctionDispatcher --- src/DrawAuction.sol | 4 +- src/DrawAuctionDispatcher.sol | 149 ++++++++++++++++++++ src/RNGRequestor.sol | 1 - src/auctions/TwoStepsAuction.sol | 2 +- src/interfaces/ISingleMessageDispatcher.sol | 24 ++++ 5 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 src/DrawAuctionDispatcher.sol create mode 100644 src/interfaces/ISingleMessageDispatcher.sol diff --git a/src/DrawAuction.sol b/src/DrawAuction.sol index 6d994d8..6b0cb1f 100644 --- a/src/DrawAuction.sol +++ b/src/DrawAuction.sol @@ -23,7 +23,7 @@ contract DrawAuction is TwoStepsAuction { /* ============ Custom Errors ============ */ /// @notice Thrown when the PrizePool address passed to the constructor is zero address. - error PrizePoolNotZeroAddress(); + error PrizePoolZeroAddress(); /* ============ Constructor ============ */ @@ -44,7 +44,7 @@ contract DrawAuction is TwoStepsAuction { uint32 auctionDuration_, address _owner ) TwoStepsAuction(rng_, rngTimeout_, _auctionPhases, auctionDuration_, _owner) { - if (address(prizePool_) == address(0)) revert PrizePoolNotZeroAddress(); + if (address(prizePool_) == address(0)) revert PrizePoolZeroAddress(); _prizePool = prizePool_; } diff --git a/src/DrawAuctionDispatcher.sol b/src/DrawAuctionDispatcher.sol new file mode 100644 index 0000000..5511336 --- /dev/null +++ b/src/DrawAuctionDispatcher.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { PrizePool } from "v5-prize-pool/PrizePool.sol"; + +import { Auction, AuctionLib } from "src/auctions/Auction.sol"; +import { TwoStepsAuction, RNGInterface } from "src/auctions/TwoStepsAuction.sol"; +import { RewardLib } from "src/libraries/RewardLib.sol"; + +/** + * @title PoolTogether V5 DrawAuctionDispatcher + * @author PoolTogether Inc. Team + * @notice The DrawAuctionDispatcher uses an auction mechanism to incentivize the completion of the Draw. + * This mechanism relies on a linear interpolation to incentivizes anyone to start and complete the Draw. + * The first user to complete the Draw gets rewarded with the partial or full PrizePool reserve amount. + */ +contract DrawAuctionDispatcher is TwoStepsAuction { + /* ============ Events ============ */ + + /** + * @notice Event emitted when the dispatcher is set. + * @param dispatcher Address of the dispatcher on Ethereum that will dispatch the phases and random number + */ + event DispatcherSet(ISingleMessageDispatcher indexed dispatcher); + + /** + * @notice Event emitted when the RNG and auction phases have been dispatched. + * @param dispatcher Address of the dispatcher on Ethereum that dispatched the phases and random number + * @param toChainId ID of the receiving chain + * @param drawAuctionExecutor Address of the DrawAuctionExecutor on the receiving chain that will award the auction and complete the Draw + * @param phases Array of auction phases + * @param randomNumber Random number computed by the RNG + */ + event RNGDispatched( + ISingleMessageDispatcher indexed dispatcher, + uint256 indexed toChainId, + address indexed drawAuctionExecutor, + AuctionLib.Phases[] phases, + uint256 randomNumber + ); + + + /* ============ Variables ============ */ + + /// @notice Address of the dispatcher on Ethereum + ISingleMessageDispatcher internal _dispatcher; + + /// @notice Instance of the PrizePool to compute Draw for. + PrizePool internal immutable _prizePool; + + /* ============ Custom Errors ============ */ + + /// @notice Thrown when the Dispatcher address passed to the constructor is zero address. + error DispatcherZeroAddress(); + + /// @notice Thrown when the PrizePool address passed to the constructor is zero address. + error PrizePoolZeroAddress(); + + /* ============ Constructor ============ */ + + /** + * @notice Contract constructor. + * @param dispatcher_ Address of the dispatcher on Ethereum that will dispatch the phases and random number + * @param rng_ Address of the RNG service + * @param rngTimeout_ Time in seconds before an RNG request can be cancelled + * @param prizePool_ Address of the prize pool + * @param _auctionPhases Number of auction phases + * @param auctionDuration_ Duration of the auction in seconds + * @param _owner Address of the DrawAuctionDispatcher owner + */ + constructor( + ISingleMessageDispatcher dispatcher_, + RNGInterface rng_, + uint32 rngTimeout_, + PrizePool prizePool_, + uint8 _auctionPhases, + uint32 auctionDuration_, + address _owner + ) TwoStepsAuction(rng_, rngTimeout_, _auctionPhases, auctionDuration_, _owner) { + if (address(prizePool_) == address(0)) revert PrizePoolZeroAddress(); + _prizePool = prizePool_; + + _setDispatcher(dispatcher_); + } + + /* ============ External Functions ============ */ + + /* ============ Setter Functions ============ */ + + /** + * @notice Set the dispatcher. + * @dev Only callable by the owner. + * @param dispatcher_ Address of the dispatcher + */ + function setDispatcher(ISingleMessageDispatcher dispatcher_) external onlyOwner { + _setDispatcher(dispatcher_); + } + + /* ============ Internal Functions ============ */ + + /* ============ Hooks ============ */ + + /** + * @notice Hook called after the auction has ended. + * @param _auctionPhases Array of auction phases + * @param _randomNumber Random number generated by the RNG service + */ + function _afterAuctionEnds(AuctionLib.Phase[] memory _auctionPhases, uint256 _randomNumber) internal override { + _dispatchMessage( + _toChainId, + _drawExecutor, + abi.encodeWithSignature("pushDraw((uint256,uint32,uint64,uint64,uint32))", _draw) + ); + + emit RNGDispatched(_auctionPhases, _randomNumber); + } + + /* ============ Dispatch ============ */ + + /** + * @notice Dispatch encoded call. + * @param _dispatcher Address of the dispatcher on Ethereum that will dispatch the call + * @param _toChainId ID of the receiving chain + * @param _drawExecutor Address of the DrawExecutor on the receiving chain that will receive the call + * @param _data Calldata to dispatch + */ + function _dispatchMessage( + uint256 _toChainId, + address _drawExecutor, + bytes memory _data + ) internal { + require(address(_dispatcher) != address(0), "DD/dispatcher-not-zero-address"); + require(_drawExecutor != address(0), "DD/drawExecutor-not-zero-address"); + + _dispatcher.dispatchMessage(_toChainId, _drawExecutor, _data); + } + + /* ============ Setters ============ */ + + /** + * @notice Set the dispatcher. + * @param dispatcher_ Address of the dispatcher + */ + function _setDispatcher(ISingleMessageDispatcher dispatcher_) internal { + if (address(dispatcher_) == address(0)) revert DispatcherZeroAddress(); + _dispatcher = dispatcher_; + emit DispatcherSet(dispatcher_); + } +} diff --git a/src/RNGRequestor.sol b/src/RNGRequestor.sol index f176a1d..8638b74 100644 --- a/src/RNGRequestor.sol +++ b/src/RNGRequestor.sol @@ -9,7 +9,6 @@ import { RNGInterface } from "rng/RNGInterface.sol"; /** * @title PoolTogether V5 RNGRequestor * @author PoolTogether Inc. Team - * TODO: rephrase doc to explain how DrawAuction inherit from this contract * @notice The RNGRequestor allows anyone to request a RNG using the RNG service set. * This contract can be inherited by other contracts and use the `_afterRNGComplete` hook * to make use of the random number generated by the RNG. diff --git a/src/auctions/TwoStepsAuction.sol b/src/auctions/TwoStepsAuction.sol index 82f785c..7924685 100644 --- a/src/auctions/TwoStepsAuction.sol +++ b/src/auctions/TwoStepsAuction.sol @@ -13,7 +13,7 @@ contract TwoStepsAuction is Auction, RNGRequestor { * @param rngTimeout_ Time in seconds before an RNG request can be cancelled * @param _auctionPhases Number of auction phases * @param auctionDuration_ Duration of the auction in seconds - * @param _owner Address of the DrawAuction owner + * @param _owner Address of the TwoStepsAuction owner */ constructor( RNGInterface rng_, diff --git a/src/interfaces/ISingleMessageDispatcher.sol b/src/interfaces/ISingleMessageDispatcher.sol new file mode 100644 index 0000000..bc7220c --- /dev/null +++ b/src/interfaces/ISingleMessageDispatcher.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.17; + +/** + * @title ERC-5164: Cross-Chain Execution Standard, optional SingleMessageDispatcher extension + * @dev See https://eips.ethereum.org/EIPS/eip-5164 + */ +interface ISingleMessageDispatcher { + /** + * @notice Dispatch a message to the receiving chain. + * @dev Must compute and return an ID uniquely identifying the message. + * @dev Must emit the `MessageDispatched` event when successfully dispatched. + * @param toChainId ID of the receiving chain + * @param to Address on the receiving chain that will receive `data` + * @param data Data dispatched to the receiving chain + * @return bytes32 ID uniquely identifying the message + */ + function dispatchMessage( + uint256 toChainId, + address to, + bytes calldata data + ) external returns (bytes32); +} From 7b0f15d3d44e3c751e837152c97f5041ac3db285 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 26 Jun 2023 19:01:24 -0500 Subject: [PATCH 6/8] feat(contracts): add DrawAuctionExecutor --- src/DrawAuctionExecutor.sol | 173 +++++++++++++++++++++++++++++++++ src/abstract/ExecutorAware.sol | 84 ++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 src/DrawAuctionExecutor.sol create mode 100644 src/abstract/ExecutorAware.sol diff --git a/src/DrawAuctionExecutor.sol b/src/DrawAuctionExecutor.sol new file mode 100644 index 0000000..da93692 --- /dev/null +++ b/src/DrawAuctionExecutor.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { PrizePool } from "v5-prize-pool/PrizePool.sol"; + +import { ExecutorAware } from "src/abstract/ExecutorAware.sol"; +import { AuctionLib } from "src/auctions/Auction.sol"; +import { RewardLib } from "src/libraries/RewardLib.sol"; +import { console2 } from "forge-std/Test.sol"; + +contract DrawAuctionExecutor is ExecutorAware { + /* ============ Events ============ */ + + /** + * @notice Emitted when an auction has completed and rewards have been distributed. + * @param phaseIds Ids of the phases + * @param rewardRecipients Addresses of the rewards recipients per phase id + * @param rewardAmounts Amounts of rewards distributed per phase id + */ + event AuctionRewardsDistributed( + uint8[] phaseIds, + address[] rewardRecipients, + uint256[] rewardAmounts + ); + + /* ============ Custom Errors ============ */ + + /// @notice Thrown when the originChainId passed to the constructor is zero. + error OriginChainIdZero(); + + /// @notice Thrown when the DrawAuctionDispatcher address passed to the constructor is zero address. + error DrawAuctionDispatcherZeroAddress(); + + /// @notice Thrown if the DrawAuctionDispatcher has already been set. + error DrawAuctionDispatcherAlreadySet(); + + /// @notice Thrown when the PrizePool address passed to the constructor is zero address. + error PrizePoolZeroAddress(); + + /// @notice Thrown when the message was dispatched from an unsupported chain ID. + error L1ChainIdUnsupported(uint256 fromChainId); + + /// @notice Thrown when the message was not executed by the executor. + error L2SenderNotExecutor(address sender); + + /// @notice Thrown when the message was not dispatched by the DrawAuctionDispatcher on the origin chain. + error L1SenderNotDispatcher(address sender); + + /* ============ Variables ============ */ + + /// @notice ID of the origin chain that dispatches the auction phases and random number. + uint256 internal immutable _originChainId; + + /// @notice Address of the DrawAuctionDispatcher on the origin chain that dispatches the auction phases and random number. + address internal _drawAuctionDispatcher; + + /// @notice Instance of the PrizePool on the destination chain to compute Draw for. + PrizePool internal immutable _prizePool; + + /* ============ Constructor ============ */ + + /** + * @notice DrawAuctionExecutor constructor. + * @param originChainId_ ID of the origin chain + * @param _executor Address of the ERC-5164 contract that executes the bridged calls + * @param prizePool_ Address of the prize pool + */ + constructor( + uint256 originChainId_, + address _executor, + PrizePool prizePool_ + ) ExecutorAware(_executor) { + if (originChainId_ == 0) revert OriginChainIdZero(); + if (address(prizePool_) == address(0)) revert PrizePoolZeroAddress(); + + _originChainId = originChainId_; + _prizePool = prizePool_; + } + + /* ============ External Functions ============ */ + + /** + * @notice Complete the auction and current draw. + * @param _auctionPhases Array of auction phases + * @param _auctionDuration Duration of the auction in seconds + * @param _randomNumber Random number generated by the RNG service on the origin chain + */ + function completeAuction( + AuctionLib.Phase[] memory _auctionPhases, + uint32 _auctionDuration, + uint256 _randomNumber + ) public { + _checkSender(); + + uint256[] memory _rewards = RewardLib.rewards(_auctionPhases, _prizePool, _auctionDuration); + + _prizePool.completeAndStartNextDraw(_randomNumber); + + AuctionLib.Phase memory _startRNGPhase = _auctionPhases[0]; + AuctionLib.Phase memory _completeRNGPhase = _auctionPhases[1]; + + if (_startRNGPhase.recipient == _completeRNGPhase.recipient) { + _prizePool.withdrawReserve(_startRNGPhase.recipient, uint104(_rewards[0] + _rewards[1])); + } else { + _prizePool.withdrawReserve(_startRNGPhase.recipient, uint104(_rewards[0])); + _prizePool.withdrawReserve(_completeRNGPhase.recipient, uint104(_rewards[1])); + } + + uint8[] memory _phaseIds = new uint8[](2); + _phaseIds[0] = _startRNGPhase.id; + _phaseIds[1] = _completeRNGPhase.id; + + address[] memory _rewardRecipients = new address[](2); + _rewardRecipients[0] = _startRNGPhase.recipient; + _rewardRecipients[1] = _completeRNGPhase.recipient; + + emit AuctionRewardsDistributed(_phaseIds, _rewardRecipients, _rewards); + } + + /* ============ Getter Functions ============ */ + + /** + * @notice Get the ID of the origin chain. + * @return ID of the origin chain + */ + function originChainId() public view returns (uint256) { + return _originChainId; + } + + /** + * @notice Get the address of the DrawAuctionDispatcher on the origin chain. + * @return Address of the DrawAuctionDispatcher on the origin chain + */ + function drawAuctionDispatcher() public view returns (address) { + return _drawAuctionDispatcher; + } + + /** + * @notice Get the instance of the PrizePool on the receiving chain. + * @return Instance of the PrizePool on the receiving chain + */ + function prizePool() public view returns (PrizePool) { + return _prizePool; + } + + /* ============ Setters ============ */ + + /** + * @notice Set the DrawAuctionDispatcher address. + * @dev Can only be called once. + * If the transaction get front-run at deployment, we can always re-deploy the contract. + */ + function setDrawAuctionDispatcher(address drawAuctionDispatcher_) public { + if (_drawAuctionDispatcher != address(0)) revert DrawAuctionDispatcherAlreadySet(); + if (drawAuctionDispatcher_ == address(0)) revert DrawAuctionDispatcherZeroAddress(); + + _drawAuctionDispatcher = drawAuctionDispatcher_; + } + + /* ============ Internal Functions ============ */ + + /** + * @notice Checks that: + * - the call has been dispatched from the supported chain + * - the sender on the receiving chain is the executor + * - the sender on the origin chain is the DrawDispatcher + */ + function _checkSender() internal view { + if (_fromChainId() != _originChainId) revert L1ChainIdUnsupported(_fromChainId()); + if (!isTrustedExecutor(msg.sender)) revert L2SenderNotExecutor(msg.sender); + if (_msgSender() != address(_drawAuctionDispatcher)) revert L1SenderNotDispatcher(_msgSender()); + } +} diff --git a/src/abstract/ExecutorAware.sol b/src/abstract/ExecutorAware.sol new file mode 100644 index 0000000..3b100f5 --- /dev/null +++ b/src/abstract/ExecutorAware.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.17; + +/** + * @title ExecutorAware abstract contract + * @notice The ExecutorAware contract allows contracts on a receiving chain to execute messages from an origin chain. + * These messages are sent by the `MessageDispatcher` contract which live on the origin chain. + * The `MessageExecutor` contract on the receiving chain executes these messages + * and then forward them to an ExecutorAware contract on the receiving chain. + * @dev This contract implements EIP-2771 (https://eips.ethereum.org/EIPS/eip-2771) + * to ensure that messages are sent by a trusted `MessageExecutor` contract. + */ +abstract contract ExecutorAware { + /* ============ Variables ============ */ + + /// @notice Address of the trusted executor contract. + address public immutable trustedExecutor; + + /* ============ Constructor ============ */ + + /** + * @notice ExecutorAware constructor. + * @param _executor Address of the `MessageExecutor` contract + */ + constructor(address _executor) { + require(_executor != address(0), "executor-not-zero-address"); + trustedExecutor = _executor; + } + + /* ============ External Functions ============ */ + + /** + * @notice Check which executor this contract trust. + * @param _executor Address to check + */ + function isTrustedExecutor(address _executor) public view returns (bool) { + return _executor == trustedExecutor; + } + + /* ============ Internal Functions ============ */ + + /** + * @notice Retrieve messageId from message data. + * @return _msgDataMessageId ID uniquely identifying the message that was executed + */ + function _messageId() internal pure returns (bytes32 _msgDataMessageId) { + _msgDataMessageId; + + if (msg.data.length >= 84) { + assembly { + _msgDataMessageId := calldataload(sub(calldatasize(), 84)) + } + } + } + + /** + * @notice Retrieve fromChainId from message data. + * @return _msgDataFromChainId ID of the chain that dispatched the messages + */ + function _fromChainId() internal pure returns (uint256 _msgDataFromChainId) { + _msgDataFromChainId; + + if (msg.data.length >= 52) { + assembly { + _msgDataFromChainId := calldataload(sub(calldatasize(), 52)) + } + } + } + + /** + * @notice Retrieve signer address from message data. + * @return _signer Address of the signer + */ + function _msgSender() internal view returns (address payable _signer) { + _signer = payable(msg.sender); + + if (msg.data.length >= 20 && isTrustedExecutor(_signer)) { + assembly { + _signer := shr(96, calldataload(sub(calldatasize(), 20))) + } + } + } +} From 5d8a39b165d20cfbebe98ea46945f6fcca5b919f Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 26 Jun 2023 19:01:56 -0500 Subject: [PATCH 7/8] feat(DrawAuctionDispatcher): add fork tests --- lcov.info | 370 +++++++++++------- src/DrawAuctionDispatcher.sol | 154 +++++--- src/RNGRequestor.sol | 2 +- test/DrawAuction.t.sol | 5 + test/RNGRequestor.t.sol | 2 +- test/auctions/Auctions.t.sol | 2 +- ...tionDispatcherEthereumToOptimismFork.t.sol | 272 +++++++++++++ 7 files changed, 616 insertions(+), 191 deletions(-) create mode 100644 test/fork/DrawAuctionDispatcherEthereumToOptimismFork.t.sol diff --git a/lcov.info b/lcov.info index 1a02388..5c86259 100644 --- a/lcov.info +++ b/lcov.info @@ -1,65 +1,139 @@ TN: SF:src/DrawAuction.sol -FN:60,DrawAuction.prizePool -FN:69,DrawAuction.reward +FN:59,DrawAuction.prizePool +FN:68,DrawAuction.reward FN:81,DrawAuction._afterAuctionEnds FNDA:1,DrawAuction.prizePool FNDA:7,DrawAuction.reward FNDA:3,DrawAuction._afterAuctionEnds FNF:3 FNH:3 -DA:61,1 -DA:70,7 -DA:82,3 -DA:83,3 +DA:60,1 +DA:69,7 DA:85,3 -DA:86,3 DA:87,3 DA:89,3 -DA:91,3 -DA:93,3 -DA:95,3 -DA:96,3 -DA:98,3 -DA:99,2 -DA:101,1 -DA:102,1 +DA:90,3 +DA:92,3 +DA:93,2 +DA:95,1 +DA:96,1 +DA:99,3 +DA:100,3 +DA:101,3 +DA:103,3 +DA:104,3 DA:105,3 -DA:106,3 DA:107,3 -DA:109,3 -DA:110,3 -DA:111,3 -DA:113,3 -LF:23 -LH:23 +LF:17 +LH:17 +end_of_record +TN: +SF:src/DrawAuctionDispatcher.sol +FN:106,DrawAuctionDispatcher.dispatcher +FN:114,DrawAuctionDispatcher.drawAuctionExecutor +FN:122,DrawAuctionDispatcher.toChainId +FN:133,DrawAuctionDispatcher.setDispatcher +FN:142,DrawAuctionDispatcher.setDrawAuctionExecutor +FN:155,DrawAuctionDispatcher._afterAuctionEnds +FN:185,DrawAuctionDispatcher._setDispatcher +FN:195,DrawAuctionDispatcher._setDrawAuctionExecutor +FNDA:3,DrawAuctionDispatcher.dispatcher +FNDA:3,DrawAuctionDispatcher.drawAuctionExecutor +FNDA:2,DrawAuctionDispatcher.toChainId +FNDA:5,DrawAuctionDispatcher.setDispatcher +FNDA:9,DrawAuctionDispatcher.setDrawAuctionExecutor +FNDA:1,DrawAuctionDispatcher._afterAuctionEnds +FNDA:3,DrawAuctionDispatcher._setDispatcher +FNDA:9,DrawAuctionDispatcher._setDrawAuctionExecutor +FNF:8 +FNH:8 +DA:107,3 +DA:115,3 +DA:123,2 +DA:134,3 +DA:143,9 +DA:159,1 +DA:170,1 +DA:186,3 +DA:187,1 +DA:188,1 +DA:196,9 +DA:197,9 +DA:198,9 +LF:13 +LH:13 +end_of_record +TN: +SF:src/DrawAuctionExecutor.sol +FN:125,DrawAuctionExecutor.originChainId +FN:133,DrawAuctionExecutor.drawAuctionDispatcher +FN:141,DrawAuctionExecutor.prizePool +FN:152,DrawAuctionExecutor.setDrawAuctionDispatcher +FN:167,DrawAuctionExecutor._checkSender +FN:87,DrawAuctionExecutor.completeAuction +FNDA:0,DrawAuctionExecutor.originChainId +FNDA:0,DrawAuctionExecutor.drawAuctionDispatcher +FNDA:0,DrawAuctionExecutor.prizePool +FNDA:8,DrawAuctionExecutor.setDrawAuctionDispatcher +FNDA:0,DrawAuctionExecutor._checkSender +FNDA:0,DrawAuctionExecutor.completeAuction +FNF:6 +FNH:1 +DA:92,0 +DA:94,0 +DA:96,0 +DA:98,0 +DA:99,0 +DA:101,0 +DA:102,0 +DA:104,0 +DA:105,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:116,0 +DA:126,0 +DA:134,0 +DA:142,0 +DA:153,8 +DA:154,8 +DA:156,8 +DA:168,0 +DA:169,0 +DA:170,0 +LF:25 +LH:3 end_of_record TN: SF:src/RNGRequestor.sol -FN:164,RNGRequestor.startRNGRequest -FN:186,RNGRequestor.completeRNGRequest -FN:198,RNGRequestor.cancelRNGRequest -FN:215,RNGRequestor.isRNGRequested -FN:223,RNGRequestor.isRNGCompleted -FN:231,RNGRequestor.isRNGTimedOut -FN:239,RNGRequestor.canStartRNGRequest -FN:247,RNGRequestor.canCompleteRNGRequest -FN:257,RNGRequestor.getRNGLockBlock -FN:266,RNGRequestor.getRNGRequestId -FN:274,RNGRequestor.getRNGTimeout -FN:282,RNGRequestor.getRNGService -FN:294,RNGRequestor.setRNGService -FN:304,RNGRequestor.setRNGTimeout -FN:314,RNGRequestor._afterRNGStart -FN:321,RNGRequestor._afterRNGComplete -FN:327,RNGRequestor._currentTime -FN:335,RNGRequestor._isRNGRequested -FN:343,RNGRequestor._isRNGCompleted -FN:351,RNGRequestor._isRNGTimedOut -FN:363,RNGRequestor._setRNGService -FN:373,RNGRequestor._setRNGTimeout -FNDA:17,RNGRequestor.startRNGRequest -FNDA:6,RNGRequestor.completeRNGRequest +FN:163,RNGRequestor.startRNGRequest +FN:185,RNGRequestor.completeRNGRequest +FN:197,RNGRequestor.cancelRNGRequest +FN:214,RNGRequestor.isRNGRequested +FN:222,RNGRequestor.isRNGCompleted +FN:230,RNGRequestor.isRNGTimedOut +FN:238,RNGRequestor.canStartRNGRequest +FN:246,RNGRequestor.canCompleteRNGRequest +FN:256,RNGRequestor.getRNGLockBlock +FN:265,RNGRequestor.getRNGRequestId +FN:273,RNGRequestor.getRNGTimeout +FN:281,RNGRequestor.getRNGService +FN:293,RNGRequestor.setRNGService +FN:303,RNGRequestor.setRNGTimeout +FN:313,RNGRequestor._afterRNGStart +FN:320,RNGRequestor._afterRNGComplete +FN:326,RNGRequestor._currentTime +FN:334,RNGRequestor._isRNGRequested +FN:342,RNGRequestor._isRNGCompleted +FN:350,RNGRequestor._isRNGTimedOut +FN:362,RNGRequestor._setRNGService +FN:372,RNGRequestor._setRNGTimeout +FNDA:18,RNGRequestor.startRNGRequest +FNDA:7,RNGRequestor.completeRNGRequest FNDA:2,RNGRequestor.cancelRNGRequest FNDA:2,RNGRequestor.isRNGRequested FNDA:2,RNGRequestor.isRNGCompleted @@ -74,86 +148,109 @@ FNDA:2,RNGRequestor.setRNGService FNDA:2,RNGRequestor.setRNGTimeout FNDA:11,RNGRequestor._afterRNGStart FNDA:1,RNGRequestor._afterRNGComplete -FNDA:19,RNGRequestor._currentTime -FNDA:29,RNGRequestor._isRNGRequested -FNDA:8,RNGRequestor._isRNGCompleted +FNDA:20,RNGRequestor._currentTime +FNDA:31,RNGRequestor._isRNGRequested +FNDA:9,RNGRequestor._isRNGCompleted FNDA:4,RNGRequestor._isRNGTimedOut FNDA:2,RNGRequestor._setRNGService FNDA:2,RNGRequestor._setRNGTimeout FNF:22 FNH:22 -DA:165,16 -DA:167,16 -DA:168,1 -DA:171,16 -DA:172,16 -DA:173,16 -DA:174,16 -DA:176,16 -DA:178,16 -DA:187,4 -DA:188,4 -DA:190,4 -DA:192,4 -DA:194,4 -DA:199,2 +DA:164,17 +DA:166,17 +DA:167,1 +DA:170,17 +DA:171,17 +DA:172,17 +DA:173,17 +DA:175,17 +DA:177,17 +DA:186,5 +DA:187,5 +DA:189,5 +DA:191,5 +DA:193,5 +DA:198,2 +DA:200,1 DA:201,1 -DA:202,1 -DA:204,1 -DA:206,1 -DA:216,2 -DA:224,2 -DA:232,2 -DA:240,2 -DA:248,2 -DA:258,3 -DA:267,3 -DA:275,3 -DA:283,3 -DA:295,2 -DA:305,2 -DA:328,19 -DA:336,29 -DA:344,8 -DA:352,4 -DA:353,1 -DA:355,3 -DA:364,2 +DA:203,1 +DA:205,1 +DA:215,2 +DA:223,2 +DA:231,2 +DA:239,2 +DA:247,2 +DA:257,3 +DA:266,3 +DA:274,3 +DA:282,3 +DA:294,2 +DA:304,2 +DA:327,20 +DA:335,31 +DA:343,9 +DA:351,4 +DA:352,1 +DA:354,3 +DA:363,2 +DA:364,1 DA:365,1 -DA:366,1 -DA:374,2 +DA:373,2 +DA:374,1 DA:375,1 -DA:376,1 LF:42 LH:42 end_of_record TN: +SF:src/abstract/ExecutorAware.sol +FN:37,ExecutorAware.isTrustedExecutor +FN:47,ExecutorAware._messageId +FN:61,ExecutorAware._fromChainId +FN:75,ExecutorAware._msgSender +FNDA:0,ExecutorAware.isTrustedExecutor +FNDA:0,ExecutorAware._messageId +FNDA:0,ExecutorAware._fromChainId +FNDA:0,ExecutorAware._msgSender +FNF:4 +FNH:0 +DA:38,0 +DA:50,0 +DA:52,0 +DA:64,0 +DA:66,0 +DA:76,0 +DA:78,0 +DA:80,0 +LF:8 +LH:0 +end_of_record +TN: SF:src/auctions/Auction.sol FN:104,Auction.getPhase -FN:116,Auction._afterAuctionEnds -FN:124,Auction._getPhases -FN:133,Auction._getPhase -FN:147,Auction._setPhase +FN:117,Auction._afterAuctionEnds +FN:128,Auction._getPhases +FN:137,Auction._getPhase +FN:151,Auction._setPhase FN:87,Auction.auctionDuration FN:95,Auction.getPhases -FNDA:1,Auction.getPhase +FNDA:2,Auction.getPhase FNDA:1,Auction._afterAuctionEnds FNDA:1,Auction._getPhases -FNDA:11,Auction._getPhase -FNDA:14,Auction._setPhase +FNDA:12,Auction._getPhase +FNDA:16,Auction._setPhase FNDA:2,Auction.auctionDuration FNDA:1,Auction.getPhases FNF:7 FNH:7 DA:88,2 DA:96,1 -DA:105,1 -DA:125,1 -DA:134,11 -DA:153,14 -DA:160,14 -DA:162,14 -DA:164,14 +DA:105,2 +DA:129,1 +DA:138,12 +DA:157,16 +DA:164,16 +DA:166,16 +DA:168,16 LF:9 LH:9 end_of_record @@ -161,55 +258,54 @@ TN: SF:src/auctions/TwoStepsAuction.sol FN:37,TwoStepsAuction._afterRNGStart FN:47,TwoStepsAuction._afterRNGComplete -FNDA:6,TwoStepsAuction._afterRNGStart -FNDA:4,TwoStepsAuction._afterRNGComplete +FNDA:7,TwoStepsAuction._afterRNGStart +FNDA:5,TwoStepsAuction._afterRNGComplete FNF:2 FNH:2 -DA:38,6 -DA:39,6 -DA:48,4 -DA:49,4 -DA:51,4 -LF:5 -LH:5 +DA:38,7 +DA:39,7 +DA:48,5 +DA:54,5 +DA:56,5 +DA:57,5 +DA:58,5 +DA:60,5 +LF:8 +LH:8 end_of_record TN: SF:src/libraries/RewardLib.sol -FN:24,RewardLib.rewards -FN:55,RewardLib.reward -FN:83,RewardLib._reward +FN:22,RewardLib.rewards +FN:53,RewardLib.reward +FN:81,RewardLib._reward FNDA:3,RewardLib.rewards FNDA:22,RewardLib.reward FNDA:28,RewardLib._reward FNF:3 FNH:3 -DA:29,3 +DA:27,3 +DA:28,3 DA:30,3 DA:32,3 -DA:34,3 +DA:33,3 DA:35,3 -DA:37,3 -DA:38,6 -DA:41,3 -DA:60,22 +DA:36,6 +DA:39,3 +DA:58,22 +DA:59,22 DA:61,22 DA:63,22 -DA:65,22 -DA:91,28 -DA:92,5 +DA:89,28 +DA:90,5 DA:95,23 -DA:96,23 -DA:100,23 -DA:101,14 -DA:106,23 -DA:107,2 -DA:111,21 -DA:112,4 -DA:118,21 -DA:119,3 -DA:122,21 -DA:123,21 -DA:125,21 -LF:27 -LH:27 +DA:96,14 +DA:101,23 +DA:102,2 +DA:106,21 +DA:107,4 +DA:113,21 +DA:114,3 +DA:117,21 +LF:23 +LH:23 end_of_record diff --git a/src/DrawAuctionDispatcher.sol b/src/DrawAuctionDispatcher.sol index 5511336..99ca191 100644 --- a/src/DrawAuctionDispatcher.sol +++ b/src/DrawAuctionDispatcher.sol @@ -5,7 +5,9 @@ import { PrizePool } from "v5-prize-pool/PrizePool.sol"; import { Auction, AuctionLib } from "src/auctions/Auction.sol"; import { TwoStepsAuction, RNGInterface } from "src/auctions/TwoStepsAuction.sol"; +import { ISingleMessageDispatcher } from "src/interfaces/ISingleMessageDispatcher.sol"; import { RewardLib } from "src/libraries/RewardLib.sol"; +import { console2 } from "forge-std/Test.sol"; /** * @title PoolTogether V5 DrawAuctionDispatcher @@ -19,75 +21,112 @@ contract DrawAuctionDispatcher is TwoStepsAuction { /** * @notice Event emitted when the dispatcher is set. - * @param dispatcher Address of the dispatcher on Ethereum that will dispatch the phases and random number + * @param dispatcher Instance of the dispatcher on Ethereum that will dispatch the phases and random number */ event DispatcherSet(ISingleMessageDispatcher indexed dispatcher); + /** + * @notice Event emitted when the drawAuctionExecutor is set. + * @param drawAuctionExecutor Address of the drawAuctionExecutor on the receiving chain that will complete the Draw + */ + event DrawAuctionExecutorSet(address indexed drawAuctionExecutor); + /** * @notice Event emitted when the RNG and auction phases have been dispatched. - * @param dispatcher Address of the dispatcher on Ethereum that dispatched the phases and random number + * @param dispatcher Instance of the dispatcher on Ethereum that dispatched the phases and random number * @param toChainId ID of the receiving chain * @param drawAuctionExecutor Address of the DrawAuctionExecutor on the receiving chain that will award the auction and complete the Draw * @param phases Array of auction phases * @param randomNumber Random number computed by the RNG */ - event RNGDispatched( + event AuctionDispatched( ISingleMessageDispatcher indexed dispatcher, uint256 indexed toChainId, address indexed drawAuctionExecutor, - AuctionLib.Phases[] phases, + AuctionLib.Phase[] phases, uint256 randomNumber ); + /* ============ Custom Errors ============ */ - /* ============ Variables ============ */ + /// @notice Thrown when the Dispatcher address passed to the constructor is zero address. + error DispatcherZeroAddress(); - /// @notice Address of the dispatcher on Ethereum - ISingleMessageDispatcher internal _dispatcher; + /// @notice Thrown when the toChainId passed to the constructor is zero. + error ToChainIdZero(); - /// @notice Instance of the PrizePool to compute Draw for. - PrizePool internal immutable _prizePool; + /// @notice Thrown when the DrawAuctionExecutor address passed to the constructor is zero address. + error DrawAuctionExecutorZeroAddress(); - /* ============ Custom Errors ============ */ + /* ============ Variables ============ */ - /// @notice Thrown when the Dispatcher address passed to the constructor is zero address. - error DispatcherZeroAddress(); + /// @notice Instance of the dispatcher on Ethereum + ISingleMessageDispatcher internal _dispatcher; + + /// @notice ID of the receiving chain + uint256 internal immutable _toChainId; - /// @notice Thrown when the PrizePool address passed to the constructor is zero address. - error PrizePoolZeroAddress(); + /// @notice Address of the DrawAuctionExecutor to compute Draw for. + address internal _drawAuctionExecutor; /* ============ Constructor ============ */ /** * @notice Contract constructor. - * @param dispatcher_ Address of the dispatcher on Ethereum that will dispatch the phases and random number - * @param rng_ Address of the RNG service + * @param dispatcher_ Instance of the dispatcher on Ethereum that will dispatch the phases and random number + * @param toChainId_ ID of the receiving chain + * @param rng_ Instance of the RNG service * @param rngTimeout_ Time in seconds before an RNG request can be cancelled - * @param prizePool_ Address of the prize pool * @param _auctionPhases Number of auction phases * @param auctionDuration_ Duration of the auction in seconds * @param _owner Address of the DrawAuctionDispatcher owner */ constructor( ISingleMessageDispatcher dispatcher_, + uint256 toChainId_, RNGInterface rng_, uint32 rngTimeout_, - PrizePool prizePool_, uint8 _auctionPhases, uint32 auctionDuration_, address _owner ) TwoStepsAuction(rng_, rngTimeout_, _auctionPhases, auctionDuration_, _owner) { - if (address(prizePool_) == address(0)) revert PrizePoolZeroAddress(); - _prizePool = prizePool_; - _setDispatcher(dispatcher_); + + if (toChainId_ == 0) revert ToChainIdZero(); + _toChainId = toChainId_; } /* ============ External Functions ============ */ - /* ============ Setter Functions ============ */ + /* ============ Getters ============ */ - /** + /** + * @notice Get the dispatcher. + * @return Instance of the dispatcher + */ + function dispatcher() external view returns (ISingleMessageDispatcher) { + return _dispatcher; + } + + /** + * @notice Get the drawAuctionExecutor address on the receiving chain. + * @return Address of the DrawAuctionExecutor on the receiving chain + */ + function drawAuctionExecutor() external view returns (address) { + return _drawAuctionExecutor; + } + + /** + * @notice Get the toChainId. + * @return ID of the receiving chain + */ + function toChainId() external view returns (uint256) { + return _toChainId; + } + + /* ============ Setters ============ */ + + /** * @notice Set the dispatcher. * @dev Only callable by the owner. * @param dispatcher_ Address of the dispatcher @@ -96,6 +135,15 @@ contract DrawAuctionDispatcher is TwoStepsAuction { _setDispatcher(dispatcher_); } + /** + * @notice Set the drawAuctionExecutor. + * @dev Only callable by the owner. + * @param drawAuctionExecutor_ Address of the drawAuctionExecutor + */ + function setDrawAuctionExecutor(address drawAuctionExecutor_) external onlyOwner { + _setDrawAuctionExecutor(drawAuctionExecutor_); + } + /* ============ Internal Functions ============ */ /* ============ Hooks ============ */ @@ -105,34 +153,28 @@ contract DrawAuctionDispatcher is TwoStepsAuction { * @param _auctionPhases Array of auction phases * @param _randomNumber Random number generated by the RNG service */ - function _afterAuctionEnds(AuctionLib.Phase[] memory _auctionPhases, uint256 _randomNumber) internal override { - _dispatchMessage( - _toChainId, - _drawExecutor, - abi.encodeWithSignature("pushDraw((uint256,uint32,uint64,uint64,uint32))", _draw) - ); - - emit RNGDispatched(_auctionPhases, _randomNumber); - } - - /* ============ Dispatch ============ */ - - /** - * @notice Dispatch encoded call. - * @param _dispatcher Address of the dispatcher on Ethereum that will dispatch the call - * @param _toChainId ID of the receiving chain - * @param _drawExecutor Address of the DrawExecutor on the receiving chain that will receive the call - * @param _data Calldata to dispatch - */ - function _dispatchMessage( - uint256 _toChainId, - address _drawExecutor, - bytes memory _data - ) internal { - require(address(_dispatcher) != address(0), "DD/dispatcher-not-zero-address"); - require(_drawExecutor != address(0), "DD/drawExecutor-not-zero-address"); - - _dispatcher.dispatchMessage(_toChainId, _drawExecutor, _data); + function _afterAuctionEnds( + AuctionLib.Phase[] memory _auctionPhases, + uint256 _randomNumber + ) internal override { + _dispatcher.dispatchMessage( + _toChainId, + _drawAuctionExecutor, + abi.encodeWithSignature( + "completeAuction((uint8,uint64,uint64,address)[],uint32,uint256)", + _auctionPhases, + _auctionDuration, + _randomNumber + ) + ); + + emit AuctionDispatched( + _dispatcher, + _toChainId, + _drawAuctionExecutor, + _auctionPhases, + _randomNumber + ); } /* ============ Setters ============ */ @@ -146,4 +188,14 @@ contract DrawAuctionDispatcher is TwoStepsAuction { _dispatcher = dispatcher_; emit DispatcherSet(dispatcher_); } + + /** + * @notice Set the drawAuctionExecutor. + * @param drawAuctionExecutor_ Address of the drawAuctionExecutor + */ + function _setDrawAuctionExecutor(address drawAuctionExecutor_) internal { + if (drawAuctionExecutor_ == address(0)) revert DrawAuctionExecutorZeroAddress(); + _drawAuctionExecutor = drawAuctionExecutor_; + emit DrawAuctionExecutorSet(drawAuctionExecutor_); + } } diff --git a/src/RNGRequestor.sol b/src/RNGRequestor.sol index 8638b74..1164464 100644 --- a/src/RNGRequestor.sol +++ b/src/RNGRequestor.sol @@ -282,7 +282,7 @@ contract RNGRequestor is Ownable { return _rng; } - /* ============ Setter Functions ============ */ + /* ============ Setters ============ */ /** * @notice Sets the RNG service used to generate random numbers. diff --git a/test/DrawAuction.t.sol b/test/DrawAuction.t.sol index d293978..6775991 100644 --- a/test/DrawAuction.t.sol +++ b/test/DrawAuction.t.sol @@ -11,6 +11,11 @@ import { Helpers, RNGInterface } from "test/helpers/Helpers.t.sol"; contract DrawAuctionTest is Helpers { /* ============ Events ============ */ + event AuctionRewardsDistributed( + uint8[] phaseIds, + address[] rewardRecipients, + uint256[] rewardAmounts + ); /* ============ Variables ============ */ diff --git a/test/RNGRequestor.t.sol b/test/RNGRequestor.t.sol index 79ddf93..d1a7335 100644 --- a/test/RNGRequestor.t.sol +++ b/test/RNGRequestor.t.sol @@ -258,7 +258,7 @@ contract RNGRequestorTest is Helpers { assertEq(address(rngRequestor.getRNGService()), address(rng)); } - /* ============ Setter Functions ============ */ + /* ============ Setters ============ */ /* ============ setRNGService ============ */ function testSetRNGService() public { diff --git a/test/auctions/Auctions.t.sol b/test/auctions/Auctions.t.sol index 92201fa..3943837 100644 --- a/test/auctions/Auctions.t.sol +++ b/test/auctions/Auctions.t.sol @@ -81,7 +81,7 @@ contract AuctionTest is Test { assertEq(_phase.recipient, _recipient); } - /* ============ Setter Functions ============ */ + /* ============ Setters ============ */ function testSetPhase() public { uint8 _phaseId = 0; diff --git a/test/fork/DrawAuctionDispatcherEthereumToOptimismFork.t.sol b/test/fork/DrawAuctionDispatcherEthereumToOptimismFork.t.sol new file mode 100644 index 0000000..3a2b596 --- /dev/null +++ b/test/fork/DrawAuctionDispatcherEthereumToOptimismFork.t.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { ERC20Mock } from "openzeppelin/mocks/ERC20Mock.sol"; + +import { UD2x18, SD1x18, ConstructorParams, PrizePool, TieredLiquidityDistributor, TwabController } from "v5-prize-pool/PrizePool.sol"; + +import { DrawAuctionDispatcher, ISingleMessageDispatcher } from "src/DrawAuctionDispatcher.sol"; +import { DrawAuctionExecutor } from "src/DrawAuctionExecutor.sol"; +import { AuctionLib } from "src/libraries/AuctionLib.sol"; +import { Helpers, RNGInterface } from "test/helpers/Helpers.t.sol"; +import { console2 } from "forge-std/Test.sol"; + +contract DrawAuctionDispatcherEthereumToOptimismForkTest is Helpers { + /* ============ Events ============ */ + + event DispatcherSet(ISingleMessageDispatcher indexed dispatcher); + event DrawAuctionExecutorSet(address indexed drawAuctionExecutor); + + event AuctionDispatched( + ISingleMessageDispatcher indexed dispatcher, + uint256 indexed toChainId, + address indexed drawAuctionExecutor, + AuctionLib.Phase[] phases, + uint256 randomNumber + ); + + /* ============ Variables ============ */ + + uint256 public mainnetFork; + uint256 public optimismFork; + + address public proxyOVML1CrossDomainMessenger = 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1; + address public l2CrossDomainMessenger = 0x4200000000000000000000000000000000000007; + + uint256 public nonce = 1; + uint256 public toChainId = 10; + uint256 public fromChainId = 1; + + ISingleMessageDispatcher public dispatcher = + ISingleMessageDispatcher(address(0xa8f85bAB964D7e6bE938B54Bf4b29A247A88CD9d)); + address public executor = 0x890a87E71E731342a6d10e7628bd1F0733ce3296; + + DrawAuctionDispatcher public drawAuctionDispatcher; + DrawAuctionExecutor public drawAuctionExecutor; + + ERC20Mock public prizeToken; + PrizePool public prizePool; + RNGInterface public rng = RNGInterface(address(1)); + + uint32 public auctionDuration = 3 hours; + uint32 public rngTimeOut = 1 hours; + uint32 public drawPeriodSeconds = 1 days; + uint256 public randomNumber = 123456789; + address public recipient = address(this); + + /* ============ Setup ============ */ + + function setUp() public { + mainnetFork = vm.createFork(vm.rpcUrl("mainnet")); + optimismFork = vm.createFork(vm.rpcUrl("optimism")); + + vm.warp(0); + } + + function deployDrawAuctionDispatcher() public { + vm.selectFork(mainnetFork); + + drawAuctionDispatcher = new DrawAuctionDispatcher( + dispatcher, + toChainId, + rng, + rngTimeOut, + 2, + auctionDuration, + address(this) + ); + + vm.makePersistent(address(drawAuctionDispatcher)); + } + + function deployDrawAuctionExecutor() public { + vm.selectFork(optimismFork); + + drawAuctionExecutor = new DrawAuctionExecutor(fromChainId, executor, prizePool); + + vm.makePersistent(address(drawAuctionExecutor)); + } + + function deployPrizePool() public { + vm.selectFork(optimismFork); + + prizeToken = new ERC20Mock(); + + prizePool = new PrizePool( + ConstructorParams({ + prizeToken: prizeToken, + twabController: TwabController(address(0)), + drawManager: address(0), + grandPrizePeriodDraws: uint32(365), + drawPeriodSeconds: drawPeriodSeconds, + firstDrawStartsAt: uint64(block.timestamp), + numberOfTiers: uint8(3), // minimum number of tiers + tierShares: 100, + canaryShares: 10, + reserveShares: 10, + claimExpansionThreshold: UD2x18.wrap(0.9e18), // claim threshold of 90% + smoothing: SD1x18.wrap(0.9e18) // alpha + }) + ); + + vm.makePersistent(address(prizePool)); + } + + function deployAll() public { + deployDrawAuctionDispatcher(); + deployPrizePool(); + deployDrawAuctionExecutor(); + } + + function setDrawAuctionExecutor() public { + vm.selectFork(mainnetFork); + drawAuctionDispatcher.setDrawAuctionExecutor(address(drawAuctionExecutor)); + } + + function setDrawAuctionDispatcher() public { + vm.selectFork(optimismFork); + drawAuctionExecutor.setDrawAuctionDispatcher(address(drawAuctionDispatcher)); + } + + function setPrizePoolDrawManager() public { + vm.selectFork(optimismFork); + prizePool.setDrawManager(address(drawAuctionDispatcher)); + } + + function setAll() public { + setDrawAuctionExecutor(); + setDrawAuctionDispatcher(); + setPrizePoolDrawManager(); + } + + /* ============ Auction Dispatch ============ */ + function testAfterAuctionEnds() public { + deployAll(); + setAll(); + + uint256 _reserveAmount = 200e18; + + vm.selectFork(optimismFork); + + prizeToken.mint(address(prizePool), _reserveAmount * 110); + prizePool.contributePrizeTokens(address(2), _reserveAmount * 110); + + vm.selectFork(mainnetFork); + + vm.warp(drawPeriodSeconds + auctionDuration / 2); + + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); + + drawAuctionDispatcher.startRNGRequest(recipient); + + uint64 _warpTimestamp = uint64(drawPeriodSeconds + auctionDuration); + vm.warp(_warpTimestamp); + + _mockCompleteRNGRequest(address(rng), _requestId, randomNumber); + + AuctionLib.Phase[] memory _auctionPhases = new AuctionLib.Phase[](2); + _auctionPhases[0] = drawAuctionDispatcher.getPhase(0); + _auctionPhases[1] = _getPhase(1, _auctionPhases[0].endTime, _warpTimestamp, recipient); + + vm.expectEmit(); + emit AuctionDispatched( + drawAuctionDispatcher.dispatcher(), + drawAuctionDispatcher.toChainId(), + drawAuctionDispatcher.drawAuctionExecutor(), + _auctionPhases, + randomNumber + ); + + drawAuctionDispatcher.completeRNGRequest(recipient); + } + + /* ============ Getters ============ */ + + function testGetters() public { + deployAll(); + setAll(); + + assertEq(address(drawAuctionDispatcher.dispatcher()), address(dispatcher)); + assertEq(drawAuctionDispatcher.drawAuctionExecutor(), address(drawAuctionExecutor)); + assertEq(drawAuctionDispatcher.toChainId(), toChainId); + } + + /* ============ Setters ============ */ + + /* ============ setDispatcher ============ */ + function testSetDispatcher() public { + deployAll(); + setAll(); + + ISingleMessageDispatcher _dispatcher = ISingleMessageDispatcher(address(2)); + + vm.expectEmit(); + emit DispatcherSet(_dispatcher); + + drawAuctionDispatcher.setDispatcher(_dispatcher); + + assertEq(address(drawAuctionDispatcher.dispatcher()), address(_dispatcher)); + } + + function testSetDispatcherFailAddressZero() public { + deployAll(); + setAll(); + + ISingleMessageDispatcher _dispatcher = ISingleMessageDispatcher(address(0)); + + vm.expectRevert(abi.encodeWithSelector(DrawAuctionDispatcher.DispatcherZeroAddress.selector)); + drawAuctionDispatcher.setDispatcher(_dispatcher); + } + + function testSetDispatcherFailNotOwner() public { + deployAll(); + setAll(); + + ISingleMessageDispatcher _dispatcher = ISingleMessageDispatcher(address(2)); + + vm.startPrank(address(4)); + + vm.expectRevert(bytes("Ownable/caller-not-owner")); + drawAuctionDispatcher.setDispatcher(_dispatcher); + } + + /* ============ setDrawAuctionExecutor ============ */ + function testSetDrawAuctionExecutor() public { + deployAll(); + setAll(); + + address _drawAuctionExecutor = address(3); + + vm.expectEmit(); + emit DrawAuctionExecutorSet(_drawAuctionExecutor); + + drawAuctionDispatcher.setDrawAuctionExecutor(_drawAuctionExecutor); + + assertEq(address(drawAuctionDispatcher.drawAuctionExecutor()), address(_drawAuctionExecutor)); + } + + function testSetDrawAuctionExecutorFailAddressZero() public { + deployAll(); + setAll(); + + ISingleMessageDispatcher _dispatcher = ISingleMessageDispatcher(address(0)); + + vm.expectRevert(abi.encodeWithSelector(DrawAuctionDispatcher.DispatcherZeroAddress.selector)); + drawAuctionDispatcher.setDispatcher(_dispatcher); + } + + function testSetDrawAuctionExecutorFailNotOwner() public { + deployAll(); + setAll(); + + ISingleMessageDispatcher _dispatcher = ISingleMessageDispatcher(address(3)); + + vm.startPrank(address(4)); + + vm.expectRevert(bytes("Ownable/caller-not-owner")); + drawAuctionDispatcher.setDispatcher(_dispatcher); + } +} From 67e32a20df08d886f645da4b5b1ebdfba26e16fb Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Thu, 29 Jun 2023 16:55:51 -0500 Subject: [PATCH 8/8] feat(DrawAuctionExecutor): add fork tests --- .gitmodules | 3 + lcov.info | 290 +++++++-------- lib/optimism | 1 + lib/v5-prize-pool | 2 +- remappings.txt | 1 + src/DrawAuctionDispatcher.sol | 1 - src/DrawAuctionExecutor.sol | 9 +- src/libraries/RewardLib.sol | 1 - test/DrawAuction.t.sol | 18 +- ...tionDispatcherEthereumToOptimismFork.t.sol | 11 +- ...uctionExecutorEthereumToOptimismFork.t.sol | 340 ++++++++++++++++++ test/interfaces/IL2CrossDomainMessenger.sol | 42 +++ test/interfaces/IMessageExecutor.sol | 56 +++ test/libraries/RewardLib.t.sol | 2 +- 14 files changed, 613 insertions(+), 164 deletions(-) create mode 160000 lib/optimism create mode 100644 test/fork/DrawAuctionExecutorEthereumToOptimismFork.t.sol create mode 100644 test/interfaces/IL2CrossDomainMessenger.sol create mode 100644 test/interfaces/IMessageExecutor.sol diff --git a/.gitmodules b/.gitmodules index 60a5d30..4909d3d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,3 +15,6 @@ [submodule "lib/v5-prize-pool"] path = lib/v5-prize-pool url = https://github.com/pooltogether/v5-prize-pool +[submodule "lib/optimism"] + path = lib/optimism + url = https://github.com/ethereum-optimism/optimism diff --git a/lcov.info b/lcov.info index 5c86259..03066f6 100644 --- a/lcov.info +++ b/lcov.info @@ -30,83 +30,84 @@ LH:17 end_of_record TN: SF:src/DrawAuctionDispatcher.sol -FN:106,DrawAuctionDispatcher.dispatcher -FN:114,DrawAuctionDispatcher.drawAuctionExecutor -FN:122,DrawAuctionDispatcher.toChainId -FN:133,DrawAuctionDispatcher.setDispatcher -FN:142,DrawAuctionDispatcher.setDrawAuctionExecutor -FN:155,DrawAuctionDispatcher._afterAuctionEnds -FN:185,DrawAuctionDispatcher._setDispatcher -FN:195,DrawAuctionDispatcher._setDrawAuctionExecutor +FN:107,DrawAuctionDispatcher.dispatcher +FN:115,DrawAuctionDispatcher.drawAuctionExecutor +FN:123,DrawAuctionDispatcher.toChainId +FN:134,DrawAuctionDispatcher.setDispatcher +FN:143,DrawAuctionDispatcher.setDrawAuctionExecutor +FN:156,DrawAuctionDispatcher._afterAuctionEnds +FN:186,DrawAuctionDispatcher._setDispatcher +FN:196,DrawAuctionDispatcher._setDrawAuctionExecutor FNDA:3,DrawAuctionDispatcher.dispatcher FNDA:3,DrawAuctionDispatcher.drawAuctionExecutor FNDA:2,DrawAuctionDispatcher.toChainId FNDA:5,DrawAuctionDispatcher.setDispatcher -FNDA:9,DrawAuctionDispatcher.setDrawAuctionExecutor -FNDA:1,DrawAuctionDispatcher._afterAuctionEnds +FNDA:13,DrawAuctionDispatcher.setDrawAuctionExecutor +FNDA:3,DrawAuctionDispatcher._afterAuctionEnds FNDA:3,DrawAuctionDispatcher._setDispatcher -FNDA:9,DrawAuctionDispatcher._setDrawAuctionExecutor +FNDA:13,DrawAuctionDispatcher._setDrawAuctionExecutor FNF:8 FNH:8 -DA:107,3 -DA:115,3 -DA:123,2 -DA:134,3 -DA:143,9 -DA:159,1 -DA:170,1 -DA:186,3 -DA:187,1 +DA:108,3 +DA:116,3 +DA:124,2 +DA:135,3 +DA:144,13 +DA:160,3 +DA:171,3 +DA:187,3 DA:188,1 -DA:196,9 -DA:197,9 -DA:198,9 +DA:189,1 +DA:197,13 +DA:198,13 +DA:199,13 LF:13 LH:13 end_of_record TN: SF:src/DrawAuctionExecutor.sol -FN:125,DrawAuctionExecutor.originChainId -FN:133,DrawAuctionExecutor.drawAuctionDispatcher -FN:141,DrawAuctionExecutor.prizePool -FN:152,DrawAuctionExecutor.setDrawAuctionDispatcher -FN:167,DrawAuctionExecutor._checkSender -FN:87,DrawAuctionExecutor.completeAuction -FNDA:0,DrawAuctionExecutor.originChainId -FNDA:0,DrawAuctionExecutor.drawAuctionDispatcher -FNDA:0,DrawAuctionExecutor.prizePool -FNDA:8,DrawAuctionExecutor.setDrawAuctionDispatcher -FNDA:0,DrawAuctionExecutor._checkSender -FNDA:0,DrawAuctionExecutor.completeAuction +FN:132,DrawAuctionExecutor.originChainId +FN:140,DrawAuctionExecutor.drawAuctionDispatcher +FN:148,DrawAuctionExecutor.prizePool +FN:159,DrawAuctionExecutor.setDrawAuctionDispatcher +FN:176,DrawAuctionExecutor._checkSender +FN:94,DrawAuctionExecutor.completeAuction +FNDA:1,DrawAuctionExecutor.originChainId +FNDA:2,DrawAuctionExecutor.drawAuctionDispatcher +FNDA:1,DrawAuctionExecutor.prizePool +FNDA:15,DrawAuctionExecutor.setDrawAuctionDispatcher +FNDA:2,DrawAuctionExecutor._checkSender +FNDA:2,DrawAuctionExecutor.completeAuction FNF:6 -FNH:1 -DA:92,0 -DA:94,0 -DA:96,0 -DA:98,0 -DA:99,0 -DA:101,0 -DA:102,0 -DA:104,0 -DA:105,0 -DA:108,0 -DA:109,0 -DA:110,0 -DA:112,0 -DA:113,0 -DA:114,0 -DA:116,0 -DA:126,0 -DA:134,0 -DA:142,0 -DA:153,8 -DA:154,8 -DA:156,8 -DA:168,0 -DA:169,0 -DA:170,0 -LF:25 -LH:3 +FNH:6 +DA:99,2 +DA:101,2 +DA:103,2 +DA:105,2 +DA:106,2 +DA:108,2 +DA:109,1 +DA:111,1 +DA:112,1 +DA:115,2 +DA:116,2 +DA:117,2 +DA:119,2 +DA:120,2 +DA:121,2 +DA:123,2 +DA:133,1 +DA:141,2 +DA:149,1 +DA:160,15 +DA:161,14 +DA:163,13 +DA:165,13 +DA:177,2 +DA:178,2 +DA:179,2 +LF:26 +LH:26 end_of_record TN: SF:src/RNGRequestor.sol @@ -132,8 +133,8 @@ FN:342,RNGRequestor._isRNGCompleted FN:350,RNGRequestor._isRNGTimedOut FN:362,RNGRequestor._setRNGService FN:372,RNGRequestor._setRNGTimeout -FNDA:18,RNGRequestor.startRNGRequest -FNDA:7,RNGRequestor.completeRNGRequest +FNDA:20,RNGRequestor.startRNGRequest +FNDA:9,RNGRequestor.completeRNGRequest FNDA:2,RNGRequestor.cancelRNGRequest FNDA:2,RNGRequestor.isRNGRequested FNDA:2,RNGRequestor.isRNGCompleted @@ -148,28 +149,28 @@ FNDA:2,RNGRequestor.setRNGService FNDA:2,RNGRequestor.setRNGTimeout FNDA:11,RNGRequestor._afterRNGStart FNDA:1,RNGRequestor._afterRNGComplete -FNDA:20,RNGRequestor._currentTime -FNDA:31,RNGRequestor._isRNGRequested -FNDA:9,RNGRequestor._isRNGCompleted +FNDA:22,RNGRequestor._currentTime +FNDA:35,RNGRequestor._isRNGRequested +FNDA:11,RNGRequestor._isRNGCompleted FNDA:4,RNGRequestor._isRNGTimedOut FNDA:2,RNGRequestor._setRNGService FNDA:2,RNGRequestor._setRNGTimeout FNF:22 FNH:22 -DA:164,17 -DA:166,17 +DA:164,19 +DA:166,19 DA:167,1 -DA:170,17 -DA:171,17 -DA:172,17 -DA:173,17 -DA:175,17 -DA:177,17 -DA:186,5 -DA:187,5 -DA:189,5 -DA:191,5 -DA:193,5 +DA:170,19 +DA:171,19 +DA:172,19 +DA:173,19 +DA:175,19 +DA:177,19 +DA:186,7 +DA:187,7 +DA:189,7 +DA:191,7 +DA:193,7 DA:198,2 DA:200,1 DA:201,1 @@ -186,9 +187,9 @@ DA:274,3 DA:282,3 DA:294,2 DA:304,2 -DA:327,20 -DA:335,31 -DA:343,9 +DA:327,22 +DA:335,35 +DA:343,11 DA:351,4 DA:352,1 DA:354,3 @@ -209,20 +210,20 @@ FN:61,ExecutorAware._fromChainId FN:75,ExecutorAware._msgSender FNDA:0,ExecutorAware.isTrustedExecutor FNDA:0,ExecutorAware._messageId -FNDA:0,ExecutorAware._fromChainId -FNDA:0,ExecutorAware._msgSender +FNDA:2,ExecutorAware._fromChainId +FNDA:2,ExecutorAware._msgSender FNF:4 -FNH:0 -DA:38,0 +FNH:2 +DA:38,4 DA:50,0 DA:52,0 -DA:64,0 -DA:66,0 -DA:76,0 -DA:78,0 -DA:80,0 +DA:64,2 +DA:66,2 +DA:76,2 +DA:78,2 +DA:80,2 LF:8 -LH:0 +LH:6 end_of_record TN: SF:src/auctions/Auction.sol @@ -233,24 +234,24 @@ FN:137,Auction._getPhase FN:151,Auction._setPhase FN:87,Auction.auctionDuration FN:95,Auction.getPhases -FNDA:2,Auction.getPhase +FNDA:6,Auction.getPhase FNDA:1,Auction._afterAuctionEnds FNDA:1,Auction._getPhases -FNDA:12,Auction._getPhase -FNDA:16,Auction._setPhase +FNDA:20,Auction._getPhase +FNDA:20,Auction._setPhase FNDA:2,Auction.auctionDuration FNDA:1,Auction.getPhases FNF:7 FNH:7 DA:88,2 DA:96,1 -DA:105,2 +DA:105,6 DA:129,1 -DA:138,12 -DA:157,16 -DA:164,16 -DA:166,16 -DA:168,16 +DA:138,20 +DA:157,20 +DA:164,20 +DA:166,20 +DA:168,20 LF:9 LH:9 end_of_record @@ -258,54 +259,59 @@ TN: SF:src/auctions/TwoStepsAuction.sol FN:37,TwoStepsAuction._afterRNGStart FN:47,TwoStepsAuction._afterRNGComplete -FNDA:7,TwoStepsAuction._afterRNGStart -FNDA:5,TwoStepsAuction._afterRNGComplete +FNDA:9,TwoStepsAuction._afterRNGStart +FNDA:7,TwoStepsAuction._afterRNGComplete FNF:2 FNH:2 -DA:38,7 -DA:39,7 -DA:48,5 -DA:54,5 -DA:56,5 -DA:57,5 -DA:58,5 -DA:60,5 +DA:38,9 +DA:39,9 +DA:48,7 +DA:54,7 +DA:56,7 +DA:57,7 +DA:58,7 +DA:60,7 LF:8 LH:8 end_of_record TN: SF:src/libraries/RewardLib.sol -FN:22,RewardLib.rewards -FN:53,RewardLib.reward -FN:81,RewardLib._reward -FNDA:3,RewardLib.rewards +FN:23,RewardLib.rewards +FN:54,RewardLib.reward +FN:82,RewardLib._reward +FNDA:5,RewardLib.rewards FNDA:22,RewardLib.reward -FNDA:28,RewardLib._reward +FNDA:32,RewardLib._reward FNF:3 FNH:3 -DA:27,3 -DA:28,3 -DA:30,3 -DA:32,3 -DA:33,3 -DA:35,3 -DA:36,6 -DA:39,3 -DA:58,22 +DA:28,5 +DA:29,5 +DA:30,5 +DA:31,5 +DA:33,5 +DA:34,5 +DA:36,5 +DA:37,10 +DA:40,5 DA:59,22 -DA:61,22 -DA:63,22 -DA:89,28 -DA:90,5 -DA:95,23 -DA:96,14 -DA:101,23 -DA:102,2 -DA:106,21 -DA:107,4 -DA:113,21 -DA:114,3 -DA:117,21 -LF:23 -LH:23 +DA:60,22 +DA:62,22 +DA:64,22 +DA:90,32 +DA:91,5 +DA:96,27 +DA:97,16 +DA:100,27 +DA:101,27 +DA:102,27 +DA:103,27 +DA:107,27 +DA:108,2 +DA:112,25 +DA:113,4 +DA:119,25 +DA:120,5 +DA:123,25 +LF:28 +LH:28 end_of_record diff --git a/lib/optimism b/lib/optimism new file mode 160000 index 0000000..13c710c --- /dev/null +++ b/lib/optimism @@ -0,0 +1 @@ +Subproject commit 13c710c7c8e39622215e8718d2c2c41f8f70d90b diff --git a/lib/v5-prize-pool b/lib/v5-prize-pool index 773b1b0..9c9fd20 160000 --- a/lib/v5-prize-pool +++ b/lib/v5-prize-pool @@ -1 +1 @@ -Subproject commit 773b1b0d36efa9eb9c06826783996cb5beefd17f +Subproject commit 9c9fd20b6648fe2bb38ee1587e1886ed80f89b6f diff --git a/remappings.txt b/remappings.txt index ecfce42..19e7eb3 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,6 +1,7 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ openzeppelin=lib/openzeppelin-contracts/contracts/ +optimism/=lib/optimism/packages/contracts-bedrock/contracts/ owner-manager/=lib/owner-manager-contracts/contracts/ rng/=lib/pooltogether-rng-contracts/contracts/ diff --git a/src/DrawAuctionDispatcher.sol b/src/DrawAuctionDispatcher.sol index 99ca191..0a42fb4 100644 --- a/src/DrawAuctionDispatcher.sol +++ b/src/DrawAuctionDispatcher.sol @@ -7,7 +7,6 @@ import { Auction, AuctionLib } from "src/auctions/Auction.sol"; import { TwoStepsAuction, RNGInterface } from "src/auctions/TwoStepsAuction.sol"; import { ISingleMessageDispatcher } from "src/interfaces/ISingleMessageDispatcher.sol"; import { RewardLib } from "src/libraries/RewardLib.sol"; -import { console2 } from "forge-std/Test.sol"; /** * @title PoolTogether V5 DrawAuctionDispatcher diff --git a/src/DrawAuctionExecutor.sol b/src/DrawAuctionExecutor.sol index da93692..fde0c3b 100644 --- a/src/DrawAuctionExecutor.sol +++ b/src/DrawAuctionExecutor.sol @@ -6,7 +6,6 @@ import { PrizePool } from "v5-prize-pool/PrizePool.sol"; import { ExecutorAware } from "src/abstract/ExecutorAware.sol"; import { AuctionLib } from "src/auctions/Auction.sol"; import { RewardLib } from "src/libraries/RewardLib.sol"; -import { console2 } from "forge-std/Test.sol"; contract DrawAuctionExecutor is ExecutorAware { /* ============ Events ============ */ @@ -23,6 +22,12 @@ contract DrawAuctionExecutor is ExecutorAware { uint256[] rewardAmounts ); + /** + * @notice Emitted when the DrawAuctionDispatcher has been set. + * @param drawAuctionDispatcher Address of the DrawAuctionDispatcher + */ + event DrawAuctionDispatcherSet(address drawAuctionDispatcher); + /* ============ Custom Errors ============ */ /// @notice Thrown when the originChainId passed to the constructor is zero. @@ -155,6 +160,8 @@ contract DrawAuctionExecutor is ExecutorAware { if (drawAuctionDispatcher_ == address(0)) revert DrawAuctionDispatcherZeroAddress(); _drawAuctionDispatcher = drawAuctionDispatcher_; + + emit DrawAuctionDispatcherSet(drawAuctionDispatcher_); } /* ============ Internal Functions ============ */ diff --git a/src/libraries/RewardLib.sol b/src/libraries/RewardLib.sol index f902fb7..c765069 100644 --- a/src/libraries/RewardLib.sol +++ b/src/libraries/RewardLib.sol @@ -26,7 +26,6 @@ library RewardLib { ) internal view returns (uint256[] memory) { uint64 _auctionStart = _prizePool.nextDrawEndsAt(); uint64 _auctionEnd = _auctionStart + _auctionDuration; - uint256 _reserve = _prizePool.reserve() + _prizePool.reserveForNextDraw(); uint256 _phasesLength = _phases.length; diff --git a/test/DrawAuction.t.sol b/test/DrawAuction.t.sol index 6775991..b948445 100644 --- a/test/DrawAuction.t.sol +++ b/test/DrawAuction.t.sol @@ -40,7 +40,7 @@ contract DrawAuctionTest is Helpers { prizeToken: prizeToken, twabController: TwabController(address(0)), drawManager: address(0), - grandPrizePeriodDraws: uint32(365), + grandPrizePeriodDraws: uint16(365), drawPeriodSeconds: drawPeriodSeconds, firstDrawStartsAt: uint64(block.timestamp), numberOfTiers: uint8(3), // minimum number of tiers @@ -207,9 +207,10 @@ contract DrawAuctionTest is Helpers { function testAfterRNGComplete() public { uint256 _reserveAmount = 200e18; + uint256 _reserveAmountForNextDraw = _reserveAmount * 220; // Reserve amount for next draw will be 200e18 - prizeToken.mint(address(prizePool), _reserveAmount * 110); - prizePool.contributePrizeTokens(address(2), _reserveAmount * 110); + prizeToken.mint(address(prizePool), _reserveAmountForNextDraw); + prizePool.contributePrizeTokens(address(2), _reserveAmountForNextDraw); vm.warp(drawPeriodSeconds + auctionDuration / 2); @@ -226,14 +227,15 @@ contract DrawAuctionTest is Helpers { drawAuction.completeRNGRequest(recipient); - assertEq(prizeToken.balanceOf(recipient), _reserveAmount / 2); + assertEq(prizeToken.balanceOf(recipient), _reserveAmount); } function testAfterRNGCompleteDifferentRecipient() public { uint256 _reserveAmount = 200e18; + uint256 _reserveAmountForNextDraw = _reserveAmount * 220; // Reserve amount for next draw will be 200e18 - prizeToken.mint(address(prizePool), _reserveAmount * 110); - prizePool.contributePrizeTokens(address(2), _reserveAmount * 110); + prizeToken.mint(address(prizePool), _reserveAmountForNextDraw); + prizePool.contributePrizeTokens(address(2), _reserveAmountForNextDraw); vm.warp(drawPeriodSeconds + auctionDuration / 2); @@ -251,7 +253,7 @@ contract DrawAuctionTest is Helpers { drawAuction.completeRNGRequest(recipient); - assertEq(prizeToken.balanceOf(recipient), _reserveAmount / 4); - assertEq(prizeToken.balanceOf(_secondRecipient), _reserveAmount / 4); + assertEq(prizeToken.balanceOf(recipient), _reserveAmount / 2); + assertEq(prizeToken.balanceOf(_secondRecipient), _reserveAmount / 2); } } diff --git a/test/fork/DrawAuctionDispatcherEthereumToOptimismFork.t.sol b/test/fork/DrawAuctionDispatcherEthereumToOptimismFork.t.sol index 3a2b596..00ca112 100644 --- a/test/fork/DrawAuctionDispatcherEthereumToOptimismFork.t.sol +++ b/test/fork/DrawAuctionDispatcherEthereumToOptimismFork.t.sol @@ -8,8 +8,8 @@ import { UD2x18, SD1x18, ConstructorParams, PrizePool, TieredLiquidityDistributo import { DrawAuctionDispatcher, ISingleMessageDispatcher } from "src/DrawAuctionDispatcher.sol"; import { DrawAuctionExecutor } from "src/DrawAuctionExecutor.sol"; import { AuctionLib } from "src/libraries/AuctionLib.sol"; + import { Helpers, RNGInterface } from "test/helpers/Helpers.t.sol"; -import { console2 } from "forge-std/Test.sol"; contract DrawAuctionDispatcherEthereumToOptimismForkTest is Helpers { /* ============ Events ============ */ @@ -97,7 +97,7 @@ contract DrawAuctionDispatcherEthereumToOptimismForkTest is Helpers { prizeToken: prizeToken, twabController: TwabController(address(0)), drawManager: address(0), - grandPrizePeriodDraws: uint32(365), + grandPrizePeriodDraws: uint16(365), drawPeriodSeconds: drawPeriodSeconds, firstDrawStartsAt: uint64(block.timestamp), numberOfTiers: uint8(3), // minimum number of tiers @@ -144,13 +144,6 @@ contract DrawAuctionDispatcherEthereumToOptimismForkTest is Helpers { deployAll(); setAll(); - uint256 _reserveAmount = 200e18; - - vm.selectFork(optimismFork); - - prizeToken.mint(address(prizePool), _reserveAmount * 110); - prizePool.contributePrizeTokens(address(2), _reserveAmount * 110); - vm.selectFork(mainnetFork); vm.warp(drawPeriodSeconds + auctionDuration / 2); diff --git a/test/fork/DrawAuctionExecutorEthereumToOptimismFork.t.sol b/test/fork/DrawAuctionExecutorEthereumToOptimismFork.t.sol new file mode 100644 index 0000000..6151726 --- /dev/null +++ b/test/fork/DrawAuctionExecutorEthereumToOptimismFork.t.sol @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import { ERC20Mock } from "openzeppelin/mocks/ERC20Mock.sol"; +import { AddressAliasHelper } from "optimism/vendor/AddressAliasHelper.sol"; + +import { UD2x18, SD1x18, ConstructorParams, PrizePool, TieredLiquidityDistributor, TwabController } from "v5-prize-pool/PrizePool.sol"; + +import { DrawAuctionDispatcher, ISingleMessageDispatcher } from "src/DrawAuctionDispatcher.sol"; +import { DrawAuctionExecutor } from "src/DrawAuctionExecutor.sol"; +import { AuctionLib } from "src/libraries/AuctionLib.sol"; + +import { Helpers, RNGInterface } from "test/helpers/Helpers.t.sol"; +import { IMessageExecutor } from "test/interfaces/IMessageExecutor.sol"; +import { IL2CrossDomainMessenger } from "test/interfaces/IL2CrossDomainMessenger.sol"; + +contract DrawAuctionExecutorEthereumToOptimismForkTest is Helpers { + /* ============ Events ============ */ + + event AuctionRewardsDistributed( + uint8[] phaseIds, + address[] rewardRecipients, + uint256[] rewardAmounts + ); + + event DrawAuctionDispatcherSet(address drawAuctionDispatcher); + event MessageIdExecuted(uint256 indexed fromChainId, bytes32 indexed messageId); + event WithdrawReserve(address indexed to, uint256 amount); + + /* ============ Variables ============ */ + + uint256 public mainnetFork; + uint256 public optimismFork; + + address public proxyOVML1CrossDomainMessenger = 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1; + address public l2CrossDomainMessenger = 0x4200000000000000000000000000000000000007; + + uint256 public nonce = 1; + uint256 public toChainId = 10; + uint256 public fromChainId = 1; + + ISingleMessageDispatcher public dispatcher = + ISingleMessageDispatcher(address(0xa8f85bAB964D7e6bE938B54Bf4b29A247A88CD9d)); + address public executor = 0x890a87E71E731342a6d10e7628bd1F0733ce3296; + + DrawAuctionDispatcher public drawAuctionDispatcher; + DrawAuctionExecutor public drawAuctionExecutor; + + ERC20Mock public prizeToken; + PrizePool public prizePool; + RNGInterface public rng = RNGInterface(address(1)); + + uint32 public auctionDuration = 3 hours; + uint32 public rngTimeOut = 1 hours; + uint32 public drawPeriodSeconds = 1 days; + uint256 public randomNumber = 123456789; + address public mainRecipient = address(this); + address public secondRecipient = address(3); + + /* ============ Setup ============ */ + + function setUp() public { + mainnetFork = vm.createFork(vm.rpcUrl("mainnet")); + optimismFork = vm.createFork(vm.rpcUrl("optimism")); + } + + function deployDrawAuctionDispatcher() public { + vm.selectFork(mainnetFork); + + drawAuctionDispatcher = new DrawAuctionDispatcher( + dispatcher, + toChainId, + rng, + rngTimeOut, + 2, + auctionDuration, + address(this) + ); + + vm.makePersistent(address(drawAuctionDispatcher)); + } + + function deployDrawAuctionExecutor() public { + vm.selectFork(optimismFork); + + drawAuctionExecutor = new DrawAuctionExecutor(fromChainId, executor, prizePool); + + vm.makePersistent(address(drawAuctionExecutor)); + } + + function deployPrizePool() public { + vm.selectFork(optimismFork); + + prizeToken = new ERC20Mock(); + + prizePool = new PrizePool( + ConstructorParams({ + prizeToken: prizeToken, + twabController: TwabController(address(0)), + drawManager: address(0), + grandPrizePeriodDraws: uint16(365), + drawPeriodSeconds: drawPeriodSeconds, + firstDrawStartsAt: uint64(block.timestamp), + numberOfTiers: uint8(3), // minimum number of tiers + tierShares: 100, + canaryShares: 10, + reserveShares: 10, + claimExpansionThreshold: UD2x18.wrap(0.9e18), // claim threshold of 90% + smoothing: SD1x18.wrap(0.9e18) // alpha + }) + ); + + vm.makePersistent(address(prizePool)); + } + + function deployAll() public { + deployDrawAuctionDispatcher(); + deployPrizePool(); + deployDrawAuctionExecutor(); + } + + function setDrawAuctionExecutor() public { + vm.selectFork(mainnetFork); + drawAuctionDispatcher.setDrawAuctionExecutor(address(drawAuctionExecutor)); + } + + function setDrawAuctionDispatcher() public { + vm.selectFork(optimismFork); + drawAuctionExecutor.setDrawAuctionDispatcher(address(drawAuctionDispatcher)); + } + + function setPrizePoolDrawManager() public { + vm.selectFork(optimismFork); + prizePool.setDrawManager(address(drawAuctionExecutor)); + } + + function setAll() public { + setDrawAuctionExecutor(); + setDrawAuctionDispatcher(); + setPrizePoolDrawManager(); + } + + /* ============ Auction Execution ============ */ + + function testCompleteAuctionSingleRecipient() public { + deployAll(); + setAll(); + + uint256 _reserveAmount = 200e18; + uint256 _reserveAmountForNextDraw = _reserveAmount * 220; // Reserve amount for next draw will be 200e18 + + vm.selectFork(optimismFork); + + prizeToken.mint(address(prizePool), _reserveAmountForNextDraw); + prizePool.contributePrizeTokens(address(2), _reserveAmountForNextDraw); + + vm.selectFork(mainnetFork); + + vm.warp(block.timestamp + drawPeriodSeconds + auctionDuration / 2); + + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); + + drawAuctionDispatcher.startRNGRequest(mainRecipient); + + vm.warp(block.timestamp + drawPeriodSeconds + auctionDuration); + + _mockCompleteRNGRequest(address(rng), _requestId, randomNumber); + + drawAuctionDispatcher.completeRNGRequest(mainRecipient); + + AuctionLib.Phase[] memory _auctionPhases = new AuctionLib.Phase[](2); + _auctionPhases[0] = drawAuctionDispatcher.getPhase(0); + _auctionPhases[1] = drawAuctionDispatcher.getPhase(1); + + vm.selectFork(optimismFork); + + vm.warp(block.timestamp + drawPeriodSeconds + auctionDuration); + + address _to = address(drawAuctionExecutor); + bytes memory _data = abi.encodeCall( + DrawAuctionExecutor.completeAuction, + (_auctionPhases, auctionDuration, randomNumber) + ); + + IL2CrossDomainMessenger l2Bridge = IL2CrossDomainMessenger(l2CrossDomainMessenger); + + address _l1CrossDomainMessengerAlias = AddressAliasHelper.applyL1ToL2Alias( + proxyOVML1CrossDomainMessenger + ); + + vm.startPrank(_l1CrossDomainMessengerAlias); + + bytes32 _expectedMessageId = keccak256( + abi.encode(nonce, address(drawAuctionDispatcher), _to, _data) + ); + + vm.expectEmit(address(executor)); + emit MessageIdExecuted(fromChainId, _expectedMessageId); + + l2Bridge.relayMessage( + l2Bridge.messageNonce() + 1, + address(dispatcher), + address(executor), + 0, + 500_000, + abi.encodeCall( + IMessageExecutor.executeMessage, + (_to, _data, _expectedMessageId, fromChainId, address(drawAuctionDispatcher)) + ) + ); + + // We use assertApproxEqAbs cause we lose 1 wei in precision, probably due to a small drift in timestamp + assertApproxEqAbs(prizeToken.balanceOf(mainRecipient), _reserveAmount, 1); + } + + function testCompleteAuctionMultipleRecipients() public { + deployAll(); + setAll(); + + uint256 _reserveAmount = 200e18; + uint256 _reserveAmountForNextDraw = _reserveAmount * 220; // Reserve amount for next draw will be 200e18 + + vm.selectFork(optimismFork); + + prizeToken.mint(address(prizePool), _reserveAmountForNextDraw); + prizePool.contributePrizeTokens(address(2), _reserveAmountForNextDraw); + + vm.selectFork(mainnetFork); + + vm.warp(block.timestamp + drawPeriodSeconds + auctionDuration / 2); + + uint32 _requestId = uint32(1); + uint32 _lockBlock = uint32(block.number); + + _mockStartRNGRequest(address(rng), address(0), 0, _requestId, _lockBlock); + + drawAuctionDispatcher.startRNGRequest(mainRecipient); + + vm.warp(block.timestamp + drawPeriodSeconds + auctionDuration); + + _mockCompleteRNGRequest(address(rng), _requestId, randomNumber); + + drawAuctionDispatcher.completeRNGRequest(secondRecipient); + + AuctionLib.Phase[] memory _auctionPhases = new AuctionLib.Phase[](2); + _auctionPhases[0] = drawAuctionDispatcher.getPhase(0); + _auctionPhases[1] = drawAuctionDispatcher.getPhase(1); + + vm.selectFork(optimismFork); + + vm.warp(block.timestamp + drawPeriodSeconds + auctionDuration); + + address _to = address(drawAuctionExecutor); + bytes memory _data = abi.encodeCall( + DrawAuctionExecutor.completeAuction, + (_auctionPhases, auctionDuration, randomNumber) + ); + + IL2CrossDomainMessenger l2Bridge = IL2CrossDomainMessenger(l2CrossDomainMessenger); + + address _l1CrossDomainMessengerAlias = AddressAliasHelper.applyL1ToL2Alias( + proxyOVML1CrossDomainMessenger + ); + + vm.startPrank(_l1CrossDomainMessengerAlias); + + bytes32 _expectedMessageId = keccak256( + abi.encode(nonce, address(drawAuctionDispatcher), _to, _data) + ); + + vm.expectEmit(address(executor)); + emit MessageIdExecuted(fromChainId, _expectedMessageId); + + l2Bridge.relayMessage( + l2Bridge.messageNonce() + 1, + address(dispatcher), + address(executor), + 0, + 500_000, + abi.encodeCall( + IMessageExecutor.executeMessage, + (_to, _data, _expectedMessageId, fromChainId, address(drawAuctionDispatcher)) + ) + ); + + // We use assertApproxEqAbs cause we lose 1 wei in precision, probably due to a small drift in timestamp + assertApproxEqAbs( + prizeToken.balanceOf(mainRecipient) + prizeToken.balanceOf(secondRecipient), + _reserveAmount, + 1 + ); + } + + /* ============ Getters ============ */ + + function testGetters() public { + deployAll(); + setAll(); + + assertEq(drawAuctionExecutor.originChainId(), fromChainId); + assertEq(drawAuctionExecutor.drawAuctionDispatcher(), address(drawAuctionDispatcher)); + assertEq(address(drawAuctionExecutor.prizePool()), address(prizePool)); + } + + /* ============ Setters ============ */ + + /* ============ setDrawAuctionDispatcher ============ */ + function testSetDrawAuctionDispatcher() public { + deployAll(); + + vm.expectEmit(); + emit DrawAuctionDispatcherSet(address(drawAuctionDispatcher)); + + drawAuctionExecutor.setDrawAuctionDispatcher(address(drawAuctionDispatcher)); + + assertEq(drawAuctionExecutor.drawAuctionDispatcher(), address(drawAuctionDispatcher)); + } + + function testSetDrawAuctionDispatcherFailAlreadySet() public { + deployAll(); + setAll(); + + vm.expectRevert( + abi.encodeWithSelector(DrawAuctionExecutor.DrawAuctionDispatcherAlreadySet.selector) + ); + drawAuctionExecutor.setDrawAuctionDispatcher(address(drawAuctionDispatcher)); + } + + function testSetDrawAuctionDispatcherFailAddressZero() public { + deployAll(); + + vm.expectRevert( + abi.encodeWithSelector(DrawAuctionExecutor.DrawAuctionDispatcherZeroAddress.selector) + ); + drawAuctionExecutor.setDrawAuctionDispatcher(address(0)); + } +} diff --git a/test/interfaces/IL2CrossDomainMessenger.sol b/test/interfaces/IL2CrossDomainMessenger.sol new file mode 100644 index 0000000..42572dc --- /dev/null +++ b/test/interfaces/IL2CrossDomainMessenger.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +/** + * @custom:proxied + * @custom:predeploy 0x4200000000000000000000000000000000000007 + * @title L2CrossDomainMessenger + * @notice The L2CrossDomainMessenger is a high-level interface for message passing between L1 and + * L2 on the L2 side. Users are generally encouraged to use this contract instead of lower + * level message passing contracts. + */ +interface IL2CrossDomainMessenger { + /** + * @notice Relays a message that was sent by the other CrossDomainMessenger contract. Can only + * be executed via cross-chain call from the other messenger OR if the message was + * already received once and is currently being replayed. + * + * @param _nonce Nonce of the message being relayed. + * @param _sender Address of the user who sent the message. + * @param _target Address that the message is targeted at. + * @param _value ETH value to send with the message. + * @param _minGasLimit Minimum amount of gas that the message can be executed with. + * @param _message Message to send to the target. + */ + function relayMessage( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _minGasLimit, + bytes calldata _message + ) external payable; + + /** + * @notice Retrieves the next message nonce. Message version will be added to the upper two + * bytes of the message nonce. Message version allows us to treat messages as having + * different structures. + * + * @return Nonce of the next message to be sent, with added message version. + */ + function messageNonce() external view returns (uint256); +} diff --git a/test/interfaces/IMessageExecutor.sol b/test/interfaces/IMessageExecutor.sol new file mode 100644 index 0000000..71ce459 --- /dev/null +++ b/test/interfaces/IMessageExecutor.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +/** + * @title MessageExecutor interface + * @notice MessageExecutor interface of the ERC-5164 standard as defined in the EIP. + */ +interface IMessageExecutor { + struct Message { + address to; + bytes data; + } + + /** + * @notice Emitted when a message has successfully been executed. + * @param fromChainId ID of the chain that dispatched the message + * @param messageId ID uniquely identifying the message that was executed + */ + event MessageIdExecuted(uint256 indexed fromChainId, bytes32 indexed messageId); + + /** + * @notice Execute message from the origin chain. + * @dev Should authenticate that the call has been performed by the bridge transport layer. + * @dev Must revert if the message fails. + * @dev Must emit the `MessageIdExecuted` event once the message has been executed. + * @param to Address that will receive `data` + * @param data Data forwarded to address `to` + * @param messageId ID uniquely identifying the message + * @param fromChainId ID of the chain that dispatched the message + * @param from Address of the sender on the origin chain + */ + function executeMessage( + address to, + bytes calldata data, + bytes32 messageId, + uint256 fromChainId, + address from + ) external; + + /** + * @notice Execute a batch messages from the origin chain. + * @dev Should authenticate that the call has been performed by the bridge transport layer. + * @dev Must revert if one of the messages fails. + * @dev Must emit the `MessageIdExecuted` event once messages have been executed. + * @param messages Array of messages being executed + * @param messageId ID uniquely identifying the messages + * @param fromChainId ID of the chain that dispatched the messages + * @param from Address of the sender on the origin chain + */ + function executeMessageBatch( + Message[] calldata messages, + bytes32 messageId, + uint256 fromChainId, + address from + ) external; +} diff --git a/test/libraries/RewardLib.t.sol b/test/libraries/RewardLib.t.sol index ba4c5c5..7c952bc 100644 --- a/test/libraries/RewardLib.t.sol +++ b/test/libraries/RewardLib.t.sol @@ -27,7 +27,7 @@ contract RewardLibTest is Helpers { prizeToken: prizeToken, twabController: TwabController(address(0)), drawManager: address(0), - grandPrizePeriodDraws: uint32(365), + grandPrizePeriodDraws: uint16(365), drawPeriodSeconds: drawPeriodSeconds, firstDrawStartsAt: uint64(block.timestamp), numberOfTiers: uint8(3), // minimum number of tiers