// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.7.0; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/SafeCastUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/introspection/ERC165CheckerUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; import "@pooltogether/fixed-point/contracts/FixedPoint.sol"; import "../external/compound/ICompLike.sol"; import "../registry/RegistryInterface.sol"; import "../reserve/ReserveInterface.sol"; import "../token/TokenListenerInterface.sol"; import "../token/TokenListenerLibrary.sol"; import "../token/ControlledToken.sol"; import "../token/TokenControllerInterface.sol"; import "../utils/MappedSinglyLinkedList.sol"; import "./PrizePoolInterface.sol"; /// @title Escrows assets and deposits them into a yield source. Exposes interest to Prize Strategy. Users deposit and withdraw from this contract to participate in Prize Pool. /// @notice Accounting is managed using Controlled Tokens, whose mint and burn functions can only be called by this contract. /// @dev Must be inherited to provide specific yield-bearing asset control, such as Compound cTokens abstract contract PrizePool is PrizePoolInterface, OwnableUpgradeable, ReentrancyGuardUpgradeable, TokenControllerInterface { using SafeMathUpgradeable for uint256; using SafeCastUpgradeable for uint256; using SafeERC20Upgradeable for IERC20Upgradeable; using MappedSinglyLinkedList for MappedSinglyLinkedList.Mapping; using ERC165CheckerUpgradeable for address; /// @dev Emitted when an instance is initialized event Initialized( address reserveRegistry, uint256 maxExitFeeMantissa, uint256 maxTimelockDuration ); /// @dev Event emitted when controlled token is added event ControlledTokenAdded( ControlledTokenInterface indexed token ); /// @dev Emitted when reserve is captured. event ReserveFeeCaptured( uint256 amount ); event AwardCaptured( uint256 amount ); /// @dev Event emitted when assets are deposited event Deposited( address indexed operator, address indexed to, address indexed token, uint256 amount, address referrer ); /// @dev Event emitted when timelocked funds are re-deposited event TimelockDeposited( address indexed operator, address indexed to, address indexed token, uint256 amount ); /// @dev Event emitted when interest is awarded to a winner event Awarded( address indexed winner, address indexed token, uint256 amount ); /// @dev Event emitted when external ERC20s are awarded to a winner event AwardedExternalERC20( address indexed winner, address indexed token, uint256 amount ); /// @dev Event emitted when external ERC20s are transferred out event TransferredExternalERC20( address indexed to, address indexed token, uint256 amount ); /// @dev Event emitted when external ERC721s are awarded to a winner event AwardedExternalERC721( address indexed winner, address indexed token, uint256[] tokenIds ); /// @dev Event emitted when assets are withdrawn instantly event InstantWithdrawal( address indexed operator, address indexed from, address indexed token, uint256 amount, uint256 redeemed, uint256 exitFee ); /// @dev Event emitted upon a withdrawal with timelock event TimelockedWithdrawal( address indexed operator, address indexed from, address indexed token, uint256 amount, uint256 unlockTimestamp ); event ReserveWithdrawal( address indexed to, uint256 amount ); /// @dev Event emitted when timelocked funds are swept back to a user event TimelockedWithdrawalSwept( address indexed operator, address indexed from, uint256 amount, uint256 redeemed ); /// @dev Event emitted when the Liquidity Cap is set event LiquidityCapSet( uint256 liquidityCap ); /// @dev Event emitted when the Credit plan is set event CreditPlanSet( address token, uint128 creditLimitMantissa, uint128 creditRateMantissa ); /// @dev Event emitted when the Prize Strategy is set event PrizeStrategySet( address indexed prizeStrategy ); /// @dev Emitted when credit is minted event CreditMinted( address indexed user, address indexed token, uint256 amount ); /// @dev Emitted when credit is burned event CreditBurned( address indexed user, address indexed token, uint256 amount ); struct CreditPlan { uint128 creditLimitMantissa; uint128 creditRateMantissa; } struct CreditBalance { uint192 balance; uint32 timestamp; bool initialized; } /// @dev Reserve to which reserve fees are sent RegistryInterface public reserveRegistry; /// @dev A linked list of all the controlled tokens MappedSinglyLinkedList.Mapping internal _tokens; /// @dev The Prize Strategy that this Prize Pool is bound to. TokenListenerInterface public prizeStrategy; /// @dev The maximum possible exit fee fraction as a fixed point 18 number. /// For example, if the maxExitFeeMantissa is "0.1 ether", then the maximum exit fee for a withdrawal of 100 Dai will be 10 Dai uint256 public maxExitFeeMantissa; /// @dev The maximum possible timelock duration for a timelocked withdrawal (in seconds). uint256 public maxTimelockDuration; /// @dev The total funds that are timelocked. uint256 public timelockTotalSupply; /// @dev The total funds that have been allocated to the reserve uint256 public reserveTotalSupply; /// @dev The total amount of funds that the prize pool can hold. uint256 public liquidityCap; /// @dev the The awardable balance uint256 internal _currentAwardBalance; /// @dev The timelocked balances for each user mapping(address => uint256) internal _timelockBalances; /// @dev The unlock timestamps for each user mapping(address => uint256) internal _unlockTimestamps; /// @dev Stores the credit plan for each token. mapping(address => CreditPlan) internal _tokenCreditPlans; /// @dev Stores each users balance of credit per token. mapping(address => mapping(address => CreditBalance)) internal _tokenCreditBalances; /// @notice Initializes the Prize Pool /// @param _controlledTokens Array of ControlledTokens that are controlled by this Prize Pool. /// @param _maxExitFeeMantissa The maximum exit fee size /// @param _maxTimelockDuration The maximum length of time the withdraw timelock function initialize ( RegistryInterface _reserveRegistry, ControlledTokenInterface[] memory _controlledTokens, uint256 _maxExitFeeMantissa, uint256 _maxTimelockDuration ) public initializer { require(address(_reserveRegistry) != address(0), "PrizePool/reserveRegistry-not-zero"); _tokens.initialize(); for (uint256 i = 0; i < _controlledTokens.length; i++) { _addControlledToken(_controlledTokens[i]); } __Ownable_init(); __ReentrancyGuard_init(); _setLiquidityCap(uint256(-1)); reserveRegistry = _reserveRegistry; maxExitFeeMantissa = _maxExitFeeMantissa; maxTimelockDuration = _maxTimelockDuration; emit Initialized( address(_reserveRegistry), maxExitFeeMantissa, maxTimelockDuration ); } /// @dev Returns the address of the underlying ERC20 asset /// @return The address of the asset function token() external override view returns (address) { return address(_token()); } /// @dev Returns the total underlying balance of all assets. This includes both principal and interest. /// @return The underlying balance of assets function balance() external returns (uint256) { return _balance(); } /// @dev Checks with the Prize Pool if a specific token type may be awarded as an external prize /// @param _externalToken The address of the token to check /// @return True if the token may be awarded, false otherwise function canAwardExternal(address _externalToken) external view returns (bool) { return _canAwardExternal(_externalToken); } /// @notice Deposits timelocked tokens for a user back into the Prize Pool as another asset. /// @param to The address receiving the tokens /// @param amount The amount of timelocked assets to re-deposit /// @param controlledToken The type of token to be minted in exchange (i.e. tickets or sponsorship) function timelockDepositTo( address to, uint256 amount, address controlledToken ) external onlyControlledToken(controlledToken) canAddLiquidity(amount) nonReentrant { address operator = _msgSender(); _mint(to, amount, controlledToken, address(0)); _timelockBalances[operator] = _timelockBalances[operator].sub(amount); timelockTotalSupply = timelockTotalSupply.sub(amount); emit TimelockDeposited(operator, to, controlledToken, amount); } /// @notice Deposit assets into the Prize Pool in exchange for tokens /// @param to The address receiving the newly minted tokens /// @param amount The amount of assets to deposit /// @param controlledToken The address of the type of token the user is minting /// @param referrer The referrer of the deposit function depositTo( address to, uint256 amount, address controlledToken, address referrer ) external override onlyControlledToken(controlledToken) canAddLiquidity(amount) nonReentrant { address operator = _msgSender(); _mint(to, amount, controlledToken, referrer); _token().safeTransferFrom(operator, address(this), amount); _supply(amount); emit Deposited(operator, to, controlledToken, amount, referrer); } /// @notice Withdraw assets from the Prize Pool instantly. A fairness fee may be charged for an early exit. /// @param from The address to redeem tokens from. /// @param amount The amount of tokens to redeem for assets. /// @param controlledToken The address of the token to redeem (i.e. ticket or sponsorship) /// @param maximumExitFee The maximum exit fee the caller is willing to pay. This should be pre-calculated by the calculateExitFee() fxn. /// @return The actual exit fee paid function withdrawInstantlyFrom( address from, uint256 amount, address controlledToken, uint256 maximumExitFee ) external override nonReentrant onlyControlledToken(controlledToken) returns (uint256) { (uint256 exitFee, uint256 burnedCredit) = _calculateEarlyExitFeeLessBurnedCredit(from, controlledToken, amount); require(exitFee <= maximumExitFee, "PrizePool/exit-fee-exceeds-user-maximum"); // burn the credit _burnCredit(from, controlledToken, burnedCredit); // burn the tickets ControlledToken(controlledToken).controllerBurnFrom(_msgSender(), from, amount); // redeem the tickets less the fee uint256 amountLessFee = amount.sub(exitFee); uint256 redeemed = _redeem(amountLessFee); _token().safeTransfer(from, redeemed); emit InstantWithdrawal(_msgSender(), from, controlledToken, amount, redeemed, exitFee); return exitFee; } /// @notice Limits the exit fee to the maximum as hard-coded into the contract /// @param withdrawalAmount The amount that is attempting to be withdrawn /// @param exitFee The exit fee to check against the limit /// @return The passed exit fee if it is less than the maximum, otherwise the maximum fee is returned. function _limitExitFee(uint256 withdrawalAmount, uint256 exitFee) internal view returns (uint256) { uint256 maxFee = FixedPoint.multiplyUintByMantissa(withdrawalAmount, maxExitFeeMantissa); if (exitFee > maxFee) { exitFee = maxFee; } return exitFee; } /// @notice Withdraw assets from the Prize Pool by placing them into the timelock. /// The timelock is used to ensure that the tickets have contributed their fair share of the prize. /// @dev Note that if the user has previously timelocked funds then this contract will try to sweep them. /// If the existing timelocked funds are still locked, then the incoming /// balance is added to their existing balance and the new timelock unlock timestamp will overwrite the old one. /// @param from The address to withdraw from /// @param amount The amount to withdraw /// @param controlledToken The type of token being withdrawn /// @return The timestamp from which the funds can be swept function withdrawWithTimelockFrom( address from, uint256 amount, address controlledToken ) external override nonReentrant onlyControlledToken(controlledToken) returns (uint256) { uint256 blockTime = _currentTime(); (uint256 lockDuration, uint256 burnedCredit) = _calculateTimelockDuration(from, controlledToken, amount); uint256 unlockTimestamp = blockTime.add(lockDuration); _burnCredit(from, controlledToken, burnedCredit); ControlledToken(controlledToken).controllerBurnFrom(_msgSender(), from, amount); _mintTimelock(from, amount, unlockTimestamp); emit TimelockedWithdrawal(_msgSender(), from, controlledToken, amount, unlockTimestamp); // return the block at which the funds will be available return unlockTimestamp; } /// @notice Adds to a user's timelock balance. It will attempt to sweep before updating the balance. /// Note that this will overwrite the previous unlock timestamp. /// @param user The user whose timelock balance should increase /// @param amount The amount to increase by /// @param timestamp The new unlock timestamp function _mintTimelock(address user, uint256 amount, uint256 timestamp) internal { // Sweep the old balance, if any address[] memory users = new address[](1); users[0] = user; _sweepTimelockBalances(users); timelockTotalSupply = timelockTotalSupply.add(amount); _timelockBalances[user] = _timelockBalances[user].add(amount); _unlockTimestamps[user] = timestamp; // if the funds should already be unlocked if (timestamp <= _currentTime()) { _sweepTimelockBalances(users); } } /// @notice Updates the Prize Strategy when tokens are transferred between holders. /// @param from The address the tokens are being transferred from (0 if minting) /// @param to The address the tokens are being transferred to (0 if burning) /// @param amount The amount of tokens being trasferred function beforeTokenTransfer(address from, address to, uint256 amount) external override onlyControlledToken(msg.sender) { if (from != address(0)) { uint256 fromBeforeBalance = IERC20Upgradeable(msg.sender).balanceOf(from); // first accrue credit for their old balance uint256 newCreditBalance = _calculateCreditBalance(from, msg.sender, fromBeforeBalance, 0); if (from != to) { // if they are sending funds to someone else, we need to limit their accrued credit to their new balance newCreditBalance = _applyCreditLimit(msg.sender, fromBeforeBalance.sub(amount), newCreditBalance); } _updateCreditBalance(from, msg.sender, newCreditBalance); } if (to != address(0) && to != from) { _accrueCredit(to, msg.sender, IERC20Upgradeable(msg.sender).balanceOf(to), 0); } // if we aren't minting if (from != address(0) && address(prizeStrategy) != address(0)) { prizeStrategy.beforeTokenTransfer(from, to, amount, msg.sender); } } /// @notice Returns the balance that is available to award. /// @dev captureAwardBalance() should be called first /// @return The total amount of assets to be awarded for the current prize function awardBalance() external override view returns (uint256) { return _currentAwardBalance; } /// @notice Captures any available interest as award balance. /// @dev This function also captures the reserve fees. /// @return The total amount of assets to be awarded for the current prize function captureAwardBalance() external override nonReentrant returns (uint256) { uint256 tokenTotalSupply = _tokenTotalSupply(); // it's possible for the balance to be slightly less due to rounding errors in the underlying yield source uint256 currentBalance = _balance(); uint256 totalInterest = (currentBalance > tokenTotalSupply) ? currentBalance.sub(tokenTotalSupply) : 0; uint256 unaccountedPrizeBalance = (totalInterest > _currentAwardBalance) ? totalInterest.sub(_currentAwardBalance) : 0; if (unaccountedPrizeBalance > 0) { uint256 reserveFee = calculateReserveFee(unaccountedPrizeBalance); if (reserveFee > 0) { reserveTotalSupply = reserveTotalSupply.add(reserveFee); unaccountedPrizeBalance = unaccountedPrizeBalance.sub(reserveFee); emit ReserveFeeCaptured(reserveFee); } _currentAwardBalance = _currentAwardBalance.add(unaccountedPrizeBalance); emit AwardCaptured(unaccountedPrizeBalance); } return _currentAwardBalance; } function withdrawReserve(address to) external override onlyReserve returns (uint256) { uint256 amount = reserveTotalSupply; reserveTotalSupply = 0; uint256 redeemed = _redeem(amount); _token().safeTransfer(address(to), redeemed); emit ReserveWithdrawal(to, amount); return redeemed; } /// @notice Called by the prize strategy to award prizes. /// @dev The amount awarded must be less than the awardBalance() /// @param to The address of the winner that receives the award /// @param amount The amount of assets to be awarded /// @param controlledToken The address of the asset token being awarded function award( address to, uint256 amount, address controlledToken ) external override onlyPrizeStrategy onlyControlledToken(controlledToken) { if (amount == 0) { return; } require(amount <= _currentAwardBalance, "PrizePool/award-exceeds-avail"); _currentAwardBalance = _currentAwardBalance.sub(amount); _mint(to, amount, controlledToken, address(0)); uint256 extraCredit = _calculateEarlyExitFeeNoCredit(controlledToken, amount); _accrueCredit(to, controlledToken, IERC20Upgradeable(controlledToken).balanceOf(to), extraCredit); emit Awarded(to, controlledToken, amount); } /// @notice Called by the Prize-Strategy to transfer out external ERC20 tokens /// @dev Used to transfer out tokens held by the Prize Pool. Could be liquidated, or anything. /// @param to The address of the winner that receives the award /// @param amount The amount of external assets to be awarded /// @param externalToken The address of the external asset token being awarded function transferExternalERC20( address to, address externalToken, uint256 amount ) external override onlyPrizeStrategy { if (_transferOut(to, externalToken, amount)) { emit TransferredExternalERC20(to, externalToken, amount); } } /// @notice Called by the Prize-Strategy to award external ERC20 prizes /// @dev Used to award any arbitrary tokens held by the Prize Pool /// @param to The address of the winner that receives the award /// @param amount The amount of external assets to be awarded /// @param externalToken The address of the external asset token being awarded function awardExternalERC20( address to, address externalToken, uint256 amount ) external override onlyPrizeStrategy { if (_transferOut(to, externalToken, amount)) { emit AwardedExternalERC20(to, externalToken, amount); } } function _transferOut( address to, address externalToken, uint256 amount ) internal returns (bool) { require(_canAwardExternal(externalToken), "PrizePool/invalid-external-token"); if (amount == 0) { return false; } IERC20Upgradeable(externalToken).safeTransfer(to, amount); return true; } /// @notice Called to mint controlled tokens. Ensures that token listener callbacks are fired. /// @param to The user who is receiving the tokens /// @param amount The amount of tokens they are receiving /// @param controlledToken The token that is going to be minted /// @param referrer The user who referred the minting function _mint(address to, uint256 amount, address controlledToken, address referrer) internal { if (address(prizeStrategy) != address(0)) { prizeStrategy.beforeTokenMint(to, amount, controlledToken, referrer); } ControlledToken(controlledToken).controllerMint(to, amount); } /// @notice Called by the prize strategy to award external ERC721 prizes /// @dev Used to award any arbitrary NFTs held by the Prize Pool /// @param to The address of the winner that receives the award /// @param externalToken The address of the external NFT token being awarded /// @param tokenIds An array of NFT Token IDs to be transferred function awardExternalERC721( address to, address externalToken, uint256[] calldata tokenIds ) external override onlyPrizeStrategy { require(_canAwardExternal(externalToken), "PrizePool/invalid-external-token"); if (tokenIds.length == 0) { return; } for (uint256 i = 0; i < tokenIds.length; i++) { IERC721Upgradeable(externalToken).transferFrom(address(this), to, tokenIds[i]); } emit AwardedExternalERC721(to, externalToken, tokenIds); } /// @notice Calculates the reserve portion of the given amount of funds. If there is no reserve address, the portion will be zero. /// @param amount The prize amount /// @return The size of the reserve portion of the prize function calculateReserveFee(uint256 amount) public view returns (uint256) { ReserveInterface reserve = ReserveInterface(reserveRegistry.lookup()); if (address(reserve) == address(0)) { return 0; } uint256 reserveRateMantissa = reserve.reserveRateMantissa(address(this)); if (reserveRateMantissa == 0) { return 0; } return FixedPoint.multiplyUintByMantissa(amount, reserveRateMantissa); } /// @notice Sweep all timelocked balances and transfer unlocked assets to owner accounts /// @param users An array of account addresses to sweep balances for /// @return The total amount of assets swept from the Prize Pool function sweepTimelockBalances( address[] calldata users ) external override nonReentrant returns (uint256) { return _sweepTimelockBalances(users); } /// @notice Sweep available timelocked balances to their owners. The full balances will be swept to the owners. /// @param users An array of owner addresses /// @return The total amount of assets swept from the Prize Pool function _sweepTimelockBalances( address[] memory users ) internal returns (uint256) { address operator = _msgSender(); uint256[] memory balances = new uint256[](users.length); uint256 totalWithdrawal; uint256 i; for (i = 0; i < users.length; i++) { address user = users[i]; if (_unlockTimestamps[user] <= _currentTime()) { totalWithdrawal = totalWithdrawal.add(_timelockBalances[user]); balances[i] = _timelockBalances[user]; delete _timelockBalances[user]; } } // if there is nothing to do, just quit if (totalWithdrawal == 0) { return 0; } timelockTotalSupply = timelockTotalSupply.sub(totalWithdrawal); uint256 redeemed = _redeem(totalWithdrawal); IERC20Upgradeable underlyingToken = IERC20Upgradeable(_token()); for (i = 0; i < users.length; i++) { if (balances[i] > 0) { delete _unlockTimestamps[users[i]]; uint256 shareMantissa = FixedPoint.calculateMantissa(balances[i], totalWithdrawal); uint256 transferAmount = FixedPoint.multiplyUintByMantissa(redeemed, shareMantissa); underlyingToken.safeTransfer(users[i], transferAmount); emit TimelockedWithdrawalSwept(operator, users[i], balances[i], transferAmount); } } return totalWithdrawal; } /// @notice Calculates a timelocked withdrawal duration and credit consumption. /// @param from The user who is withdrawing /// @param amount The amount the user is withdrawing /// @param controlledToken The type of collateral the user is withdrawing (i.e. ticket or sponsorship) /// @return durationSeconds The duration of the timelock in seconds function calculateTimelockDuration( address from, address controlledToken, uint256 amount ) external override returns ( uint256 durationSeconds, uint256 burnedCredit ) { return _calculateTimelockDuration(from, controlledToken, amount); } /// @dev Calculates a timelocked withdrawal duration and credit consumption. /// @param from The user who is withdrawing /// @param amount The amount the user is withdrawing /// @param controlledToken The type of collateral the user is withdrawing (i.e. ticket or sponsorship) /// @return durationSeconds The duration of the timelock in seconds /// @return burnedCredit The credit that was burned function _calculateTimelockDuration( address from, address controlledToken, uint256 amount ) internal returns ( uint256 durationSeconds, uint256 burnedCredit ) { (uint256 exitFee, uint256 _burnedCredit) = _calculateEarlyExitFeeLessBurnedCredit(from, controlledToken, amount); uint256 duration = _estimateCreditAccrualTime(controlledToken, amount, exitFee); if (duration > maxTimelockDuration) { duration = maxTimelockDuration; } return (duration, _burnedCredit); } /// @notice Calculates the early exit fee for the given amount /// @param from The user who is withdrawing /// @param controlledToken The type of collateral being withdrawn /// @param amount The amount of collateral to be withdrawn /// @return exitFee The exit fee /// @return burnedCredit The user's credit that was burned function calculateEarlyExitFee( address from, address controlledToken, uint256 amount ) external override returns ( uint256 exitFee, uint256 burnedCredit ) { return _calculateEarlyExitFeeLessBurnedCredit(from, controlledToken, amount); } /// @dev Calculates the early exit fee for the given amount /// @param amount The amount of collateral to be withdrawn /// @return Exit fee function _calculateEarlyExitFeeNoCredit(address controlledToken, uint256 amount) internal view returns (uint256) { return _limitExitFee( amount, FixedPoint.multiplyUintByMantissa(amount, _tokenCreditPlans[controlledToken].creditLimitMantissa) ); } /// @notice Estimates the amount of time it will take for a given amount of funds to accrue the given amount of credit. /// @param _principal The principal amount on which interest is accruing /// @param _interest The amount of interest that must accrue /// @return durationSeconds The duration of time it will take to accrue the given amount of interest, in seconds. function estimateCreditAccrualTime( address _controlledToken, uint256 _principal, uint256 _interest ) external override view returns (uint256 durationSeconds) { return _estimateCreditAccrualTime( _controlledToken, _principal, _interest ); } /// @notice Estimates the amount of time it will take for a given amount of funds to accrue the given amount of credit /// @param _principal The principal amount on which interest is accruing /// @param _interest The amount of interest that must accrue /// @return durationSeconds The duration of time it will take to accrue the given amount of interest, in seconds. function _estimateCreditAccrualTime( address _controlledToken, uint256 _principal, uint256 _interest ) internal view returns (uint256 durationSeconds) { // interest = credit rate * principal * time // => time = interest / (credit rate * principal) uint256 accruedPerSecond = FixedPoint.multiplyUintByMantissa(_principal, _tokenCreditPlans[_controlledToken].creditRateMantissa); if (accruedPerSecond == 0) { return 0; } return _interest.div(accruedPerSecond); } /// @notice Burns a users credit. /// @param user The user whose credit should be burned /// @param credit The amount of credit to burn function _burnCredit(address user, address controlledToken, uint256 credit) internal { _tokenCreditBalances[controlledToken][user].balance = uint256(_tokenCreditBalances[controlledToken][user].balance).sub(credit).toUint128(); emit CreditBurned(user, controlledToken, credit); } /// @notice Accrues ticket credit for a user assuming their current balance is the passed balance. May burn credit if they exceed their limit. /// @param user The user for whom to accrue credit /// @param controlledToken The controlled token whose balance we are checking /// @param controlledTokenBalance The balance to use for the user /// @param extra Additional credit to be added function _accrueCredit(address user, address controlledToken, uint256 controlledTokenBalance, uint256 extra) internal { _updateCreditBalance( user, controlledToken, _calculateCreditBalance(user, controlledToken, controlledTokenBalance, extra) ); } function _calculateCreditBalance(address user, address controlledToken, uint256 controlledTokenBalance, uint256 extra) internal view returns (uint256) { uint256 newBalance; CreditBalance storage creditBalance = _tokenCreditBalances[controlledToken][user]; if (!creditBalance.initialized) { newBalance = 0; } else { uint256 credit = _calculateAccruedCredit(user, controlledToken, controlledTokenBalance); newBalance = _applyCreditLimit(controlledToken, controlledTokenBalance, uint256(creditBalance.balance).add(credit).add(extra)); } return newBalance; } function _updateCreditBalance(address user, address controlledToken, uint256 newBalance) internal { uint256 oldBalance = _tokenCreditBalances[controlledToken][user].balance; _tokenCreditBalances[controlledToken][user] = CreditBalance({ balance: newBalance.toUint128(), timestamp: _currentTime().toUint32(), initialized: true }); if (oldBalance < newBalance) { emit CreditMinted(user, controlledToken, newBalance.sub(oldBalance)); } else { emit CreditBurned(user, controlledToken, oldBalance.sub(newBalance)); } } /// @notice Applies the credit limit to a credit balance. The balance cannot exceed the credit limit. /// @param controlledToken The controlled token that the user holds /// @param controlledTokenBalance The users ticket balance (used to calculate credit limit) /// @param creditBalance The new credit balance to be checked /// @return The users new credit balance. Will not exceed the credit limit. function _applyCreditLimit(address controlledToken, uint256 controlledTokenBalance, uint256 creditBalance) internal view returns (uint256) { uint256 creditLimit = FixedPoint.multiplyUintByMantissa( controlledTokenBalance, _tokenCreditPlans[controlledToken].creditLimitMantissa ); if (creditBalance > creditLimit) { creditBalance = creditLimit; } return creditBalance; } /// @notice Calculates the accrued interest for a user /// @param user The user whose credit should be calculated. /// @param controlledToken The controlled token that the user holds /// @param controlledTokenBalance The user's current balance of the controlled tokens. /// @return The credit that has accrued since the last credit update. function _calculateAccruedCredit(address user, address controlledToken, uint256 controlledTokenBalance) internal view returns (uint256) { uint256 userTimestamp = _tokenCreditBalances[controlledToken][user].timestamp; if (!_tokenCreditBalances[controlledToken][user].initialized) { return 0; } uint256 deltaTime = _currentTime().sub(userTimestamp); uint256 creditPerSecond = FixedPoint.multiplyUintByMantissa(controlledTokenBalance, _tokenCreditPlans[controlledToken].creditRateMantissa); return deltaTime.mul(creditPerSecond); } /// @notice Returns the credit balance for a given user. Not that this includes both minted credit and pending credit. /// @param user The user whose credit balance should be returned /// @return The balance of the users credit function balanceOfCredit(address user, address controlledToken) external override onlyControlledToken(controlledToken) returns (uint256) { _accrueCredit(user, controlledToken, IERC20Upgradeable(controlledToken).balanceOf(user), 0); return _tokenCreditBalances[controlledToken][user].balance; } /// @notice Sets the rate at which credit accrues per second. The credit rate is a fixed point 18 number (like Ether). /// @param _controlledToken The controlled token for whom to set the credit plan /// @param _creditRateMantissa The credit rate to set. Is a fixed point 18 decimal (like Ether). /// @param _creditLimitMantissa The credit limit to set. Is a fixed point 18 decimal (like Ether). function setCreditPlanOf( address _controlledToken, uint128 _creditRateMantissa, uint128 _creditLimitMantissa ) external override onlyControlledToken(_controlledToken) onlyOwner { _tokenCreditPlans[_controlledToken] = CreditPlan({ creditLimitMantissa: _creditLimitMantissa, creditRateMantissa: _creditRateMantissa }); emit CreditPlanSet(_controlledToken, _creditLimitMantissa, _creditRateMantissa); } /// @notice Returns the credit rate of a controlled token /// @param controlledToken The controlled token to retrieve the credit rates for /// @return creditLimitMantissa The credit limit fraction. This number is used to calculate both the credit limit and early exit fee. /// @return creditRateMantissa The credit rate. This is the amount of tokens that accrue per second. function creditPlanOf( address controlledToken ) external override view returns ( uint128 creditLimitMantissa, uint128 creditRateMantissa ) { creditLimitMantissa = _tokenCreditPlans[controlledToken].creditLimitMantissa; creditRateMantissa = _tokenCreditPlans[controlledToken].creditRateMantissa; } /// @notice Calculate the early exit for a user given a withdrawal amount. The user's credit is taken into account. /// @param from The user who is withdrawing /// @param controlledToken The token they are withdrawing /// @param amount The amount of funds they are withdrawing /// @return earlyExitFee The additional exit fee that should be charged. /// @return creditBurned The amount of credit that will be burned function _calculateEarlyExitFeeLessBurnedCredit( address from, address controlledToken, uint256 amount ) internal returns ( uint256 earlyExitFee, uint256 creditBurned ) { uint256 controlledTokenBalance = IERC20Upgradeable(controlledToken).balanceOf(from); require(controlledTokenBalance >= amount, "PrizePool/insuff-funds"); _accrueCredit(from, controlledToken, controlledTokenBalance, 0); /* The credit is used *last*. Always charge the fees up-front. How to calculate: Calculate their remaining exit fee. I.e. full exit fee of their balance less their credit. If the exit fee on their withdrawal is greater than the remaining exit fee, then they'll have to pay the difference. */ // Determine available usable credit based on withdraw amount uint256 remainingExitFee = _calculateEarlyExitFeeNoCredit(controlledToken, controlledTokenBalance.sub(amount)); uint256 availableCredit; if (_tokenCreditBalances[controlledToken][from].balance >= remainingExitFee) { availableCredit = uint256(_tokenCreditBalances[controlledToken][from].balance).sub(remainingExitFee); } // Determine amount of credit to burn and amount of fees required uint256 totalExitFee = _calculateEarlyExitFeeNoCredit(controlledToken, amount); creditBurned = (availableCredit > totalExitFee) ? totalExitFee : availableCredit; earlyExitFee = totalExitFee.sub(creditBurned); return (earlyExitFee, creditBurned); } /// @notice Allows the Governor to set a cap on the amount of liquidity that he pool can hold /// @param _liquidityCap The new liquidity cap for the prize pool function setLiquidityCap(uint256 _liquidityCap) external override onlyOwner { _setLiquidityCap(_liquidityCap); } function _setLiquidityCap(uint256 _liquidityCap) internal { liquidityCap = _liquidityCap; emit LiquidityCapSet(_liquidityCap); } /// @notice Adds a new controlled token /// @param _controlledToken The controlled token to add. Cannot be a duplicate. function _addControlledToken(ControlledTokenInterface _controlledToken) internal { require(_controlledToken.controller() == this, "PrizePool/token-ctrlr-mismatch"); _tokens.addAddress(address(_controlledToken)); emit ControlledTokenAdded(_controlledToken); } /// @notice Sets the prize strategy of the prize pool. Only callable by the owner. /// @param _prizeStrategy The new prize strategy function setPrizeStrategy(TokenListenerInterface _prizeStrategy) external override onlyOwner { _setPrizeStrategy(_prizeStrategy); } /// @notice Sets the prize strategy of the prize pool. Only callable by the owner. /// @param _prizeStrategy The new prize strategy function _setPrizeStrategy(TokenListenerInterface _prizeStrategy) internal { require(address(_prizeStrategy) != address(0), "PrizePool/prizeStrategy-not-zero"); require(address(_prizeStrategy).supportsInterface(TokenListenerLibrary.ERC165_INTERFACE_ID_TOKEN_LISTENER), "PrizePool/prizeStrategy-invalid"); prizeStrategy = _prizeStrategy; emit PrizeStrategySet(address(_prizeStrategy)); } /// @notice An array of the Tokens controlled by the Prize Pool (ie. Tickets, Sponsorship) /// @return An array of controlled token addresses function tokens() external override view returns (address[] memory) { return _tokens.addressArray(); } /// @dev Gets the current time as represented by the current block /// @return The timestamp of the current block function _currentTime() internal virtual view returns (uint256) { return block.timestamp; } /// @notice The timestamp at which an account's timelocked balance will be made available to sweep /// @param user The address of an account with timelocked assets /// @return The timestamp at which the locked assets will be made available function timelockBalanceAvailableAt(address user) external override view returns (uint256) { return _unlockTimestamps[user]; } /// @notice The balance of timelocked assets for an account /// @param user The address of an account with timelocked assets /// @return The amount of assets that have been timelocked function timelockBalanceOf(address user) external override view returns (uint256) { return _timelockBalances[user]; } /// @notice The total of all controlled tokens and timelock. /// @return The current total of all tokens and timelock. function accountedBalance() external override view returns (uint256) { return _tokenTotalSupply(); } /// @notice Delegate the votes for a Compound COMP-like token held by the prize pool /// @param compLike The COMP-like token held by the prize pool that should be delegated /// @param to The address to delegate to function compLikeDelegate(ICompLike compLike, address to) external onlyOwner { if (compLike.balanceOf(address(this)) > 0) { compLike.delegate(to); } } /// @notice The total of all controlled tokens and timelock. /// @return The current total of all tokens and timelock. function _tokenTotalSupply() internal view returns (uint256) { uint256 total = timelockTotalSupply.add(reserveTotalSupply); address currentToken = _tokens.start(); while (currentToken != address(0) && currentToken != _tokens.end()) { total = total.add(IERC20Upgradeable(currentToken).totalSupply()); currentToken = _tokens.next(currentToken); } return total; } /// @dev Checks if the Prize Pool can receive liquidity based on the current cap /// @param _amount The amount of liquidity to be added to the Prize Pool /// @return True if the Prize Pool can receive the specified amount of liquidity function _canAddLiquidity(uint256 _amount) internal view returns (bool) { uint256 tokenTotalSupply = _tokenTotalSupply(); return (tokenTotalSupply.add(_amount) <= liquidityCap); } /// @dev Checks if a specific token is controlled by the Prize Pool /// @param controlledToken The address of the token to check /// @return True if the token is a controlled token, false otherwise function _isControlled(address controlledToken) internal view returns (bool) { return _tokens.contains(controlledToken); } /// @notice Determines whether the passed token can be transferred out as an external award. /// @dev Different yield sources will hold the deposits as another kind of token: such a Compound's cToken. The /// prize strategy should not be allowed to move those tokens. /// @param _externalToken The address of the token to check /// @return True if the token may be awarded, false otherwise function _canAwardExternal(address _externalToken) internal virtual view returns (bool); /// @notice Returns the ERC20 asset token used for deposits. /// @return The ERC20 asset token function _token() internal virtual view returns (IERC20Upgradeable); /// @notice Returns the total balance (in asset tokens). This includes the deposits and interest. /// @return The underlying balance of asset tokens function _balance() internal virtual returns (uint256); /// @notice Supplies asset tokens to the yield source. /// @param mintAmount The amount of asset tokens to be supplied function _supply(uint256 mintAmount) internal virtual; /// @notice Redeems asset tokens from the yield source. /// @param redeemAmount The amount of yield-bearing tokens to be redeemed /// @return The actual amount of tokens that were redeemed. function _redeem(uint256 redeemAmount) internal virtual returns (uint256); /// @dev Function modifier to ensure usage of tokens controlled by the Prize Pool /// @param controlledToken The address of the token to check modifier onlyControlledToken(address controlledToken) { require(_isControlled(controlledToken), "PrizePool/unknown-token"); _; } /// @dev Function modifier to ensure caller is the prize-strategy modifier onlyPrizeStrategy() { require(_msgSender() == address(prizeStrategy), "PrizePool/only-prizeStrategy"); _; } /// @dev Function modifier to ensure the deposit amount does not exceed the liquidity cap (if set) modifier canAddLiquidity(uint256 _amount) { require(_canAddLiquidity(_amount), "PrizePool/exceeds-liquidity-cap"); _; } modifier onlyReserve() { ReserveInterface reserve = ReserveInterface(reserveRegistry.lookup()); require(address(reserve) == msg.sender, "PrizePool/only-reserve"); _; } }