Skip to content

Commit 470b32b

Browse files
authored
Feat/async hub (#236)
* WIP * Update current tests before adding new ones and optimizing * Add tests to all pools except public * Update coverage * Stabilize tests * Optimize contracts size * Cleanup tests * Extend time span in aave tests * More fine tuning Aave tests * WIP * Update tests * Update tests and scripts, WIP * Fix scripts * Reduce pools code size with optimizer config * Remove unused constant from Aave pool * Remove Everclear * Fix lint * Fix imports * Fix Repayer deployment in Gnosis tests * WIP * Finalize tests for async redeem * Address comments * Add implementation prefix to impl deployments * Update coverage * Fix merge * Perform state updates before external calls * Allow operator to redeem/withdraw Use floor when calculating maxRedeem and claimableRedeemRequest * Add precision to maxWithdraw() * Avoid overflow when calculating maxRedeem on insane numbers * Disallow requesting redeem to zero controller
1 parent b9060fa commit 470b32b

5 files changed

Lines changed: 668 additions & 14 deletions

File tree

contracts/LiquidityHub.sol

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

contracts/echidna/EchidnaLiquidityHub.sol

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ contract EchidnaLiquidityHub {
7676
function testDeposit(uint256 amount) public {
7777
// Preconditions
7878
uint256 assets = hub.totalAssets();
79+
require(amount <= hub.maxDeposit(address(this)), RequireFailed());
7980
require(amount > assets / 10 ** 12, RequireFailed());
8081
uint256 depositedBefore = hub.totalAssets();
8182
hub.setAssetsLimit(depositedBefore + amount);
@@ -124,7 +125,11 @@ contract EchidnaLiquidityHub {
124125
// withdraw() should be successful
125126
function testWithdraw(uint256 amount) public {
126127
// Preconditions
128+
uint256 assets = hub.totalAssets();
129+
uint256 balanceSharesBeforeDeposit = shares.balanceOf(address(this));
130+
require(balanceSharesBeforeDeposit > 0 || assets > liquidityToken.balanceOf(address(pool)), RequireFailed());
127131
require(amount > 0, RequireFailed());
132+
require(amount <= hub.maxDeposit(address(this)), RequireFailed());
128133
// require(shares.balanceOf(address(this)) >= amount);
129134
liquidityToken.mint(address(this), amount);
130135
liquidityToken.approve(address(hub), amount);
@@ -154,11 +159,13 @@ contract EchidnaLiquidityHub {
154159
function testRedeem(uint256 amount) public {
155160
// Preconditions
156161
uint256 assets = hub.totalAssets();
162+
uint256 balanceSharesBeforeDeposit = shares.balanceOf(address(this));
163+
require(balanceSharesBeforeDeposit > 0 || assets > liquidityToken.balanceOf(address(pool)), RequireFailed());
157164
require(assets > 0, RequireFailed());
158165
require((amount > assets / 10 ** 12) && (amount > 1), RequireFailed());
166+
require(amount <= hub.maxDeposit(address(this)), RequireFailed());
159167
uint256 previewShares = hub.previewDeposit(amount);
160168
require(previewShares > 0, RequireFailed());
161-
uint256 balanceSharesBeforeDeposit = shares.balanceOf(address(this));
162169
liquidityToken.mint(address(this), amount);
163170
liquidityToken.approve(address(hub), amount);
164171
hub.deposit(amount, address(this));

contracts/interfaces/ILiquidityHub.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,12 @@ import {IManagedToken} from "./IManagedToken.sol";
55

66
interface ILiquidityHub {
77
function SHARES() external view returns (IManagedToken);
8+
function totalRedeemRequest() external view returns (uint256);
9+
function setOperator(address operator, bool approved) external;
10+
function isOperator(address owner, address operator) external view returns (bool);
11+
function requestRedeem(uint256 shares, address controller, address owner) external returns (uint256 requestId);
12+
function requestRedeemWithFulfil(uint256 shares) external returns (uint256 requestId);
13+
function claimableRedeemRequest(uint256 requestId, address controller) external view returns (uint256 shares);
14+
function pendingRedeemRequest(uint256 requestId, address controller) external view returns (uint256 shares);
15+
function fulfilRedeem(address[] calldata receivers) external;
816
}

coverage-baseline.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"lines": "99.66",
3-
"functions": "99.64",
4-
"branches": "92.56",
5-
"statements": "99.66"
2+
"lines": "99.68",
3+
"functions": "99.65",
4+
"branches": "92.47",
5+
"statements": "99.68"
66
}

0 commit comments

Comments
 (0)