|
| 1 | +// SPDX-License-Identifier: BUSL-1.1 |
| 2 | +pragma solidity 0.8.24; |
| 3 | + |
| 4 | +import { IERC20Upgradeable as IERC20 } from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; |
| 5 | +// solhint-disable-next-line max-line-length |
| 6 | +import { SafeERC20Upgradeable as SafeERC20 } from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/utils/SafeERC20Upgradeable.sol"; |
| 7 | + |
| 8 | +import { LiquidityStrategy } from "./LiquidityStrategy.sol"; |
| 9 | +import { ILiquidityStrategy } from "../interfaces/ILiquidityStrategy.sol"; |
| 10 | +import { IOpenLiquidityStrategy } from "../interfaces/IOpenLiquidityStrategy.sol"; |
| 11 | +import { IFPMM } from "../interfaces/IFPMM.sol"; |
| 12 | +import { LiquidityStrategyTypes as LQ } from "../libraries/LiquidityStrategyTypes.sol"; |
| 13 | + |
| 14 | +/** |
| 15 | + * @title OpenLiquidityStrategy |
| 16 | + * @notice Liquidity strategy where the caller of rebalance() acts as the liquidity source. |
| 17 | + * @dev The rebalancer provides tokens to the pool and receives tokens from the pool |
| 18 | + * via standard ERC20 transfers. The rebalancer must approve this contract for |
| 19 | + * the tokens owed to the pool before calling rebalance(). |
| 20 | + */ |
| 21 | +contract OpenLiquidityStrategy is IOpenLiquidityStrategy, LiquidityStrategy { |
| 22 | + using LQ for LQ.Context; |
| 23 | + using SafeERC20 for IERC20; |
| 24 | + |
| 25 | + /// @dev Transient storage slot for the rebalancer address |
| 26 | + bytes32 private constant REBALANCER_TSLOT = keccak256("OpenLiquidityStrategy.rebalancer"); |
| 27 | + |
| 28 | + /* ============================================================ */ |
| 29 | + /* ======================= Constructor ======================== */ |
| 30 | + /* ============================================================ */ |
| 31 | + |
| 32 | + /** |
| 33 | + * @notice Disables initializers on implementation contracts. |
| 34 | + * @param disable Set to true to disable initializers (for proxy pattern). |
| 35 | + */ |
| 36 | + constructor(bool disable) LiquidityStrategy(disable) {} |
| 37 | + |
| 38 | + /// @inheritdoc IOpenLiquidityStrategy |
| 39 | + function initialize(address _initialOwner) public initializer { |
| 40 | + __LiquidityStrategy_init(_initialOwner); |
| 41 | + } |
| 42 | + |
| 43 | + /* ============================================================ */ |
| 44 | + /* ==================== External Functions ==================== */ |
| 45 | + /* ============================================================ */ |
| 46 | + |
| 47 | + /// @inheritdoc IOpenLiquidityStrategy |
| 48 | + function addPool(AddPoolParams calldata params) external onlyOwner { |
| 49 | + LiquidityStrategy._addPool(params); |
| 50 | + } |
| 51 | + |
| 52 | + /// @inheritdoc IOpenLiquidityStrategy |
| 53 | + function removePool(address pool) external onlyOwner { |
| 54 | + LiquidityStrategy._removePool(pool); |
| 55 | + } |
| 56 | + |
| 57 | + function rebalance(address pool) external override(ILiquidityStrategy, LiquidityStrategy) nonReentrant { |
| 58 | + _setRebalancer(_msgSender()); |
| 59 | + |
| 60 | + _ensurePool(pool); |
| 61 | + if (_isHookCalled(pool)) { |
| 62 | + revert LS_CAN_ONLY_REBALANCE_ONCE(pool); |
| 63 | + } |
| 64 | + |
| 65 | + PoolConfig memory config = poolConfigs[pool]; |
| 66 | + // Skip cooldown check for first rebalance (lastRebalance == 0) |
| 67 | + if (config.lastRebalance > 0 && block.timestamp < config.lastRebalance + config.rebalanceCooldown) { |
| 68 | + revert LS_COOLDOWN_ACTIVE(); |
| 69 | + } |
| 70 | + |
| 71 | + LQ.Context memory ctx = LQ.newRebalanceContext(pool, config); |
| 72 | + LQ.Action memory action = _determineAction(ctx); |
| 73 | + |
| 74 | + (address debtToken, address collToken) = ctx.tokens(); |
| 75 | + |
| 76 | + bytes memory hookData = abi.encode( |
| 77 | + LQ.CallbackData({ |
| 78 | + amountOwedToPool: action.amountOwedToPool, |
| 79 | + dir: action.dir, |
| 80 | + isToken0Debt: ctx.isToken0Debt, |
| 81 | + debtToken: debtToken, |
| 82 | + collToken: collToken |
| 83 | + }) |
| 84 | + ); |
| 85 | + |
| 86 | + poolConfigs[pool].lastRebalance = uint32(block.timestamp); |
| 87 | + IFPMM(pool).rebalance(action.amount0Out, action.amount1Out, hookData); |
| 88 | + if (!_isHookCalled(pool)) { |
| 89 | + revert LS_HOOK_NOT_CALLED(); |
| 90 | + } |
| 91 | + |
| 92 | + // slither-disable-start incorrect-equality |
| 93 | + emit LiquidityMoved({ |
| 94 | + pool: pool, |
| 95 | + direction: action.dir, |
| 96 | + tokenGivenToPool: action.dir == LQ.Direction.Expand ? debtToken : collToken, |
| 97 | + amountGivenToPool: action.amountOwedToPool, |
| 98 | + tokenTakenFromPool: action.dir == LQ.Direction.Expand ? collToken : debtToken, |
| 99 | + amountTakenFromPool: action.amount0Out + action.amount1Out // only one is positive |
| 100 | + }); |
| 101 | + // slither-disable-end incorrect-equality |
| 102 | + } |
| 103 | + |
| 104 | + /* =========================================================== */ |
| 105 | + /* ==================== Virtual Functions ==================== */ |
| 106 | + /* =========================================================== */ |
| 107 | + |
| 108 | + /** |
| 109 | + * @notice Clamps expansion amounts based on the rebalancer's debt token balance |
| 110 | + * @dev For expansions, checks the rebalancer's debt token balance and adjusts if insufficient |
| 111 | + * @param ctx The liquidity context containing pool state and configuration |
| 112 | + * @param idealDebtToExpand The calculated ideal amount of debt tokens to add to pool |
| 113 | + * @param idealCollateralToPay The calculated ideal amount of collateral to receive from pool |
| 114 | + * @return debtToExpand The actual debt amount to expand (may be less than ideal) |
| 115 | + * @return collateralToPay The actual collateral amount to receive (adjusted if balance insufficient) |
| 116 | + */ |
| 117 | + function _clampExpansion( |
| 118 | + LQ.Context memory ctx, |
| 119 | + uint256 idealDebtToExpand, |
| 120 | + uint256 idealCollateralToPay |
| 121 | + ) internal view override returns (uint256 debtToExpand, uint256 collateralToPay) { |
| 122 | + address debtToken = ctx.debtToken(); |
| 123 | + uint256 debtBalance = IERC20(debtToken).balanceOf(_getRebalancer()); |
| 124 | + |
| 125 | + // slither-disable-next-line incorrect-equality |
| 126 | + if (debtBalance == 0) revert OLS_OUT_OF_DEBT(); |
| 127 | + |
| 128 | + if (debtBalance < idealDebtToExpand) { |
| 129 | + uint256 combinedFeeMultiplier = LQ.combineFees( |
| 130 | + ctx.incentives.protocolIncentiveExpansion, |
| 131 | + ctx.incentives.liquiditySourceIncentiveExpansion |
| 132 | + ); |
| 133 | + debtToExpand = debtBalance; |
| 134 | + collateralToPay = ctx.convertToCollateralWithFee(debtBalance, LQ.FEE_DENOMINATOR, combinedFeeMultiplier); |
| 135 | + } else { |
| 136 | + debtToExpand = idealDebtToExpand; |
| 137 | + collateralToPay = idealCollateralToPay; |
| 138 | + } |
| 139 | + |
| 140 | + return (debtToExpand, collateralToPay); |
| 141 | + } |
| 142 | + |
| 143 | + /** |
| 144 | + * @notice Clamps contraction amounts based on the rebalancer's collateral balance |
| 145 | + * @dev For contractions, checks the rebalancer's collateral balance and adjusts if insufficient |
| 146 | + * @param ctx The liquidity context containing pool state and configuration |
| 147 | + * @param idealDebtToContract The calculated ideal amount of debt tokens to receive from pool |
| 148 | + * @param idealCollateralToReceive The calculated ideal amount of collateral to add to pool |
| 149 | + * @return debtToContract The actual debt amount to contract (may be less than ideal) |
| 150 | + * @return collateralToReceive The actual collateral amount to send (adjusted if balance insufficient) |
| 151 | + */ |
| 152 | + function _clampContraction( |
| 153 | + LQ.Context memory ctx, |
| 154 | + uint256 idealDebtToContract, |
| 155 | + uint256 idealCollateralToReceive |
| 156 | + ) internal view override returns (uint256 debtToContract, uint256 collateralToReceive) { |
| 157 | + address collateralToken = ctx.collateralToken(); |
| 158 | + uint256 collateralBalance = IERC20(collateralToken).balanceOf(_getRebalancer()); |
| 159 | + |
| 160 | + // slither-disable-next-line incorrect-equality |
| 161 | + if (collateralBalance == 0) revert OLS_OUT_OF_COLLATERAL(); |
| 162 | + |
| 163 | + if (collateralBalance < idealCollateralToReceive) { |
| 164 | + uint256 combinedFeeMultiplier = LQ.combineFees( |
| 165 | + ctx.incentives.protocolIncentiveContraction, |
| 166 | + ctx.incentives.liquiditySourceIncentiveContraction |
| 167 | + ); |
| 168 | + collateralToReceive = collateralBalance; |
| 169 | + debtToContract = ctx.convertToDebtWithFee(collateralBalance, LQ.FEE_DENOMINATOR, combinedFeeMultiplier); |
| 170 | + } else { |
| 171 | + collateralToReceive = idealCollateralToReceive; |
| 172 | + debtToContract = idealDebtToContract; |
| 173 | + } |
| 174 | + |
| 175 | + return (debtToContract, collateralToReceive); |
| 176 | + } |
| 177 | + |
| 178 | + /* ============================================================ */ |
| 179 | + /* ================= Callback Implementation ================== */ |
| 180 | + /* ============================================================ */ |
| 181 | + |
| 182 | + /** |
| 183 | + * @notice Handles the rebalance callback by transferring tokens to/from the rebalancer |
| 184 | + * @dev Tokens received from the pool (minus protocol incentive) go to the rebalancer. |
| 185 | + * Tokens owed to the pool are pulled from the rebalancer via transferFrom. |
| 186 | + * @param pool The address of the FPMM pool |
| 187 | + * @param amount0Out The amount of token0 sent by the pool |
| 188 | + * @param amount1Out The amount of token1 sent by the pool |
| 189 | + * @param cb The callback data containing rebalance parameters |
| 190 | + */ |
| 191 | + function _handleCallback( |
| 192 | + address pool, |
| 193 | + uint256 amount0Out, |
| 194 | + uint256 amount1Out, |
| 195 | + LQ.CallbackData memory cb |
| 196 | + ) internal override { |
| 197 | + PoolConfig memory config = poolConfigs[pool]; |
| 198 | + address rebalancer = _getRebalancer(); |
| 199 | + |
| 200 | + (address tokenFromPool, address tokenToPool, uint256 protocolIncentive) = cb.dir == LQ.Direction.Expand |
| 201 | + ? (cb.collToken, cb.debtToken, uint256(config.protocolIncentiveExpansion)) |
| 202 | + : (cb.debtToken, cb.collToken, uint256(config.protocolIncentiveContraction)); |
| 203 | + |
| 204 | + uint256 amountFromPool = amount0Out > 0 ? amount0Out : amount1Out; |
| 205 | + uint256 protocolIncentiveAmount = (amountFromPool * protocolIncentive) / LQ.FEE_DENOMINATOR; |
| 206 | + |
| 207 | + // Transfer protocol incentive to protocol fee recipient |
| 208 | + _transferRebalanceIncentive(tokenFromPool, protocolIncentiveAmount, config.protocolFeeRecipient); |
| 209 | + // Transfer remaining tokens to rebalancer (includes liquidity source incentive) |
| 210 | + IERC20(tokenFromPool).safeTransfer(rebalancer, amountFromPool - protocolIncentiveAmount); |
| 211 | + // Pull tokens from rebalancer and send to pool |
| 212 | + IERC20(tokenToPool).safeTransferFrom(rebalancer, pool, cb.amountOwedToPool); |
| 213 | + } |
| 214 | + |
| 215 | + /* ============================================================ */ |
| 216 | + /* ==================== Private Functions ===================== */ |
| 217 | + /* ============================================================ */ |
| 218 | + |
| 219 | + /** |
| 220 | + * @notice Stores the rebalancer address in transient storage |
| 221 | + * @param rebalancer The address of the rebalancer (msg.sender of rebalance()) |
| 222 | + */ |
| 223 | + function _setRebalancer(address rebalancer) private { |
| 224 | + bytes32 slot = REBALANCER_TSLOT; |
| 225 | + // solhint-disable-next-line no-inline-assembly |
| 226 | + assembly { |
| 227 | + tstore(slot, rebalancer) |
| 228 | + } |
| 229 | + } |
| 230 | + |
| 231 | + /** |
| 232 | + * @notice Reads the rebalancer address from transient storage |
| 233 | + * @return rebalancer The stored rebalancer address |
| 234 | + */ |
| 235 | + function _getRebalancer() private view returns (address rebalancer) { |
| 236 | + bytes32 slot = REBALANCER_TSLOT; |
| 237 | + // solhint-disable-next-line no-inline-assembly |
| 238 | + assembly { |
| 239 | + rebalancer := tload(slot) |
| 240 | + } |
| 241 | + } |
| 242 | + |
| 243 | + /** |
| 244 | + * @notice Checks if the hook was called for a pool in the current transaction |
| 245 | + * @dev Mirrors LiquidityStrategy._getHookCalled (which is private) using the same key derivation |
| 246 | + * @param pool The address of the pool being checked |
| 247 | + * @return called True if the hook was called for this pool |
| 248 | + */ |
| 249 | + function _isHookCalled(address pool) private view returns (bool called) { |
| 250 | + bytes32 key = bytes32(uint256(uint160(pool))); |
| 251 | + // solhint-disable-next-line no-inline-assembly |
| 252 | + assembly { |
| 253 | + called := tload(key) |
| 254 | + } |
| 255 | + } |
| 256 | +} |
0 commit comments