diff --git a/contracts/JBBuybackDelegate.sol b/contracts/JBBuybackDelegate.sol index b025638..3c952d3 100644 --- a/contracts/JBBuybackDelegate.sol +++ b/contracts/JBBuybackDelegate.sol @@ -49,9 +49,16 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate { // --------------------- public constant properties ------------------ // //*********************************************************************// + /// @notice The unit of the swap amount limit percentage. + uint256 public constant SWAP_PERCENT_LIMIT_DENOMINATOR = 10_000; + /// @notice The unit of the max slippage. uint256 public constant SLIPPAGE_DENOMINATOR = 10_000; + /// @notice The minimum possible percent limit of swapping. + /// @dev This serves to avoid being able to bypass swapping with more than 10% of a payment. + uint256 public constant MIN_SWAP_PERCENT_LIMIT = 9_000; + /// @notice The minimum twap deviation allowed, out of MAX_SLIPPAGE. /// @dev This serves to avoid operators settings values that force the bypassing the swap when a quote is not provided in payment metadata. uint256 public constant MIN_TWAP_SLIPPAGE_TOLERANCE = 100; @@ -166,8 +173,11 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate { if (_quoteExists) (_amountToSwapWith, _minimumSwapAmountOut) = abi.decode(_metadata, (uint256, uint256)); } + // Make sure the amount to swap with is at most the swap percent limit. + if (_amountToSwapWith != 0 && _amountToSwapWith > mulDiv(_totalPaid, _swapPercentLimit, SWAP_PERCENT_LIMIT_DENOMINATOR)) revert JuiceBuyback_SwapAmountOutOfBounds(); + // If no amount was specified to swap with, default to the full amount of the payment. - if (_amountToSwapWith == 0) _amountToSwapWith = _totalPaid; + if (_amountToSwapWith == 0) _amountToSwapWith = mulDiv(_totalPaid, _swapPercentLimit, SWAP_PERCENT_LIMIT_DENOMINATOR); // Find the default total number of tokens to mint as if no Buyback Delegate were installed, as a fixed point number with 18 decimals @@ -216,14 +226,21 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate { /// @param _projectId The ID of the project for which the value applies. /// @return _secondsAgo The period over which the TWAP is computed. function twapWindowOf(uint256 _projectId) external view returns (uint32) { - return uint32(_twapParamsOf[_projectId]); + return uint256(uint32(_twapParamsOf[_projectId] << 96 >> 128)); } /// @notice The TWAP max deviation acepted, out of SLIPPAGE_DENOMINATOR. /// @param _projectId The ID of the project for which the value applies. /// @return _delta the maximum deviation allowed between the token amount received and the TWAP quote. function twapSlippageToleranceOf(uint256 _projectId) external view returns (uint256) { - return _twapParamsOf[_projectId] >> 128; + return uint256(uint32(_twapParamsOf[_projectId] >> 96)); + } + + /// @notice The limit of paid funds that can be allocated towards a swap, as a percent out of SWAP_PERCENT_LIMIT_DENOMINATOR. + /// @param _projectId The ID of the project for which the value applies. + /// @return The limit percentage of payments that can be used for swapping. + function swapPercentLimit(uint256 _projectId) external view returns (uint256) { + return uint256(uint96(_twapParamsOf[_projectId])); } /// @notice Generic redeem params, for interface completion. @@ -350,9 +367,10 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate { /// @param _fee The fee that is used in the pool being set. /// @param _twapWindow The period over which the TWAP is computed. /// @param _twapSlippageTolerance The maximum deviation allowed between amount received and TWAP. + /// @param _swapPercentLimit The limit of paid funds that can be allocated towards a swap. /// @param _terminalToken The terminal token that payments are made in. /// @return newPool The pool that was created. - function setPoolFor(uint256 _projectId, uint24 _fee, uint32 _twapWindow, uint256 _twapSlippageTolerance, address _terminalToken) + function setPoolFor(uint256 _projectId, uint24 _fee, uint256 _twapWindow, uint256 _twapSlippageTolerance, uint256 _swapPercentLimit, address _terminalToken) external requirePermission(PROJECTS.ownerOf(_projectId), _projectId, JBBuybackDelegateOperations.CHANGE_POOL) returns (IUniswapV3Pool newPool) @@ -363,6 +381,9 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate { // Make sure the provided period is within sane bounds. if (_twapWindow < MIN_TWAP_WINDOW || _twapWindow > MAX_TWAP_WINDOW) revert JuiceBuyback_InvalidTwapWindow(); + // Make sure the provided delta is within sane bounds. + if (_swapPercentLimit < MIN_SWAP_PERCENT_LIMIT || _swapPercentLimit > SWAP_PERCENT_LIMIT_DENOMINATOR) revert JuiceBuyback_InvalidSwapPercentLimit(); + // Keep a reference to the project's token. address _projectToken = address(CONTROLLER.tokenStore().tokenOf(_projectId)); @@ -407,7 +428,11 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate { poolOf[_projectId][_terminalToken] = newPool; // Store the twap period and max slipage. - _twapParamsOf[_projectId] = _twapSlippageTolerance << 128 | _twapWindow; + // _twapSlippageTolerance - occupies the topmost bits + // _twapWindow - occupies the next 32 bits + // _swapPercentLimit - occupies the remaining 32 bits + _twapParamsOf[_projectId] = _twapSlippageTolerance << 96 | _twapWindow << 64 | _swapPercentLimit; + projectTokenOf[_projectId] = address(_projectToken); emit BuybackDelegate_TwapWindowChanged(_projectId, 0, _twapWindow, msg.sender); @@ -435,7 +460,7 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate { uint256 _oldWindow = uint128(_twapParams); // Store the new packed value of the TWAP params. - _twapParamsOf[_projectId] = uint256(_newWindow) | ((_twapParams >> 128) << 128); + _twapParamsOf[_projectId] = _twapParams >> 96 << 64 | _twapParams << 128 >> 128 | _newWindow << 64; emit BuybackDelegate_TwapWindowChanged(_projectId, _oldWindow, _newWindow, msg.sender); } @@ -458,11 +483,34 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate { uint256 _oldSlippageTolerance = _twapParams >> 128; // Store the new packed value of the TWAP params. - _twapParamsOf[_projectId] = _newSlippageTolerance << 128 | ((_twapParams << 128) >> 128); + _twapParamsOf[_projectId] = newTwapSlippageTolerance << 96 | _twapParams >> 96 << 96; emit BuybackDelegate_TwapSlippageToleranceChanged(_projectId, _oldSlippageTolerance, _newSlippageTolerance, msg.sender); } + /// @notice Set the limit of paid funds that can be allocated towards a swap, as a percent out of SWAP_PERCENT_LIMIT_DENOMINATOR. + /// @dev This can be called by the project owner or an address having the SET_POOL permission in JBOperatorStore. + /// @param _projectId The ID for which the new value applies. + /// @param _newSwapPercentLimit the new limit, out of SWAP_PERCENT_LIMIT_DENOMINATOR. + function setSwapPercentLimitOf(uint256 _projectId, uint256 _newSwapPercentLimit) + external + requirePermission(PROJECTS.ownerOf(_projectId), _projectId, JBBuybackDelegateOperations.SET_POOL_PARAMS) + { + // Make sure the provided delta is within sane bounds. + if (_newSwapPercentLimit < MIN_SWAP_PERCENT_LIMIT) revert JuiceBuyback_InvalidSwapPercentLimit(); + + // Keep a reference to the currently stored TWAP params. + uint256 _twapParams = _twapParamsOf[_projectId]; + + // Keep a reference to the old swap percent limit. + uint256 _oldSwapPercentLimit = _twapParams >> 128; + + // Store the new packed value of the TWAP params. + _twapParamsOf[_projectId] = _twapParams << 96 >> 96 | newSwapPercentLimit; + + emit BuybackDelegate_TwapPercentLimitChanged(_projectId, _oldSwapPercentLimit, _newSwapPercentLimit, msg.sender); + } + //*********************************************************************// // ---------------------- internal functions ------------------------- // //*********************************************************************// diff --git a/contracts/interfaces/IJBBuybackDelegate.sol b/contracts/interfaces/IJBBuybackDelegate.sol index f4a95b5..3902ef8 100644 --- a/contracts/interfaces/IJBBuybackDelegate.sol +++ b/contracts/interfaces/IJBBuybackDelegate.sol @@ -23,7 +23,9 @@ interface IJBBuybackDelegate is IJBPayDelegate3_1_1, IJBFundingCycleDataSource3_ error JuiceBuyback_NewSecondsAgoTooLow(); error JuiceBuyback_NoProjectToken(); error JuiceBuyback_PoolAlreadySet(); + error JuiceBuyback_SwapAmountOutOfBounds(); error JuiceBuyback_TransferFailed(); + error JuiceBuyback_InvalidSwapPercentLimit(); error JuiceBuyback_InvalidTwapSlippageTolerance(); error JuiceBuyback_InvalidTwapWindow(); error JuiceBuyback_Unauthorized(); @@ -36,6 +38,7 @@ interface IJBBuybackDelegate is IJBPayDelegate3_1_1, IJBFundingCycleDataSource3_ event BuybackDelegate_Mint(uint256 indexed projectId, uint256 amountIn, uint256 tokenCount, address caller); event BuybackDelegate_TwapWindowChanged(uint256 indexed projectId, uint256 oldSecondsAgo, uint256 newSecondsAgo, address caller); event BuybackDelegate_TwapSlippageToleranceChanged(uint256 indexed projectId, uint256 oldTwapDelta, uint256 newTwapDelta, address caller); + event BuybackDelegate_TwapPercentLimitChanged(uint256 indexed projectId, uint256 oldSwapPercentLimit, newSwapPercentLimit, address caller); event BuybackDelegate_PoolAdded(uint256 indexed projectId, address indexed terminalToken, address newPool, address caller); ///////////////////////////////////////////////////////////////////// @@ -69,4 +72,6 @@ interface IJBBuybackDelegate is IJBPayDelegate3_1_1, IJBFundingCycleDataSource3_ function setTwapWindowOf(uint256 projectId, uint32 newWindow) external; function setTwapSlippageToleranceOf(uint256 projectId, uint256 newSlippageTolerance) external; + + function setSwapPercentLimitOf(uint256 _projectId, uint256 _newSwapPercentLimit) external; }