@@ -35,22 +35,32 @@ contract LiquidityHub is ILiquidityHub, ERC4626Upgradeable, AccessControlUpgrade
3535 bytes32 public constant ASSETS_ADJUST_ROLE = "ASSETS_ADJUST_ROLE " ;
3636 bytes32 public constant DEPOSIT_PROFIT_ROLE = "DEPOSIT_PROFIT_ROLE " ;
3737 bytes32 public constant SET_ASSETS_LIMIT_ROLE = "SET_ASSETS_LIMIT_ROLE " ;
38+ bytes32 public constant FULFIL_REDEEM_ROLE = "FULFIL_REDEEM_ROLE " ;
3839
3940 event TotalAssetsAdjustment (uint256 oldAssets , uint256 newAssets );
4041 event AssetsLimitSet (uint256 oldLimit , uint256 newLimit );
4142 event DepositProfit (address caller , uint256 assets );
43+ event RedeemRequest (
44+ address indexed controller , address indexed owner , uint256 indexed requestId ,
45+ address sender , uint256 shares
46+ );
47+ event OperatorSet (address indexed owner , address indexed operator , bool approved );
4248
4349 error ZeroAddress ();
4450 error NotImplemented ();
4551 error IncompatibleAssetsAndShares ();
4652 error AssetsLimitIsTooBig ();
4753 error EmptyHub ();
4854 error AssetsExceedHardLimit ();
55+ error Unauthorized ();
4956
5057 /// @custom:storage-location erc7201:sprinter.storage.LiquidityHub
5158 struct LiquidityHubStorage {
5259 uint256 totalAssets;
5360 uint256 assetsLimit;
61+ uint256 totalRedeemRequest;
62+ mapping (address controller = > uint256 shares ) redeemRequests;
63+ mapping (address ownerOrController = > mapping (address operator = > bool )) operators;
5464 }
5565
5666 bytes32 private constant STORAGE_LOCATION = 0xb877bfaae1674461dd1960c90f24075e3de3265a91f6906fe128ab8da6ba1700 ;
@@ -133,7 +143,11 @@ contract LiquidityHub is ILiquidityHub, ERC4626Upgradeable, AccessControlUpgrade
133143 }
134144
135145 function totalSupply () public view virtual override (IERC20 , ERC20Upgradeable ) returns (uint256 ) {
136- return IERC20 (address (SHARES)).totalSupply ();
146+ return IERC20 (address (SHARES)).totalSupply () + totalRedeemRequest ();
147+ }
148+
149+ function totalRedeemRequest () public view returns (uint256 ) {
150+ return _getStorage ().totalRedeemRequest;
137151 }
138152
139153 function balanceOf (address owner ) public view virtual override (IERC20 , ERC20Upgradeable ) returns (uint256 ) {
@@ -215,6 +229,78 @@ contract LiquidityHub is ILiquidityHub, ERC4626Upgradeable, AccessControlUpgrade
215229 emit DepositProfit (_msgSender (), assets);
216230 }
217231
232+ function setOperator (address operator , bool approved ) public {
233+ _getStorage ().operators[_msgSender ()][operator] = approved;
234+ emit OperatorSet (_msgSender (), operator, approved);
235+ }
236+
237+ function isOperator (address controller , address operator ) public view returns (bool ) {
238+ return _getStorage ().operators[controller][operator];
239+ }
240+
241+ function requestRedeem (uint256 shares , address controller , address owner ) external returns (uint256 requestId ) {
242+ require (controller != address (0 ), ZeroAddress ());
243+ return _requestRedeem (shares, controller, owner, _msgSender ());
244+ }
245+
246+ function requestRedeemWithFulfil (uint256 shares ) external returns (uint256 requestId ) {
247+ setOperator (address (this ), true );
248+ return _requestRedeem (shares, _msgSender (), _msgSender (), _msgSender ());
249+ }
250+
251+ function _requestRedeem (uint256 shares , address controller , address owner , address caller )
252+ internal returns (uint256 requestId )
253+ {
254+ LiquidityHubStorage storage $ = _getStorage ();
255+ bool ownerOrOperator = caller == owner || isOperator (owner, caller);
256+ if (! ownerOrOperator) {
257+ _spendAllowance (owner, caller, shares);
258+ }
259+ _burn (owner, shares);
260+ $.redeemRequests[controller] += shares;
261+ $.totalRedeemRequest += shares;
262+ emit RedeemRequest (controller, owner, 0 , caller, shares);
263+ return 0 ;
264+ }
265+
266+ function claimableRedeemRequest (uint256 /* requestId */ , address controller ) public view returns (uint256 ) {
267+ uint256 pending = _getStorage ().redeemRequests[controller];
268+ if (pending == 0 ) return 0 ;
269+ uint256 availableAssets = LIQUIDITY_POOL.balance (IERC20 (asset ()));
270+ uint256 availableShares = _convertToShares (availableAssets, Math.Rounding.Floor);
271+ return Math.min (pending, availableShares);
272+ }
273+
274+ function pendingRedeemRequest (uint256 /* requestId */ , address controller ) external view returns (uint256 ) {
275+ return _getStorage ().redeemRequests[controller] - claimableRedeemRequest (0 , controller);
276+ }
277+
278+ function fulfilRedeem (address [] calldata receivers ) external onlyRole (FULFIL_REDEEM_ROLE) {
279+ for (uint256 i = 0 ; i < receivers.length ; i++ ) {
280+ address receiver = receivers[i];
281+ uint256 shares = claimableRedeemRequest (0 , receiver);
282+ if (shares == 0 ) continue ;
283+ this .redeem (shares, receiver, receiver);
284+ }
285+ }
286+
287+ function maxRedeem (address owner ) public view override returns (uint256 ) {
288+ uint256 totalShares = balanceOf (owner) + _getStorage ().redeemRequests[owner];
289+ uint256 total = _convertToAssets (totalShares, Math.Rounding.Floor);
290+ uint256 availableAssets = LIQUIDITY_POOL.balance (IERC20 (asset ()));
291+ if (total > availableAssets) {
292+ return _convertToShares (availableAssets, Math.Rounding.Floor);
293+ }
294+ return totalShares;
295+ }
296+
297+ function maxWithdraw (address owner ) public view override returns (uint256 ) {
298+ uint256 totalShares = balanceOf (owner) + _getStorage ().redeemRequests[owner];
299+ uint256 total = _convertToAssets (totalShares, Math.Rounding.Floor);
300+ uint256 availableAssets = LIQUIDITY_POOL.balance (IERC20 (asset ()));
301+ return Math.min (total, availableAssets);
302+ }
303+
218304 function _convertToShares (uint256 assets , Math.Rounding rounding ) internal view virtual override returns (uint256 ) {
219305 (uint256 supplyShares , uint256 supplyAssets ) = _getTotalsForConversion ();
220306 return assets.mulDiv (supplyShares, supplyAssets, rounding);
@@ -273,11 +359,22 @@ contract LiquidityHub is ILiquidityHub, ERC4626Upgradeable, AccessControlUpgrade
273359 uint256 shares
274360 ) internal virtual override {
275361 LiquidityHubStorage storage $ = _getStorage ();
276- if (caller != owner) {
277- _spendAllowance (owner, caller, shares);
278- }
279362 $.totalAssets -= assets;
280- _burn (owner, shares);
363+ uint256 pending = $.redeemRequests[owner];
364+ uint256 fromPending = Math.min (pending, shares);
365+ bool ownerOrOperator = caller == owner || isOperator (owner, caller);
366+ if (fromPending > 0 ) {
367+ require (ownerOrOperator, Unauthorized ());
368+ $.redeemRequests[owner] = pending - fromPending;
369+ $.totalRedeemRequest -= fromPending;
370+ }
371+ uint256 fromOwner = shares - fromPending;
372+ if (fromOwner > 0 ) {
373+ if (! ownerOrOperator) {
374+ _spendAllowance (owner, caller, fromOwner);
375+ }
376+ _burn (owner, fromOwner);
377+ }
281378 LIQUIDITY_POOL.withdraw (receiver, assets);
282379 emit Withdraw (caller, receiver, owner, assets, shares);
283380 }
0 commit comments