diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f99b901..7985434 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: FORGE_GAS_REPORT: true FOUNDRY_PROFILE: ${{ github.event_name == 'push' && 'ci' || '' }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: gas-report path: gas.txt @@ -94,7 +94,7 @@ jobs: FOUNDRY_PROFILE: ${{ github.event_name == 'push' && 'ci' || '' }} - name: Report code coverage - uses: zgosalvez/github-actions-report-lcov@v1 + uses: zgosalvez/github-actions-report-lcov@v4 with: coverage-files: lcov.info.pruned minimum-coverage: 90 @@ -108,7 +108,7 @@ jobs: runs-on: ubuntu-latest needs: [test] steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: gas-report path: gas.txt diff --git a/.gitignore b/.gitignore index f712bc8..deeea5d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ docs/ # Dotenv file .env + +# Vscode +.vscode* diff --git a/README.md b/README.md index 54446f8..3cb7988 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Withdrawal Manager Queue -![CI](https://github.com/maple-labs/withdrawal-manager-queue-private/actions/workflows/ci.yml/badge.svg) +![CI](https://github.com/maple-labs/withdrawal-manager-queue/actions/workflows/ci.yml/badge.svg) [![GitBook - Documentation](https://img.shields.io/badge/GitBook-Documentation-orange?logo=gitbook&logoColor=white)](https://maplefinance.gitbook.io/maple/maple-for-developers/protocol-overview) [![Foundry][foundry-badge]][foundry] -[![License: BUSL 1.1](https://img.shields.io/badge/License-BUSL%201.1-blue.svg)](https://github.com/maple-labs/withdrawal-manager-queue-private/blob/main/LICENSE) +[![License: BUSL 1.1](https://img.shields.io/badge/License-BUSL%201.1-blue.svg)](https://github.com/maple-labs/withdrawal-manager-queue/blob/main/LICENSE) [foundry]: https://getfoundry.sh/ [foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg @@ -23,12 +23,13 @@ Versions of dependencies can be checked with `git submodule status`. This project was built using [Foundry](https://book.getfoundry.sh/). Refer to installation instructions [here](https://github.com/foundry-rs/foundry#installation). ```sh -git clone git@github.com:maple-labs/withdrawal-manager-queue-private.git -cd withdrawal-manager-queue-private +git clone git@github.com:maple-labs/withdrawal-manager-queue.git +cd withdrawal-manager-queue forge install ``` ## Audit Reports +For all audit reports please refer to https://docs.maple.finance/technical-resources/security/security ## Bug Bounty @@ -41,5 +42,5 @@ For all information related to the ongoing bug bounty for these contracts run by ---

- +

diff --git a/contracts/MapleWithdrawalManager.sol b/contracts/MapleWithdrawalManager.sol index e693114..a1ccc62 100644 --- a/contracts/MapleWithdrawalManager.sol +++ b/contracts/MapleWithdrawalManager.sol @@ -16,6 +16,8 @@ import { import { MapleWithdrawalManagerStorage } from "./proxy/MapleWithdrawalManagerStorage.sol"; +import { SortedLinkedList } from "./utils/SortedLinkedList.sol"; + /* ███╗ ███╗ █████╗ ██████╗ ██╗ ███████╗ @@ -25,7 +27,6 @@ import { MapleWithdrawalManagerStorage } from "./proxy/MapleWithdrawalManagerSto ██║ ╚═╝ ██║██║ ██║██║ ███████╗███████╗ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚══════╝╚══════╝ - ██╗ ██╗██╗████████╗██╗ ██╗██████╗ ██████╗ █████╗ ██╗ ██╗ █████╗ ██╗ ██║ ██║██║╚══██╔══╝██║ ██║██╔══██╗██╔══██╗██╔══██╗██║ ██║██╔══██╗██║ ██║ █╗ ██║██║ ██║ ███████║██║ ██║██████╔╝███████║██║ █╗ ██║███████║██║ @@ -33,7 +34,6 @@ import { MapleWithdrawalManagerStorage } from "./proxy/MapleWithdrawalManagerSto ╚███╔███╔╝██║ ██║ ██║ ██║██████╔╝██║ ██║██║ ██║╚███╔███╔╝██║ ██║███████╗ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚══════╝ - ███╗ ███╗ █████╗ ███╗ ██╗ █████╗ ██████╗ ███████╗██████╗ ████╗ ████║██╔══██╗████╗ ██║██╔══██╗██╔════╝ ██╔════╝██╔══██╗ ██╔████╔██║███████║██╔██╗ ██║███████║██║ ███╗█████╗ ██████╔╝ @@ -41,6 +41,13 @@ import { MapleWithdrawalManagerStorage } from "./proxy/MapleWithdrawalManagerSto ██║ ╚═╝ ██║██║ ██║██║ ╚████║██║ ██║╚██████╔╝███████╗██║ ██║ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ + ██╗ ██╗██████╗ ██████╗ ██████╗ + ██║ ██║╚════██╗██╔═████╗██╔═████╗ + ██║ ██║ █████╔╝██║██╔██║██║██╔██║ + ╚██╗ ██╔╝██╔═══╝ ████╔╝██║████╔╝██║ + ╚████╔╝ ███████╗╚██████╔╝╚██████╔╝ + ╚═══╝ ╚══════╝ ╚═════╝ ╚═════╝ + */ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManagerStorage , MapleProxiedInternals { @@ -59,39 +66,37 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag _locked = 1; } - modifier onlyRedeemer { + modifier onlyPoolDelegateOrOperationalAdmin { address globals_ = globals(); require( msg.sender == IPoolManagerLike(poolManager).poolDelegate() || - msg.sender == IGlobalsLike(globals_).governor() || - msg.sender == IGlobalsLike(globals_).operationalAdmin() || - IGlobalsLike(globals_).isInstanceOf("WITHDRAWAL_REDEEMER", msg.sender), - "WM:NOT_REDEEMER" + msg.sender == IGlobalsLike(globals_).operationalAdmin(), + "WM:NOT_POOL_DELEG_OR_OPS_ADMIN" ); _; } - modifier onlyPoolDelegateOrProtocolAdmins { + modifier onlyPoolManager { + require(msg.sender == poolManager, "WM:NOT_PM"); + + _; + } + + modifier onlyRedeemer { address globals_ = globals(); require( + IGlobalsLike(globals_).isInstanceOf("WITHDRAWAL_REDEEMER", msg.sender) || msg.sender == IPoolManagerLike(poolManager).poolDelegate() || - msg.sender == IGlobalsLike(globals_).governor() || msg.sender == IGlobalsLike(globals_).operationalAdmin(), - "WM:NOT_PD_OR_GOV_OR_OA" + "WM:NOT_REDEEMER" ); _; } - modifier onlyPoolManager { - require(msg.sender == poolManager, "WM:NOT_PM"); - - _; - } - modifier whenProtocolNotPaused() { require(!IGlobalsLike(globals()).isFunctionPaused(msg.sig), "WM:PAUSED"); _; @@ -128,25 +133,13 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag } /**************************************************************************************************************************************/ - /*** State-Changing Functions ***/ + /*** State-Changing Functions - OnlyPoolManager ***/ /**************************************************************************************************************************************/ - function addShares(uint256 shares_, address owner_) external override onlyPoolManager { - require(shares_ > 0, "WM:AS:ZERO_SHARES"); - require(requestIds[owner_] == 0, "WM:AS:IN_QUEUE"); + function addShares(uint256 shares_, address owner_) external override onlyPoolManager returns (uint256 lastRequestId_) { + require(shares_ > 0, "WM:AS:ZERO_SHARES"); - uint128 lastRequestId_ = ++queue.lastRequestId; - - queue.requests[lastRequestId_] = WithdrawalRequest(owner_, shares_); - - requestIds[owner_] = lastRequestId_; - - // Increase the number of shares locked. - totalShares += shares_; - - require(ERC20Helper.transferFrom(pool, msg.sender, address(this), shares_), "WM:AS:FAILED_TRANSFER"); - - emit RequestCreated(lastRequestId_, owner_, shares_); + lastRequestId_ = _addRequest(owner_, shares_); } function processExit( @@ -163,6 +156,52 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag : _processManualExit(shares_, owner_); } + function removeShares(uint256 shares_, address owner_) external override onlyPoolManager returns (uint256 sharesReturned_) { + require(shares_ > 0, "WM:RS:ZERO_SHARES"); + + uint256 totalEscrowedShares_ = userEscrowedShares[owner_]; + + require(totalEscrowedShares_ >= shares_, "WM:RS:INSUFFICIENT_SHARES"); + + while (sharesReturned_ < shares_) { + uint256 requestId_ = SortedLinkedList.getLast(_userRequests[owner_]); + WithdrawalRequest memory request_ = queue.requests[_toUint128(requestId_)]; + + uint256 sharesToRemove_ = _min(shares_ - sharesReturned_, request_.shares); + sharesReturned_ += _removeShares(requestId_, sharesToRemove_, owner_, request_.shares); + } + } + + /**************************************************************************************************************************************/ + /*** State-Changing Functions - OnlyRedeemer ***/ + /**************************************************************************************************************************************/ + + function processEmptyRedemptions(uint256 numberOfRequests_) external override whenProtocolNotPaused onlyRedeemer { + require(numberOfRequests_ > 0, "WM:PER:ZERO_REQUESTS"); + + uint256 nextRequestId_ = queue.nextRequestId; + uint256 lastRequestId_ = queue.lastRequestId; + uint256 requestsProcessed_ = 0; + + // Iterate through the queue and process empty requests, if the owner is address(0). + while (requestsProcessed_ < numberOfRequests_ && nextRequestId_ <= lastRequestId_) { + address owner_ = queue.requests[_toUint128(nextRequestId_)].owner; + + if (owner_ != address(0)) { + // Stop if we encounter a non-empty request. + break; + } + + ++nextRequestId_; + ++requestsProcessed_; + } + + // Update the queue's next request ID. + queue.nextRequestId = _toUint128(nextRequestId_); + + emit EmptyRedemptionsProcessed(requestsProcessed_); + } + function processRedemptions(uint256 maxSharesToProcess_) external override whenProtocolNotPaused nonReentrant onlyRedeemer { require(maxSharesToProcess_ > 0, "WM:PR:ZERO_SHARES"); @@ -171,8 +210,8 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag // Revert if there are insufficient assets to redeem all shares. require(maxSharesToProcess_ == redeemableShares_, "WM:PR:LOW_LIQUIDITY"); - uint128 nextRequestId_ = queue.nextRequestId; - uint128 lastRequestId_ = queue.lastRequestId; + uint256 nextRequestId_ = queue.nextRequestId; + uint256 lastRequestId_ = queue.lastRequestId; // Iterate through the loop and process as many requests as possible. // Stop iterating when there are no more shares to process or if you have reached the end of the queue. @@ -189,64 +228,118 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag } // Adjust the new start of the queue. - queue.nextRequestId = nextRequestId_; + queue.nextRequestId = _toUint128(nextRequestId_); } - function removeShares(uint256 shares_, address owner_) external override onlyPoolManager returns (uint256 sharesReturned_) { - uint128 requestId_ = requestIds[owner_]; - - require(shares_ > 0, "WM:RS:ZERO_SHARES"); - require(requestId_ > 0, "WM:RS:NOT_IN_QUEUE"); + // NOTE: Not to be used in a router based system where the router is managing user requests. + function removeRequest( + address owner_, + uint256[] calldata requestIds_ + ) + external override whenProtocolNotPaused onlyRedeemer + { + require(owner_ != address(0), "WM:RR:ZERO_OWNER"); + require(requestIds_.length > 0, "WM:RR:ZERO_REQUESTS"); - uint256 currentShares_ = queue.requests[requestId_].shares; + uint256 sharesToRemove_; - require(shares_ <= currentShares_, "WM:RS:INSUFFICIENT_SHARES"); + WithdrawalRequest memory withdrawalRequest_; - uint256 sharesRemaining_ = currentShares_ - shares_; + for (uint256 i = 0; i < requestIds_.length; ++i) { + withdrawalRequest_ = queue.requests[_toUint128(requestIds_[i])]; - totalShares -= shares_; + require(withdrawalRequest_.shares > 0, "WM:RR:NOT_IN_QUEUE"); + require(withdrawalRequest_.owner == owner_, "WM:RR:NOT_OWNER"); - // If there are no shares remaining, cancel the withdrawal request. - if (sharesRemaining_ == 0) { - _removeRequest(owner_, requestId_); - } else { - queue.requests[requestId_].shares = sharesRemaining_; + _removeRequest(owner_, requestIds_[i]); - emit RequestDecreased(requestId_, shares_); + sharesToRemove_ += withdrawalRequest_.shares; } - require(ERC20Helper.transfer(pool, owner_, shares_), "WM:RS:TRANSFER_FAIL"); + require(ERC20Helper.transfer(pool, owner_, sharesToRemove_), "WM:RR:TRANSFER_FAIL"); - sharesReturned_ = shares_; + totalShares -= sharesToRemove_; } - function removeRequest(address owner_) external override whenProtocolNotPaused onlyPoolDelegateOrProtocolAdmins { - uint128 requestId_ = requestIds[owner_]; + function setManualWithdrawal( + address owner_, + bool isManual_ + ) + external override whenProtocolNotPaused onlyPoolDelegateOrOperationalAdmin + { + isManualWithdrawal[owner_] = isManual_; - require(requestId_ > 0, "WM:RR:NOT_IN_QUEUE"); + emit ManualWithdrawalSet(owner_, isManual_); + } - uint256 shares_ = queue.requests[requestId_].shares; + /**************************************************************************************************************************************/ + /*** Unprivileged External Functions ***/ + /**************************************************************************************************************************************/ - totalShares -= shares_; + function removeSharesById( + uint256 requestId_, + uint256 sharesToRemove_ + ) + external override whenProtocolNotPaused nonReentrant returns (uint256 sharesReturned_, uint256 sharesRemaining_) + { + WithdrawalRequest memory request_ = queue.requests[_toUint128(requestId_)]; - _removeRequest(owner_, requestId_); + require(request_.owner != address(0), "WM:RSBI:INVALID_REQUEST"); + require(request_.owner == msg.sender, "WM:RSBI:NOT_OWNER"); + require(sharesToRemove_ != 0, "WM:RSBI:NO_CHANGE"); + require(sharesToRemove_ <= request_.shares, "WM:RSBI:INSUFFICIENT_SHARES"); - require(ERC20Helper.transfer(pool, owner_, shares_), "WM:RR:TRANSFER_FAIL"); + // Removes shares and will cancel the request if there are no shares remaining. + sharesReturned_ = _removeShares(requestId_, sharesToRemove_, request_.owner, request_.shares); + sharesRemaining_ = request_.shares - sharesToRemove_; } - function setManualWithdrawal(address owner_, bool isManual_) external override whenProtocolNotPaused onlyPoolDelegateOrProtocolAdmins { - uint128 requestId_ = requestIds[owner_]; + /**************************************************************************************************************************************/ + /*** Internal Functions ***/ + /**************************************************************************************************************************************/ - require(requestId_ == 0, "WM:SMW:IN_QUEUE"); + function _addRequest(address owner_, uint256 shares_) internal returns (uint256 lastRequestId_) { + lastRequestId_ = ++queue.lastRequestId; - isManualWithdrawal[owner_] = isManual_; + queue.requests[_toUint128(lastRequestId_)] = WithdrawalRequest(owner_, shares_); + userEscrowedShares[owner_] += shares_; - emit ManualWithdrawalSet(owner_, isManual_); + SortedLinkedList.push(_userRequests[owner_], _toUint128(lastRequestId_)); + + // Increase the number of shares locked. + totalShares += shares_; + + require(ERC20Helper.transferFrom(pool, msg.sender, address(this), shares_), "WM:AS:FAILED_TRANSFER"); + + emit RequestCreated(lastRequestId_, owner_, shares_); } - /**************************************************************************************************************************************/ - /*** Internal Functions ***/ - /**************************************************************************************************************************************/ + function _removeShares( + uint256 requestId_, + uint256 sharesToRemove_, + address owner_, + uint256 currentShares_ + ) + internal returns (uint256 sharesReturned_) + { + uint256 sharesRemaining_ = currentShares_ - sharesToRemove_; + + totalShares -= sharesToRemove_; + + // If there are no shares remaining, cancel the withdrawal request. + if (sharesRemaining_ == 0) { + _removeRequest(owner_, requestId_); + } else { + queue.requests[_toUint128(requestId_)].shares = sharesRemaining_; + userEscrowedShares[owner_] -= sharesToRemove_; + + emit RequestDecreased(requestId_, sharesToRemove_); + } + + require(ERC20Helper.transfer(pool, owner_, sharesToRemove_), "WM:RS:TRANSFER_FAIL"); + + sharesReturned_ = sharesToRemove_; + } function _calculateRedemption(uint256 sharesToRedeem_) internal view returns (uint256 redeemableShares_, uint256 resultingAssets_) { IPoolManagerLike poolManager_ = IPoolManagerLike(poolManager); @@ -293,7 +386,7 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag } function _processRequest( - uint128 requestId_, + uint256 requestId_, uint256 maximumSharesToProcess_ ) internal returns ( @@ -301,7 +394,7 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag bool isProcessed_ ) { - WithdrawalRequest memory request_ = queue.requests[requestId_]; + WithdrawalRequest memory request_ = queue.requests[_toUint128(requestId_)]; // If the request has already been cancelled, skip it. if (request_.owner == address(0)) return (0, true); @@ -314,8 +407,10 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag ( processedShares_, resultingAssets_ ) = _calculateRedemption(sharesToProcess_); + uint256 sharesRemaining_ = request_.shares - processedShares_; + // If there are no remaining shares, request has been fully processed. - isProcessed_ = (request_.shares - processedShares_) == 0; + isProcessed_ = sharesRemaining_ == 0; emit RequestProcessed(requestId_, request_.owner, processedShares_, resultingAssets_); @@ -324,7 +419,8 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag _removeRequest(request_.owner, requestId_); } else { // Update the withdrawal request. - queue.requests[requestId_].shares = request_.shares - processedShares_; + queue.requests[_toUint128(requestId_)].shares = sharesRemaining_; + userEscrowedShares[request_.owner] -= processedShares_; emit RequestDecreased(requestId_, processedShares_); } @@ -333,7 +429,7 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag if (isManualWithdrawal[request_.owner]) { manualSharesAvailable[request_.owner] += processedShares_; - emit ManualSharesIncreased(request_.owner, processedShares_); + emit ManualSharesIncreased(requestId_, request_.owner, processedShares_); } else { // Otherwise, just adjust totalShares and perform the redeem. totalShares -= processedShares_; @@ -342,13 +438,19 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag } } - function _removeRequest(address owner_, uint128 requestId_) internal { - delete requestIds[owner_]; - delete queue.requests[requestId_]; + function _removeRequest(address owner_, uint256 requestId_) internal { + userEscrowedShares[owner_] -= queue.requests[_toUint128(requestId_)].shares; + SortedLinkedList.remove(_userRequests[owner_], _toUint128(requestId_)); + delete queue.requests[_toUint128(requestId_)]; emit RequestRemoved(requestId_); } + function _toUint128(uint256 input_) internal pure returns (uint128 output_) { + require(input_ <= uint256(type(uint128).max), "WM:TU:UINT256_CAST"); + output_ = uint128(input_); + } + /**************************************************************************************************************************************/ /*** View Functions ***/ /**************************************************************************************************************************************/ @@ -409,7 +511,7 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag require(shares_ <= sharesAvailable_, "WM:PR:TOO_MANY_SHARES"); - ( redeemableShares_, resultingAssets_ ) = _calculateRedemption(shares_); + ( redeemableShares_, resultingAssets_ ) = _calculateRedemption(shares_); // NOTE: Recommend using convertToExitAssets instead } function previewWithdraw(address owner_, uint256 assets_) @@ -419,9 +521,25 @@ contract MapleWithdrawalManager is IMapleWithdrawalManager, MapleWithdrawalManag return ( redeemableAssets_, resultingShares_ ); // NOTE: Withdrawal not implemented use redeem instead } - function requests(uint128 requestId_) external view override returns (address owner_, uint256 shares_) { - owner_ = queue.requests[requestId_].owner; - shares_ = queue.requests[requestId_].shares; + function requestIds(address owner_) external view override returns (uint256 requestId_) { + requestId_ = SortedLinkedList.getLast(_userRequests[owner_]); + } + + function requests(uint256 requestId_) external view override returns (address owner_, uint256 shares_) { + owner_ = queue.requests[_toUint128(requestId_)].owner; + shares_ = queue.requests[_toUint128(requestId_)].shares; + } + + function requestsByOwner(address owner_) external view override returns (uint256[] memory requestIds_, uint256[] memory shares_) { + uint128[] memory requestIdsByOwner_ = SortedLinkedList.getAllValues(_userRequests[owner_]); + + requestIds_ = new uint256[](requestIdsByOwner_.length); + shares_ = new uint256[](requestIdsByOwner_.length); + + for (uint256 i = 0; i < requestIdsByOwner_.length; ++i) { + requestIds_[i] = requestIdsByOwner_[i]; + shares_[i] = queue.requests[requestIdsByOwner_[i]].shares; + } } function securityAdmin() public view override returns (address securityAdmin_) { diff --git a/contracts/interfaces/IMapleWithdrawalManager.sol b/contracts/interfaces/IMapleWithdrawalManager.sol index 261ebab..5bb096a 100644 --- a/contracts/interfaces/IMapleWithdrawalManager.sol +++ b/contracts/interfaces/IMapleWithdrawalManager.sol @@ -11,6 +11,12 @@ interface IMapleWithdrawalManager is IMapleWithdrawalManagerStorage, IMapleProxi /*** Events ***/ /**************************************************************************************************************************************/ + /** + * @dev Emitted when empty redemption requests are processed. + * @param numberOfRequestsProcessed Number of empty requests that were processed. + */ + event EmptyRedemptionsProcessed(uint256 numberOfRequestsProcessed); + /** * @dev Emitted when a manual redemption takes place. * @param owner Address of the account. @@ -20,10 +26,11 @@ interface IMapleWithdrawalManager is IMapleWithdrawalManagerStorage, IMapleProxi /** * @dev Emitted when a manual redemption is processed. + * @param requestId Identifier of the withdrawal request. * @param owner Address of the account. * @param sharesAdded Amount of shares added to the redeemable amount. */ - event ManualSharesIncreased(address indexed owner, uint256 sharesAdded); + event ManualSharesIncreased(uint256 indexed requestId, address indexed owner, uint256 sharesAdded); /** * @dev Emitted when the withdrawal type of an account is updated. @@ -38,14 +45,14 @@ interface IMapleWithdrawalManager is IMapleWithdrawalManagerStorage, IMapleProxi * @param owner Address of the owner of the shares. * @param shares Amount of shares requested for redemption. */ - event RequestCreated(uint128 indexed requestId, address indexed owner, uint256 shares); + event RequestCreated(uint256 indexed requestId, address indexed owner, uint256 shares); /** * @dev Emitted when a withdrawal request is updated. * @param requestId Identifier of the withdrawal request. * @param shares Amount of shares reduced during a redemption request. */ - event RequestDecreased(uint128 indexed requestId, uint256 shares); + event RequestDecreased(uint256 indexed requestId, uint256 shares); /** * @dev Emitted when a withdrawal request is processed. @@ -54,13 +61,13 @@ interface IMapleWithdrawalManager is IMapleWithdrawalManagerStorage, IMapleProxi * @param shares Amount of redeemable shares. * @param assets Amount of withdrawable assets. */ - event RequestProcessed(uint128 indexed requestId, address indexed owner, uint256 shares, uint256 assets); + event RequestProcessed(uint256 indexed requestId, address indexed owner, uint256 shares, uint256 assets); /** * @dev Emitted when a withdrawal request is removed. * @param requestId Identifier of the withdrawal request. */ - event RequestRemoved(uint128 indexed requestId); + event RequestRemoved(uint256 indexed requestId); /**************************************************************************************************************************************/ /*** State-Changing Functions ***/ @@ -71,7 +78,16 @@ interface IMapleWithdrawalManager is IMapleWithdrawalManagerStorage, IMapleProxi * @param shares Amount of shares to add. * @param owner Address of the owner of shares. */ - function addShares(uint256 shares, address owner) external; + function addShares(uint256 shares, address owner) external returns (uint256 lastRequestId); + + /** + * @dev Processes empty redemption requests at the front of the queue. + * Iterates through the queue starting from the front and advances the queue's nextRequestId + * for each empty request encountered. Stops when a non-empty request is found or the + * specified number of requests has been processed. + * @param numberOfRequests Maximum number of empty requests to process. + */ + function processEmptyRedemptions(uint256 numberOfRequests) external; /** * @dev Processes a withdrawal request. @@ -100,11 +116,22 @@ interface IMapleWithdrawalManager is IMapleWithdrawalManagerStorage, IMapleProxi function removeShares(uint256 shares, address owner) external returns (uint256 sharesReturned); /** - * @dev Removes a withdrawal request from the queue. + * @dev Remove shares from a specific withdrawal request. + * @param requestId Identifier of the withdrawal request that is being updated. + * @param sharesToRemove Amount of shares to remove from the request. + * @return sharesReturned Amount of shares that were returned. + * @return sharesRemaining Amount of shares remaining in the request. + */ + function removeSharesById(uint256 requestId, uint256 sharesToRemove) external returns (uint256 sharesReturned, uint256 sharesRemaining); + + /** + * @dev Removes withdrawal requests from the queue. * Can only be called by the pool delegate. - * @param owner Address of the owner of shares. + * NOTE: Not to be used in a router based system where the router is managing user requests. + * @param owner Address of the owner of shares. + * @param requestIds Array of identifiers of the withdrawal requests to remove. */ - function removeRequest(address owner) external; + function removeRequest(address owner, uint256[] calldata requestIds) external; /** * @dev Defines if an account will withdraw shares manually or automatically. @@ -138,24 +165,24 @@ interface IMapleWithdrawalManager is IMapleWithdrawalManagerStorage, IMapleProxi /** * @dev Returns if a user is able to withdraw. Required for compatibility with pool managers. * NOTE: Always returns true to fulfil interface requirements. - * @param owner_ The account to check if it's in withdraw window. - * @return isInExitWindow_ True if the account is in the withdraw window. + * @param owner The account to check if it's in withdraw window. + * @return isInExitWindow True if the account is in the withdraw window. */ - function isInExitWindow(address owner_) external view returns (bool isInExitWindow_); + function isInExitWindow(address owner) external view returns (bool isInExitWindow); /** * @dev Gets the total amount of funds that need to be locked to fulfill exits. * NOTE: Always zero for this implementation. - * @return lockedLiquidity_ The amount of locked liquidity. + * @return lockedLiquidity The amount of locked liquidity. */ - function lockedLiquidity() external view returns (uint256 lockedLiquidity_); + function lockedLiquidity() external view returns (uint256 lockedLiquidity); /** * @dev Gets the amount of locked shares for an account. - * @param owner_ The address to check the exit for. - * @return lockedShares_ The amount of manual shares available. + * @param owner The address to check the exit for. + * @return lockedShares The amount of manual shares available. */ - function lockedShares(address owner_) external view returns (uint256 lockedShares_); + function lockedShares(address owner) external view returns (uint256 lockedShares); /** * @dev Returns the address of the pool delegate. @@ -176,12 +203,20 @@ interface IMapleWithdrawalManager is IMapleWithdrawalManagerStorage, IMapleProxi /** * @dev Gets the amount of shares that can be withdrawn. * NOTE: Values just passed through as withdraw is not implemented. - * @param owner_ The address to check the withdrawal for. - * @param assets_ The amount of requested shares to withdraw. - * @return redeemableAssets_ The amount of assets that can be withdrawn. - * @return resultingShares_ The amount of shares that will be burned. + * @param owner The address to check the withdrawal for. + * @param assets The amount of requested shares to withdraw. + * @return redeemableAssets The amount of assets that can be withdrawn. + * @return resultingShares The amount of shares that will be burned. + */ + function previewWithdraw(address owner, uint256 assets) external view returns (uint256 redeemableAssets, uint256 resultingShares); + + /** + * @dev Returns the last request id for a given owner. + * Function must exist for backwards compatibility with the old implementation where we supported only one request per owner. + * @param owner The account to check the last request id for. + * @return requestId The id of the last valid withdrawal request for the account. */ - function previewWithdraw(address owner_, uint256 assets_) external view returns (uint256 redeemableAssets_, uint256 resultingShares_); + function requestIds(address owner) external view returns (uint256 requestId); /** * @dev Returns the owner and amount of shares associated with a withdrawal request. @@ -189,7 +224,16 @@ interface IMapleWithdrawalManager is IMapleWithdrawalManagerStorage, IMapleProxi * @return owner Address of the share owner. * @return shares Amount of shares pending redemption. */ - function requests(uint128 requestId) external view returns (address owner, uint256 shares); + function requests(uint256 requestId) external view returns (address owner, uint256 shares); + + /** + * @dev Returns the pending requests by owner. + * NOTE: This function may run out of gas if there are too many requests. Use the overload with pagination. + * @param owner Address of the account to check for pending requests. + * @return requestIds Array of request identifiers. + * @return shares Array of shares associated with each request. + */ + function requestsByOwner(address owner) external view returns (uint256[] memory requestIds, uint256[] memory shares); /** * @dev Returns the address of the security admin. diff --git a/contracts/interfaces/IMapleWithdrawalManagerStorage.sol b/contracts/interfaces/IMapleWithdrawalManagerStorage.sol index 9b40966..7288bc7 100644 --- a/contracts/interfaces/IMapleWithdrawalManagerStorage.sol +++ b/contracts/interfaces/IMapleWithdrawalManagerStorage.sol @@ -36,12 +36,11 @@ interface IMapleWithdrawalManagerStorage { function manualSharesAvailable(address owner) external view returns (uint256 sharesAvailable); /** - * @dev Returns the request identifier of an account. - * Returns zero if the account does not have a withdrawal request. - * @param account Address of the account. - * @return requestId Identifier of the withdrawal request. + * @dev Returns the amount of shares escrowed for a specific user yet to be processed. + * @param owner The address of the owner of shares. + * @return escrowedShares Amount of shares escrowed for the user. */ - function requestIds(address account) external view returns (uint128 requestId); + function userEscrowedShares(address owner) external view returns (uint256 escrowedShares); /** * @dev Returns the first and last withdrawal requests pending redemption. @@ -49,5 +48,5 @@ interface IMapleWithdrawalManagerStorage { * @return lastRequestId Identifier of the last created withdrawal request. */ function queue() external view returns (uint128 nextRequestId, uint128 lastRequestId); - + } diff --git a/contracts/proxy/MapleWithdrawalManagerMigratorV200.sol b/contracts/proxy/MapleWithdrawalManagerMigratorV200.sol new file mode 100644 index 0000000..e5d3034 --- /dev/null +++ b/contracts/proxy/MapleWithdrawalManagerMigratorV200.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.7; + +import { MapleProxiedInternals } from "../../modules/maple-proxy-factory/contracts/MapleProxiedInternals.sol"; + +import { SortedLinkedList } from "../utils/SortedLinkedList.sol"; + +import { MapleWithdrawalManagerStorage } from "./MapleWithdrawalManagerStorage.sol"; + +contract MapleWithdrawalManagerMigratorV200 is MapleProxiedInternals, MapleWithdrawalManagerStorage { + + fallback() external { + uint128 nextRequestId_ = queue.nextRequestId; + uint128 lastRequestId_ = queue.lastRequestId; + + for (uint128 i = nextRequestId_; i <= lastRequestId_; ++i) { + WithdrawalRequest memory request_ = queue.requests[i]; + + if (request_.owner == address(0)) continue; + + userEscrowedShares[request_.owner] += request_.shares; + + SortedLinkedList.push(_userRequests[request_.owner], i); + } + } + +} diff --git a/contracts/proxy/MapleWithdrawalManagerStorage.sol b/contracts/proxy/MapleWithdrawalManagerStorage.sol index 71dc763..ca92292 100644 --- a/contracts/proxy/MapleWithdrawalManagerStorage.sol +++ b/contracts/proxy/MapleWithdrawalManagerStorage.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.7; import { IMapleWithdrawalManagerStorage } from "../interfaces/IMapleWithdrawalManagerStorage.sol"; +import { SortedLinkedList } from "../utils/SortedLinkedList.sol"; + contract MapleWithdrawalManagerStorage is IMapleWithdrawalManagerStorage { /**************************************************************************************************************************************/ @@ -25,7 +27,7 @@ contract MapleWithdrawalManagerStorage is IMapleWithdrawalManagerStorage { /**************************************************************************************************************************************/ uint256 internal _locked; // Used when checking for reentrancy. - + address public override pool; address public override poolManager; @@ -35,8 +37,12 @@ contract MapleWithdrawalManagerStorage is IMapleWithdrawalManagerStorage { mapping(address => bool) public override isManualWithdrawal; // Defines which users use automated withdrawals (false by default). - mapping(address => uint128) public override requestIds; // Maps users to their withdrawal requests identifiers. + mapping(address => uint128) internal __deprecated_requestIds; // Maps users to their last withdrawal request. mapping(address => uint256) public override manualSharesAvailable; // Shares available to withdraw for a given manual owner. + mapping(address => uint256) public override userEscrowedShares; // Maps users to their escrowed shares yet to be processed. + + mapping(address => SortedLinkedList.List) internal _userRequests; // Maps users to their withdrawal requests. + } diff --git a/contracts/utils/SortedLinkedList.sol b/contracts/utils/SortedLinkedList.sol new file mode 100644 index 0000000..97f1f1e --- /dev/null +++ b/contracts/utils/SortedLinkedList.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.7; + +library SortedLinkedList { + + struct Node { + uint128 next; + uint128 prev; + bool exists; + } + + struct List { + uint128 head; + uint128 tail; + uint256 size; + + mapping(uint128 => Node) nodes; + } + + /**************************************************************************************************************************************/ + /*** Write Functions ***/ + /**************************************************************************************************************************************/ + + /** + * @dev Pushes a value to the list. + * It is expected that the value is biggest so far so it will be added at the end of the list. + * @param list The list to push the value to. + * @param value_ The value to push to the list. + */ + function push(List storage list, uint128 value_) internal { + uint128 tail_ = list.tail; + + require(value_ > 0, "SLL:P:ZERO_VALUE"); + require(!contains(list, value_), "SLL:P:VALUE_EXISTS"); + require(value_ > tail_, "SLL:P:NOT_LARGEST"); + + list.nodes[value_] = Node({ + next: 0, + prev: tail_, + exists: true + }); + + if (tail_ != 0) { + list.nodes[tail_].next = value_; + } + + list.tail = value_; + + if (list.head == 0) { + list.head = value_; + } + + list.size++; + } + + /** + * @dev Removes a value from the list in O(1) time. + * @param list The list to remove the value from. + * @param value_ The value to remove from the list. + */ + function remove(List storage list, uint128 value_) internal { + require(contains(list, value_), "SLL:R:VALUE_NOT_EXISTS"); + + uint128 prev_ = list.nodes[value_].prev; + uint128 next_ = list.nodes[value_].next; + + if (prev_ != 0) { + list.nodes[prev_].next = next_; + } else { + list.head = next_; + } + + if (next_ != 0) { + list.nodes[next_].prev = prev_; + } else { + list.tail = prev_; + } + + delete list.nodes[value_]; + list.size--; + } + + /**************************************************************************************************************************************/ + /*** View Functions ***/ + /**************************************************************************************************************************************/ + + /** + * @dev Gets the length of the list. + * @param list The list to get the length of. + * @return length_ The length of the list. + */ + function length(List storage list) internal view returns (uint256 length_) { + length_ = list.size; + } + + /** + * @dev Gets all values from the list. + * @param list The list to get the values from. + * @return values_ All values from the list. + */ + function getAllValues(List storage list) internal view returns (uint128[] memory values_) { + values_ = new uint128[](list.size); + + uint128 current_ = list.head; + uint256 size_ = list.size; + + for (uint256 i = 0; i < size_; i++) { + values_[i] = current_; + current_ = list.nodes[current_].next; + } + } + + /** + * @dev Gets the last value in the list. + * @param list The list to get the last value from. + * @return value_ The last value in the list. + */ + function getLast(List storage list) internal view returns (uint128 value_) { + value_ = list.tail; + } + + /** + * @dev Checks if a value exists in the list. + * @param list The list to check. + * @param value_ The value to check for. + * @return exists_ True if the value exists in the list. + */ + function contains(List storage list, uint128 value_) internal view returns (bool exists_) { + exists_ = list.nodes[value_].exists; + } + +} diff --git a/foundry.toml b/foundry.toml index 05293fe..172f501 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,8 +8,8 @@ verbosity = 3 # The verbosity of tests block_timestamp = 1_622_400_000 # Timestamp for tests (non-zero) fuzz_runs = 100 # Number of fuzz runs -[profile.deep] -fuzz_runs = 1000 +[profile.ci.fuzz] +runs = 1000 [profile.super_deep] fuzz_runs = 50000 diff --git a/tests/fuzz/AddSharesFuzz.t.sol b/tests/fuzz/AddSharesFuzz.t.sol index 93e8176..0c079c4 100644 --- a/tests/fuzz/AddSharesFuzz.t.sol +++ b/tests/fuzz/AddSharesFuzz.t.sol @@ -9,23 +9,15 @@ contract AddSharesFuzzTests is TestBase { super.setUp(); } - function testFuzz_addShares(uint256[50] memory amount_, address[50] calldata account_) external { - address owner_; - - uint128 lastRequestId; - uint256 shares_; + function testFuzz_addShares(uint256[10] memory amount_, address[10] calldata account_) external { + uint256 lastRequestId; uint256 totalShares_; + uint256 requestId_; + uint256 shares_; for (uint256 i; i < account_.length; ++i) { amount_[i] = bound(amount_[i], 1, 1e29); - if (withdrawalManager.requestIds(account_[i]) > 0) { - vm.prank(pm); - vm.expectRevert("WM:AS:IN_QUEUE"); - withdrawalManager.addShares(amount_[i], account_[i]); - break; - } - pool.mint(pm, amount_[i]); vm.startPrank(pm); @@ -40,11 +32,11 @@ contract AddSharesFuzzTests is TestBase { assertEq(lastRequestId, i + 1); - ( owner_, shares_ ) = withdrawalManager.requests(lastRequestId); + (requestId_, shares_) = getLastRequestByOwner(account_[i]); - assertEq(shares_, amount_[i]); - assertEq(withdrawalManager.totalShares(), totalShares_); - assertEq(withdrawalManager.requestIds(owner_), lastRequestId); + assertEq(shares_, amount_[i]); + assertEq(withdrawalManager.totalShares(), totalShares_); + assertEq(requestId_, lastRequestId); } } diff --git a/tests/fuzz/RemoveSharesFuzz.t.sol b/tests/fuzz/RemoveSharesFuzz.t.sol index e8311b5..52eed7d 100644 --- a/tests/fuzz/RemoveSharesFuzz.t.sol +++ b/tests/fuzz/RemoveSharesFuzz.t.sol @@ -9,18 +9,20 @@ contract RemoveSharesFuzzTests is TestBase { super.setUp(); } - function testFuzz_removeShares(address[50] calldata account_, uint256[50] memory amount0_, uint256[50] memory amount1_) external { + function testFuzz_removeShares(address[10] calldata account_, uint256[10] memory amount0_, uint256[10] memory amount1_) external { address owner_; - uint128 lastRequestId; + uint256 lastRequestId; uint256 shares_; uint256 totalShares_; + uint256 requestId_; for (uint256 i; i < account_.length; ++i) { amount0_[i] = bound(amount0_[i], 1, 1e29); amount1_[i] = bound(amount1_[i], 1, 1e29); - if (withdrawalManager.requestIds(account_[i]) > 0) break; + ( requestId_, ) = getLastRequestByOwner(account_[i]); + if (requestId_ > 0) break; pool.mint(pm, amount0_[i]); @@ -36,10 +38,12 @@ contract RemoveSharesFuzzTests is TestBase { ( owner_, shares_ ) = withdrawalManager.requests(lastRequestId); - assertEq(shares_, amount0_[i]); - assertEq(withdrawalManager.totalShares(), totalShares_); - assertEq(lastRequestId, i + 1); - assertEq(withdrawalManager.requestIds(owner_), lastRequestId); + ( requestId_, ) = getLastRequestByOwner(account_[i]); + + assertEq(shares_, amount0_[i]); + assertEq(withdrawalManager.totalShares(), totalShares_); + assertEq(lastRequestId, i + 1); + assertEq(requestId_, lastRequestId); if (amount1_[i] > amount0_[i]) { vm.prank(pm); @@ -61,10 +65,11 @@ contract RemoveSharesFuzzTests is TestBase { assertEq(withdrawalManager.totalShares(), totalShares_); assertEq(lastRequestId, i + 1); + ( requestId_, ) = getLastRequestByOwner(owner_); if (amount0_[i] == amount1_[i]) { - assertEq(withdrawalManager.requestIds(owner_), 0); + assertEq(requestId_, 0); } else { - assertEq(withdrawalManager.requestIds(owner_), lastRequestId); + assertEq(requestId_, lastRequestId); } } } diff --git a/tests/integration/EndToEndTests.t.sol b/tests/integration/EndToEndTests.t.sol index e128f02..3d2f24d 100644 --- a/tests/integration/EndToEndTests.t.sol +++ b/tests/integration/EndToEndTests.t.sol @@ -6,7 +6,7 @@ import { console, TestBase } from "../utils/TestBase.sol"; contract EndToEndTests is TestBase { // Helper storage variable for fuzz test - mapping(uint128 => address) lpsRequest; + mapping(uint256 => address) lpsRequest; mapping(address => bool) manualLps; mapping(address => uint256) lpShares; @@ -26,6 +26,8 @@ contract EndToEndTests is TestBase { uint256 totalInitialShares = shares1 + shares2 + shares3; + uint256 requestId_; + // Simulate shares being sent to PM pool.mint(pm, totalInitialShares); @@ -45,21 +47,27 @@ contract EndToEndTests is TestBase { assertQueue({ nextRequestId: 1, lastRequestId: 1 }); assertRequest({ requestId: 1, shares: shares1, owner: lp1 }); - assertEq(withdrawalManager.requestIds(lp1), 1); + ( requestId_, ) = getLastRequestByOwner(lp1); + + assertEq(requestId_, 1); assertEq(pool.balanceOf(address(withdrawalManager)), shares1); withdrawalManager.addShares(shares2, lp2); assertQueue({ nextRequestId: 1, lastRequestId: 2 }); assertRequest({ requestId: 2, shares: shares2, owner: lp2 }); - assertEq(withdrawalManager.requestIds(lp2), 2); + ( requestId_, ) = getLastRequestByOwner(lp2); + + assertEq(requestId_, 2); assertEq(pool.balanceOf(address(withdrawalManager)), shares1 + shares2); withdrawalManager.addShares(shares3, lp3); assertQueue({ nextRequestId: 1, lastRequestId: 3 }); assertRequest({ requestId: 3, shares: shares3, owner: lp3 }); - assertEq(withdrawalManager.requestIds(lp3), 3); + ( requestId_, ) = getLastRequestByOwner(lp3); + + assertEq(requestId_, 3); assertEq(pool.balanceOf(address(withdrawalManager)), shares1 + shares2 + shares3); vm.stopPrank(); @@ -77,7 +85,9 @@ contract EndToEndTests is TestBase { assertQueue({ nextRequestId: 1, lastRequestId: 3 }); assertRequest({ requestId: 1, shares: shares1 / 2, owner: lp1 }); - assertEq(withdrawalManager.requestIds(lp1), 1); + ( requestId_, ) = getLastRequestByOwner(lp1); + + assertEq(requestId_, 1); // Pool Delegate process rest of request 1 + half of request 2 vm.prank(poolDelegate); @@ -86,15 +96,19 @@ contract EndToEndTests is TestBase { // Shares of lp2 remain locked in wm assertEq(withdrawalManager.totalShares(), totalInitialShares - shares1); + ( requestId_, ) = getLastRequestByOwner(lp1); + // Lp1 is removed from queue, although the `request` data structure remains populated. assertQueue({ nextRequestId: 2, lastRequestId: 3 }); assertRequest({ requestId: 1, shares: 0, owner: address(0) }); - assertEq(withdrawalManager.requestIds(lp1), 0); + assertEq(requestId_, 0); // Lp2 is still on the queue, and had it's manual shares incremented. assertRequest({ requestId: 2, shares: shares2 / 2, owner: lp2 }); - assertEq(withdrawalManager.requestIds(lp2), 2); + ( requestId_, ) = getLastRequestByOwner(lp2); + + assertEq(requestId_, 2); assertEq(withdrawalManager.manualSharesAvailable(lp2), shares2 / 2); vm.prank(pm); @@ -120,13 +134,17 @@ contract EndToEndTests is TestBase { function testFuzz_fullFLow_fixedExchangeRate(address[10] memory lps, bool[10] memory isManual, uint256[10] memory shares) external { uint256 totalShares; - uint128 inQueue; + uint256 inQueue; + uint256 requestId_; // Iterate through all users and add shares to pool manager for (uint256 i = 0; i < 10; i++) { vm.assume(lps[i] != address(0)); + + ( requestId_, ) = getLastRequestByOwner(lps[i]); + // If it's a unique user, add to the queue - if (withdrawalManager.requestIds(lps[i]) == 0) { + if (requestId_ == 0) { uint256 sharesRequested = bound(shares[i], 1, 1e18); // Save each LP value to verify later @@ -160,12 +178,12 @@ contract EndToEndTests is TestBase { withdrawalManager.processRedemptions(sharesToProcess); // To avoid doing the same iteration as the function `processRedemptions`, fetch the queue state, then check it's integrity. - ( uint128 nextRequestId_, uint128 lastRequestId_ ) = withdrawalManager.queue(); + ( uint256 nextRequestId_, uint256 lastRequestId_ ) = withdrawalManager.queue(); uint256 sharesProcessed; // First, check that the requests processes are correct - for (uint128 i = 1; i < nextRequestId_; i++) { + for (uint256 i = 1; i < nextRequestId_; i++) { address lp = lpsRequest[i]; assertRequest({ requestId: i, shares: 0, owner: address(0) }); @@ -187,7 +205,7 @@ contract EndToEndTests is TestBase { } // Finally, check that all subsequent requests are still on the queue. - for (uint128 i = nextRequestId_ + 1; i <= lastRequestId_; i++) { + for (uint256 i = nextRequestId_ + 1; i <= lastRequestId_; i++) { address lp = lpsRequest[i]; assertRequest({ requestId: i, shares: lpShares[lp], owner: lp }); diff --git a/tests/unit/AddShares.t.sol b/tests/unit/AddShares.t.sol index e4a1fa5..f29233a 100644 --- a/tests/unit/AddShares.t.sol +++ b/tests/unit/AddShares.t.sol @@ -5,8 +5,6 @@ import { TestBase } from "../utils/TestBase.sol"; contract AddSharesTests is TestBase { - event RequestCreated(uint128 indexed requestId, address indexed owner, uint256 shares); - function setUp() public override { super.setUp(); @@ -28,13 +26,32 @@ contract AddSharesTests is TestBase { withdrawalManager.addShares(0, lp); } - function test_addShares_alreadyInQueue() external { + function test_addShares_multiple_requests() external{ vm.prank(pm); withdrawalManager.addShares(1, lp); vm.prank(pm); - vm.expectRevert("WM:AS:IN_QUEUE"); withdrawalManager.addShares(1, lp); + + ( , uint256 lastRequestId_ ) = withdrawalManager.queue(); + + assertEq(lastRequestId_, 2); + + assertEq(withdrawalManager.userEscrowedShares(lp), 2); + + ( address owner_, uint256 shares_ ) = withdrawalManager.requests(lastRequestId_); + + assertEq(owner_, lp); + assertEq(shares_, 1); + + (uint256[] memory requestIds, uint256[] memory requestShares) = withdrawalManager.requestsByOwner(lp); + + assertEq(requestIds.length, 2); + assertEq(requestShares.length, 2); + assertEq(requestIds[0], 1); + assertEq(requestIds[1], 2); + assertEq(requestShares[0], 1); + assertEq(requestShares[1], 1); } function test_addShares_failedTransfer() external { @@ -44,9 +61,9 @@ contract AddSharesTests is TestBase { } function test_addShares_newRequestAddedToQueue() external { - ( , uint128 lastRequestId ) = withdrawalManager.queue(); + ( , uint256 lastRequestId_ ) = withdrawalManager.queue(); - assertEq(lastRequestId, 0); + assertEq(lastRequestId_, 0); vm.expectEmit(); emit RequestCreated(1, lp, 1); @@ -54,17 +71,19 @@ contract AddSharesTests is TestBase { vm.prank(pm); withdrawalManager.addShares(1, lp); - ( , lastRequestId ) = withdrawalManager.queue(); + ( , lastRequestId_ ) = withdrawalManager.queue(); + + assertEq(lastRequestId_, 1); - assertEq(lastRequestId, 1); + assertEq(withdrawalManager.userEscrowedShares(lp), 1); } function test_addShares_newRequestAddedToQueue_manual() external { withdrawalManager.__setManualWithdrawal(lp, true); - ( , uint128 lastRequestId ) = withdrawalManager.queue(); + ( , uint256 lastRequestId_ ) = withdrawalManager.queue(); - assertEq(lastRequestId, 0); + assertEq(lastRequestId_, 0); vm.expectEmit(); emit RequestCreated(1, lp, 1); @@ -72,27 +91,34 @@ contract AddSharesTests is TestBase { vm.prank(pm); withdrawalManager.addShares(1, lp); - ( , lastRequestId ) = withdrawalManager.queue(); + ( , lastRequestId_ ) = withdrawalManager.queue(); - assertEq(lastRequestId, 1); + assertEq(lastRequestId_, 1); + + assertEq(withdrawalManager.userEscrowedShares(lp), 1); } function test_addShares_success() external { + uint256 requestId_; + vm.expectEmit(); emit RequestCreated(1, lp, 1); vm.prank(pm); withdrawalManager.addShares(1, lp); - ( , uint128 lastRequestId ) = withdrawalManager.queue(); + ( , uint256 lastRequestId_ ) = withdrawalManager.queue(); - assertEq(lastRequestId, 1); + assertEq(lastRequestId_, 1); - ( address owner_, uint256 shares_ ) = withdrawalManager.requests(lastRequestId); + assertEq(withdrawalManager.userEscrowedShares(lp), 1); - assertEq(shares_, 1); - assertEq(withdrawalManager.totalShares(), 1); - assertEq(withdrawalManager.requestIds(owner_), lastRequestId); + ( address owner_, uint256 shares_ ) = withdrawalManager.requests(lastRequestId_); + ( requestId_,) = getLastRequestByOwner(owner_); + + assertEq(shares_, 1); + assertEq(withdrawalManager.totalShares(), 1); + assertEq(requestId_, lastRequestId_); address lp2 = makeAddr("lp2"); @@ -102,15 +128,19 @@ contract AddSharesTests is TestBase { vm.prank(pm); withdrawalManager.addShares(1, lp2); - ( , lastRequestId ) = withdrawalManager.queue(); + ( , lastRequestId_ ) = withdrawalManager.queue(); + + assertEq(lastRequestId_, 2); + + assertEq(withdrawalManager.userEscrowedShares(lp), 1); - assertEq(lastRequestId, 2); + ( owner_, shares_ ) = withdrawalManager.requests(lastRequestId_); - ( owner_, shares_ ) = withdrawalManager.requests(lastRequestId); + ( requestId_,) = getLastRequestByOwner(owner_); - assertEq(shares_, 1); - assertEq(withdrawalManager.totalShares(), 2); - assertEq(withdrawalManager.requestIds(owner_), lastRequestId); + assertEq(shares_, 1); + assertEq(withdrawalManager.totalShares(), 2); + assertEq(requestId_, lastRequestId_); } } diff --git a/tests/unit/CreateInstance.t.sol b/tests/unit/CreateInstance.t.sol index cee2f2a..07a6099 100644 --- a/tests/unit/CreateInstance.t.sol +++ b/tests/unit/CreateInstance.t.sol @@ -74,9 +74,9 @@ contract CreateInstanceTests is TestBase { assertEq(withdrawalManager_.pool(), address(pool)); assertEq(withdrawalManager_.poolManager(), pm); - ( uint128 nextRequestId, ) = withdrawalManager_.queue(); + ( uint128 nextRequestId_, ) = withdrawalManager_.queue(); - assertEq(nextRequestId, 1); + assertEq(nextRequestId_, 1); } } diff --git a/tests/unit/MapleWithdrawalManagerMigrator.t.sol b/tests/unit/MapleWithdrawalManagerMigrator.t.sol new file mode 100644 index 0000000..7897339 --- /dev/null +++ b/tests/unit/MapleWithdrawalManagerMigrator.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.7; + +import { MapleWithdrawalManagerMigratorV200 } from "../../contracts/proxy/MapleWithdrawalManagerMigratorV200.sol"; +import { TestBase } from "../utils/TestBase.sol"; + +contract WithdrawalManagerMigrateTests is TestBase { + + address internal migrator; + + function setUp() public override { + super.setUp(); + + migrator = address(new MapleWithdrawalManagerMigratorV200()); + } + + function test_migrate_notFactory() external { + vm.expectRevert("WM:M:NOT_FACTORY"); + withdrawalManager.migrate(migrator, ""); + } + + function test_migrate_protocolPaused() external { + globals.__setFunctionPaused(true); + + vm.expectRevert("WM:PAUSED"); + withdrawalManager.migrate(migrator, ""); + } + + function test_migrate_success_xxx() external { + withdrawalManager.__setRequestLegacy(10, address(0x01), 1); + withdrawalManager.__setRequestLegacy(12, address(0x01), 2); + withdrawalManager.__setRequestLegacy(13, address(0x02), 4); + withdrawalManager.__setRequestLegacy(15, address(0x03), 5); + + withdrawalManager.__setQueue(10, 15); + + vm.prank(address(factory)); + withdrawalManager.migrate(migrator,""); + + ( uint128 nextRequestId_, uint128 lastRequestId_ ) = withdrawalManager.queue(); + + assert(nextRequestId_ == 10); + assert(lastRequestId_ == 15); + + assertEq(withdrawalManager.userEscrowedShares(address(0x01)), 3); + assertEq(withdrawalManager.userEscrowedShares(address(0x02)), 4); + assertEq(withdrawalManager.userEscrowedShares(address(0x03)), 5); + + address owner_; + uint256 shares_; + + ( owner_, shares_ ) = withdrawalManager.requests(10); + assertEq(owner_, address(0x01)); + assertEq(shares_, 1); + + ( owner_, shares_ ) = withdrawalManager.requests(11); + assertEq(owner_, address(0)); + assertEq(shares_, 0); + + ( owner_, shares_ ) = withdrawalManager.requests(12); + assertEq(owner_, address(0x01)); + assertEq(shares_, 2); + + ( owner_, shares_ ) = withdrawalManager.requests(13); + assertEq(owner_, address(0x02)); + assertEq(shares_, 4); + + ( owner_, shares_ ) = withdrawalManager.requests(14); + assertEq(owner_, address(0)); + assertEq(shares_, 0); + + ( owner_, shares_ ) = withdrawalManager.requests(15); + assertEq(owner_, address(0x03)); + assertEq(shares_, 5); + } + +} diff --git a/tests/unit/ProcessEmptyRedemptions.t.sol b/tests/unit/ProcessEmptyRedemptions.t.sol new file mode 100644 index 0000000..4b2394f --- /dev/null +++ b/tests/unit/ProcessEmptyRedemptions.t.sol @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.7; + +import { TestBase } from "../utils/TestBase.sol"; + +contract ProcessEmptyRedemptionsFailureTests is TestBase { + + function setUp() public override { + super.setUp(); + + globals.__setIsInstanceOf(false); + } + + function test_processEmptyRedemptions_protocolPaused() external { + globals.__setFunctionPaused(true); + + vm.prank(poolDelegate); + vm.expectRevert("WM:PAUSED"); + withdrawalManager.processEmptyRedemptions(1); + } + + function test_processEmptyRedemptions_notRedeemer() external { + vm.prank(lp); + vm.expectRevert("WM:NOT_REDEEMER"); + withdrawalManager.processEmptyRedemptions(1); + } + + function test_processEmptyRedemptions_governorNotAllowed() external { + vm.prank(governor); + vm.expectRevert("WM:NOT_REDEEMER"); + withdrawalManager.processEmptyRedemptions(1); + } + + function test_processEmptyRedemptions_zeroRequests() external { + vm.prank(poolDelegate); + vm.expectRevert("WM:PER:ZERO_REQUESTS"); + withdrawalManager.processEmptyRedemptions(0); + } + +} + +contract ProcessEmptyRedemptionsSuccessTests is TestBase { + + function setUp() public override { + super.setUp(); + + // Setup pool with assets and shares + pool.mint(pm, 1000e18); + + vm.prank(pm); + pool.approve(address(withdrawalManager), 1000e18); + } + + function test_processEmptyRedemptions_poolDelegate() external { + // Add a request + vm.prank(pm); + uint256 requestId = withdrawalManager.addShares(100, lp); + + assertQueue({ nextRequestId: 1, lastRequestId: 1 }); + assertRequest({ requestId: 1, owner: lp, shares: 100 }); + + // Remove the request using removeSharesById + vm.prank(lp); + withdrawalManager.removeSharesById(requestId, 100); + + // Request should now be empty + assertRequest({ requestId: 1, owner: address(0), shares: 0 }); + assertQueue({ nextRequestId: 1, lastRequestId: 1 }); + + // Process empty redemptions as pool delegate + vm.prank(poolDelegate); + vm.expectEmit(); + emit EmptyRedemptionsProcessed(1); + withdrawalManager.processEmptyRedemptions(1); + + // Queue should have moved forward + assertQueue({ nextRequestId: 2, lastRequestId: 1 }); + } + + function test_processEmptyRedemptions_operationalAdmin() external { + // Add a request + vm.prank(pm); + uint256 requestId = withdrawalManager.addShares(100, lp); + + // Remove the request + vm.prank(lp); + withdrawalManager.removeSharesById(requestId, 100); + + // Process empty redemptions as operational admin + vm.prank(operationalAdmin); + vm.expectEmit(); + emit EmptyRedemptionsProcessed(1); + withdrawalManager.processEmptyRedemptions(1); + + assertQueue({ nextRequestId: 2, lastRequestId: 1 }); + } + + function test_processEmptyRedemptions_redeemer() external { + globals.__setIsInstanceOf(true); + + // Add a request + vm.prank(pm); + uint256 requestId = withdrawalManager.addShares(100, lp); + + // Remove the request + vm.prank(lp); + withdrawalManager.removeSharesById(requestId, 100); + + // Process empty redemptions as redeemer bot + vm.prank(redeemer); + vm.expectEmit(); + emit EmptyRedemptionsProcessed(1); + withdrawalManager.processEmptyRedemptions(1); + + assertQueue({ nextRequestId: 2, lastRequestId: 1 }); + } + + function test_processEmptyRedemptions_multipleEmptyRequests() external { + // Create 5 requests + vm.startPrank(pm); + uint256 requestId1 = withdrawalManager.addShares(100, lp); + uint256 requestId2 = withdrawalManager.addShares(200, lp); + uint256 requestId3 = withdrawalManager.addShares(300, lp); + withdrawalManager.addShares(400, lp); // requestId4 + withdrawalManager.addShares(500, lp); // requestId5 + vm.stopPrank(); + + assertQueue({ nextRequestId: 1, lastRequestId: 5 }); + + // Remove first 3 requests + vm.startPrank(lp); + withdrawalManager.removeSharesById(requestId1, 100); + withdrawalManager.removeSharesById(requestId2, 200); + withdrawalManager.removeSharesById(requestId3, 300); + vm.stopPrank(); + + // All three should now be empty + assertRequest({ requestId: 1, owner: address(0), shares: 0 }); + assertRequest({ requestId: 2, owner: address(0), shares: 0 }); + assertRequest({ requestId: 3, owner: address(0), shares: 0 }); + assertRequest({ requestId: 4, owner: lp, shares: 400 }); + assertRequest({ requestId: 5, owner: lp, shares: 500 }); + + // Process 3 empty redemptions + vm.prank(poolDelegate); + vm.expectEmit(); + emit EmptyRedemptionsProcessed(3); + withdrawalManager.processEmptyRedemptions(3); + + // Queue should have advanced by 3 + assertQueue({ nextRequestId: 4, lastRequestId: 5 }); + } + + function test_processEmptyRedemptions_stopsAtNonEmptyRequest() external { + // Create 5 requests + vm.startPrank(pm); + uint256 requestId1 = withdrawalManager.addShares(100, lp); + uint256 requestId2 = withdrawalManager.addShares(200, lp); + withdrawalManager.addShares(300, lp); // requestId3 - keep this one + withdrawalManager.addShares(400, lp); // requestId4 + withdrawalManager.addShares(500, lp); // requestId5 + vm.stopPrank(); + + assertQueue({ nextRequestId: 1, lastRequestId: 5 }); + + // Remove only first 2 requests (leave request 3) + vm.startPrank(lp); + withdrawalManager.removeSharesById(requestId1, 100); + withdrawalManager.removeSharesById(requestId2, 200); + vm.stopPrank(); + + assertRequest({ requestId: 1, owner: address(0), shares: 0 }); + assertRequest({ requestId: 2, owner: address(0), shares: 0 }); + assertRequest({ requestId: 3, owner: lp, shares: 300 }); + assertRequest({ requestId: 4, owner: lp, shares: 400 }); + assertRequest({ requestId: 5, owner: lp, shares: 500 }); + + // Try to process 5 empty redemptions, but should stop at request 3 + vm.prank(poolDelegate); + vm.expectEmit(); + emit EmptyRedemptionsProcessed(2); // Only 2 were processed + withdrawalManager.processEmptyRedemptions(5); + + // Queue should only advance by 2 (stopped at non-empty request 3) + assertQueue({ nextRequestId: 3, lastRequestId: 5 }); + + // Now remove requests 3, 4, and 5 + vm.startPrank(lp); + withdrawalManager.removeSharesById(3, 300); + withdrawalManager.removeSharesById(4, 400); + withdrawalManager.removeSharesById(5, 500); + vm.stopPrank(); + + // Process the remaining empty requests + vm.prank(poolDelegate); + vm.expectEmit(); + emit EmptyRedemptionsProcessed(3); + withdrawalManager.processEmptyRedemptions(10); // Request more than available + + // Queue should now be fully processed + assertQueue({ nextRequestId: 6, lastRequestId: 5 }); + } + + function test_processEmptyRedemptions_noEmptyRequests() external { + // Create 3 requests but don't remove any + vm.startPrank(pm); + withdrawalManager.addShares(100, lp); + withdrawalManager.addShares(200, lp); + withdrawalManager.addShares(300, lp); + vm.stopPrank(); + + assertQueue({ nextRequestId: 1, lastRequestId: 3 }); + + // Try to process empty redemptions - should process 0 requests + vm.prank(poolDelegate); + vm.expectEmit(); + emit EmptyRedemptionsProcessed(0); + withdrawalManager.processEmptyRedemptions(5); + + // Queue should not move + assertQueue({ nextRequestId: 1, lastRequestId: 3 }); + } + + function test_processEmptyRedemptions_emptyQueueNoStateChange() external { + // Queue is empty initially + assertQueue({ nextRequestId: 1, lastRequestId: 0 }); + + // Try to process empty redemptions on empty queue + vm.prank(poolDelegate); + vm.expectEmit(); + emit EmptyRedemptionsProcessed(0); + withdrawalManager.processEmptyRedemptions(10); + + // Queue should remain unchanged + assertQueue({ nextRequestId: 1, lastRequestId: 0 }); + } + + function test_processEmptyRedemptions_fullyProcessedQueueNoStateChange() external { + // Create and remove a request + vm.prank(pm); + uint256 requestId = withdrawalManager.addShares(100, lp); + + vm.prank(lp); + withdrawalManager.removeSharesById(requestId, 100); + + // Process the empty request + vm.prank(poolDelegate); + withdrawalManager.processEmptyRedemptions(1); + + assertQueue({ nextRequestId: 2, lastRequestId: 1 }); + + // Try to process again - queue is fully processed + vm.prank(poolDelegate); + vm.expectEmit(); + emit EmptyRedemptionsProcessed(0); + withdrawalManager.processEmptyRedemptions(10); + + // Queue should remain unchanged + assertQueue({ nextRequestId: 2, lastRequestId: 1 }); + } + + function test_processEmptyRedemptions_chunkProcessing() external { + // Create 10 requests + vm.startPrank(pm); + for (uint256 i = 1; i <= 10; i++) { + withdrawalManager.addShares(i * 100, lp); + } + vm.stopPrank(); + + // Remove first 5 requests + vm.startPrank(lp); + for (uint256 i = 1; i <= 5; i++) { + withdrawalManager.removeSharesById(i, i * 100); + } + vm.stopPrank(); + + assertQueue({ nextRequestId: 1, lastRequestId: 10 }); + + // Process only 3 empty requests + vm.prank(poolDelegate); + vm.expectEmit(); + emit EmptyRedemptionsProcessed(3); + withdrawalManager.processEmptyRedemptions(3); + + assertQueue({ nextRequestId: 4, lastRequestId: 10 }); + + // Process remaining 2 empty requests + vm.prank(poolDelegate); + vm.expectEmit(); + emit EmptyRedemptionsProcessed(2); + withdrawalManager.processEmptyRedemptions(2); + + assertQueue({ nextRequestId: 6, lastRequestId: 10 }); + + // Requests 6-10 should still exist + for (uint256 i = 6; i <= 10; i++) { + assertRequest({ requestId: i, owner: lp, shares: i * 100 }); + } + } + +} diff --git a/tests/unit/ProcessExit.t.sol b/tests/unit/ProcessExit.t.sol index b075bdb..a05b1e9 100644 --- a/tests/unit/ProcessExit.t.sol +++ b/tests/unit/ProcessExit.t.sol @@ -1,13 +1,11 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.7; -import { TestBase } from "../utils/TestBase.sol"; +import { TestBase, console } from "../utils/TestBase.sol"; // TODO: Add ManualSharesDecreased event to tests contract ProcessExitTests is TestBase { - event RequestRemoved(uint128 indexed requestId); - uint256 assetsDeposited = 100e18; uint256 sharesToRedeem = 250e18; @@ -45,6 +43,7 @@ contract ProcessExitTests is TestBase { withdrawalManager.__setManualWithdrawal(lp, true); withdrawalManager.__setRequest(1, lp, sharesToRedeem); withdrawalManager.__setQueue(1, 1); + withdrawalManager.__setUserRequestCount(lp, 1, sharesToRedeem); vm.prank(pm); vm.expectRevert("WM:PE:TOO_MANY_SHARES"); @@ -56,6 +55,8 @@ contract ProcessExitTests is TestBase { withdrawalManager.__setRequest(1, lp, sharesToRedeem); withdrawalManager.__setTotalShares(sharesToRedeem); withdrawalManager.__setManualSharesAvailable(lp, sharesToRedeem); + withdrawalManager.__setQueue(1, 1); + withdrawalManager.__setUserRequestCount(lp, 1, sharesToRedeem); asset.burn(address(pool), assetsDeposited); @@ -69,6 +70,8 @@ contract ProcessExitTests is TestBase { withdrawalManager.__setRequest(1, lp, sharesToRedeem); withdrawalManager.__setTotalShares(sharesToRedeem); withdrawalManager.__setManualSharesAvailable(lp, sharesToRedeem); + withdrawalManager.__setQueue(1, 1); + withdrawalManager.__setUserRequestCount(lp, 1, sharesToRedeem); pool.burn(wm, 1); @@ -81,7 +84,7 @@ contract ProcessExitTests is TestBase { withdrawalManager.__setManualWithdrawal(lp, true); withdrawalManager.__setRequest(1, lp, sharesToRedeem); withdrawalManager.__setTotalShares(sharesToRedeem); - withdrawalManager.__setOwnerRequest(lp, 0); + withdrawalManager.__setLastRequest(lp, 2); withdrawalManager.__setManualSharesAvailable(lp, sharesToRedeem); assertEq(pool.balanceOf(lp), 0); @@ -93,8 +96,12 @@ contract ProcessExitTests is TestBase { assertEq(pool.balanceOf(lp), sharesToRedeem); assertEq(pool.balanceOf(wm), 0); - assertEq(withdrawalManager.totalShares(), 0); - assertEq(withdrawalManager.requestIds(lp), 0); + ( uint256 lastRequestId_, ) = getLastRequestByOwner(lp); + assertEq(lastRequestId_, 2); + + assertEq(withdrawalManager.requestIds(lp), lastRequestId_); + + assertEq(withdrawalManager.totalShares(), 0); assertEq(withdrawalManager.manualSharesAvailable(lp), 0); assertRequest({ requestId: 1, owner: lp, shares: sharesToRedeem }); @@ -118,19 +125,22 @@ contract ProcessExitTests is TestBase { assertEq(pool.balanceOf(lp), sharesToRedeem / 2); assertEq(pool.balanceOf(wm), sharesToRedeem / 2); - assertEq(withdrawalManager.totalShares(), sharesToRedeem / 2); + ( uint256 lastRequestId_, ) = getLastRequestByOwner(lp); + assertEq(lastRequestId_, 1); - assertEq(withdrawalManager.requestIds(lp), 1); + assertEq(withdrawalManager.requestIds(lp), lastRequestId_); + + assertEq(withdrawalManager.totalShares(), sharesToRedeem / 2); assertRequest({ requestId: 1, owner: lp, shares: sharesToRedeem / 2 }); } function test_processExit_automatic() external { vm.prank(pm); - ( uint256 redeemableShares, uint256 resultingAssets ) = withdrawalManager.processExit(sharesToRedeem, wm); + ( uint256 redeemableShares_, uint256 resultingAssets_ ) = withdrawalManager.processExit(sharesToRedeem, wm); - assertEq(redeemableShares, sharesToRedeem); - assertEq(resultingAssets, assetsDeposited); + assertEq(redeemableShares_, sharesToRedeem); + assertEq(resultingAssets_, assetsDeposited); } } diff --git a/tests/unit/ProcessRedemptions.t.sol b/tests/unit/ProcessRedemptions.t.sol index 13400e6..83549a4 100644 --- a/tests/unit/ProcessRedemptions.t.sol +++ b/tests/unit/ProcessRedemptions.t.sol @@ -7,10 +7,6 @@ import { TestBase } from "../utils/TestBase.sol"; // TODO: Add test case for reentrancy check contract ProcessRedemptionsTests is TestBase { - event RequestDecreased(uint128 indexed requestId, uint256 shares); - event RequestProcessed(uint128 indexed requestId, address indexed owner, uint256 shares, uint256 assets); - event RequestRemoved(uint128 indexed requestId); - uint256 assetsDeposited = 100e18; uint256 sharesLocked = 250e18; @@ -22,6 +18,8 @@ contract ProcessRedemptionsTests is TestBase { poolManager.__setTotalAssets(assetsDeposited); withdrawalManager.__setTotalShares(sharesLocked); + + globals.__setIsInstanceOf(false); } function test_processRedemptions_protocolPaused() external { @@ -32,8 +30,12 @@ contract ProcessRedemptionsTests is TestBase { } function test_processRedemptions_notRedeemer() external { - globals.__setIsInstanceOf(false); + vm.expectRevert("WM:NOT_REDEEMER"); + withdrawalManager.processRedemptions(sharesLocked); + } + function test_processRedemptions_governorNotAllowed() external { + vm.prank(governor); vm.expectRevert("WM:NOT_REDEEMER"); withdrawalManager.processRedemptions(sharesLocked); } @@ -59,20 +61,6 @@ contract ProcessRedemptionsTests is TestBase { assertQueue({ nextRequestId: 1, lastRequestId: 0 }); } - function test_processRedemptions_governor() external { - vm.prank(governor); - withdrawalManager.processRedemptions(sharesLocked); - - assertQueue({ nextRequestId: 1, lastRequestId: 0 }); - } - - function test_processRedemptions_operationalAdmin() external { - vm.prank(operationalAdmin); - withdrawalManager.processRedemptions(sharesLocked); - - assertQueue({ nextRequestId: 1, lastRequestId: 0 }); - } - function test_processRedemptions_bot() external { globals.__setIsInstanceOf(true); @@ -86,23 +74,106 @@ contract ProcessRedemptionsTests is TestBase { withdrawalManager.__setManualWithdrawal(lp, true); withdrawalManager.__setRequest(1, lp, sharesLocked); withdrawalManager.__setQueue(1, 1); + withdrawalManager.__setUserRequestCount(lp, 1, sharesLocked); + + assertEq(withdrawalManager.userEscrowedShares(lp), sharesLocked); vm.prank(poolDelegate); withdrawalManager.processRedemptions(sharesLocked); + (uint256[] memory requestIdsLp_, uint256[] memory sharesLp_) = withdrawalManager.requestsByOwner(lp); + assertEq(withdrawalManager.totalShares(), sharesLocked); - assertEq(withdrawalManager.requestIds(lp), 0); assertEq(withdrawalManager.manualSharesAvailable(lp), sharesLocked); + assertEq(requestIdsLp_.length, 0); + assertEq(sharesLp_.length, 0); assertRequest({ requestId: 1, owner: address(0), shares: 0 }); assertQueue({ nextRequestId: 2, lastRequestId: 1 }); + + assertEq(withdrawalManager.userEscrowedShares(lp), 0); + } + + function test_processRedemptions_manual_multiple_requests() external { + withdrawalManager.__setManualWithdrawal(lp, true); + withdrawalManager.__setRequest(1, lp, sharesLocked / 2); + withdrawalManager.__setRequest(2, lp, sharesLocked / 2); + withdrawalManager.__setQueue(1, 2); + withdrawalManager.__setUserRequestCount(lp, 2, sharesLocked); + + assertEq(withdrawalManager.userEscrowedShares(lp), sharesLocked); + + vm.prank(poolDelegate); + withdrawalManager.processRedemptions(sharesLocked); + + (uint256[] memory requestIdsLp_, uint256[] memory sharesLp_) = withdrawalManager.requestsByOwner(lp); + + assertEq(withdrawalManager.totalShares(), sharesLocked); + assertEq(withdrawalManager.manualSharesAvailable(lp), sharesLocked); + assertEq(requestIdsLp_.length, 0); + assertEq(sharesLp_.length, 0); + + assertRequest({ requestId: 1, owner: address(0), shares: 0 }); + assertRequest({ requestId: 2, owner: address(0), shares: 0 }); + + assertQueue({ nextRequestId: 3, lastRequestId: 2 }); + + assertEq(withdrawalManager.userEscrowedShares(lp), 0); + } + + function test_processRedemptions_manual_multipleLps_multiple_requests() external { + + address lp2 = makeAddr("lp2"); + address lp3 = makeAddr("lp3"); + + withdrawalManager.__setManualWithdrawal(lp, true); + withdrawalManager.__setManualWithdrawal(lp2, true); + withdrawalManager.__setManualWithdrawal(lp3, true); + withdrawalManager.__setRequest(1, lp, sharesLocked / 4); + withdrawalManager.__setRequest(2, lp2, sharesLocked / 4); + withdrawalManager.__setRequest(3, lp3, sharesLocked / 4); + withdrawalManager.__setRequest(4, lp, sharesLocked / 4); + withdrawalManager.__setQueue(1, 4); + withdrawalManager.__setUserRequestCount(lp, 2, sharesLocked / 2); + withdrawalManager.__setUserRequestCount(lp2, 1, sharesLocked / 4); + withdrawalManager.__setUserRequestCount(lp3, 1, sharesLocked / 4); + + assertEq(withdrawalManager.userEscrowedShares(lp), sharesLocked / 2); + assertEq(withdrawalManager.userEscrowedShares(lp2), sharesLocked / 4); + assertEq(withdrawalManager.userEscrowedShares(lp3), sharesLocked / 4); + + vm.prank(poolDelegate); + withdrawalManager.processRedemptions(sharesLocked); + + (uint256[] memory requestIdsLp_, uint256[] memory sharesLp_) = withdrawalManager.requestsByOwner(lp); + + assertEq(withdrawalManager.totalShares(), sharesLocked); + assertEq(withdrawalManager.manualSharesAvailable(lp), sharesLocked / 2); + assertEq(withdrawalManager.manualSharesAvailable(lp2), sharesLocked / 4); + assertEq(withdrawalManager.manualSharesAvailable(lp3), sharesLocked / 4); + assertEq(requestIdsLp_.length, 0); + assertEq(sharesLp_.length, 0); + + assertRequest({ requestId: 1, owner: address(0), shares: 0 }); + assertRequest({ requestId: 2, owner: address(0), shares: 0 }); + assertRequest({ requestId: 3, owner: address(0), shares: 0 }); + assertRequest({ requestId: 4, owner: address(0), shares: 0 }); + + assertQueue({ nextRequestId: 5, lastRequestId: 4 }); + + assertEq(withdrawalManager.userEscrowedShares(lp), 0); + assertEq(withdrawalManager.userEscrowedShares(lp2), 0); + assertEq(withdrawalManager.userEscrowedShares(lp3), 0); } function test_processRedemptions_manual_partial() external { withdrawalManager.__setManualWithdrawal(lp, true); withdrawalManager.__setRequest(1, lp, sharesLocked); withdrawalManager.__setQueue(1, 1); + withdrawalManager.__setUserRequestCount(lp, 1, sharesLocked); + + assertEq(withdrawalManager.userEscrowedShares(lp), sharesLocked); // Only half of the liquidity is available. asset.burn(address(pool), assetsDeposited / 2); @@ -110,19 +181,31 @@ contract ProcessRedemptionsTests is TestBase { vm.prank(poolDelegate); withdrawalManager.processRedemptions(sharesLocked / 2); + (uint256[] memory requestIdsLp_, uint256[] memory sharesLp_) = withdrawalManager.requestsByOwner(lp); + ( uint256 lastRequestId_, ) = getLastRequestByOwner(lp); + assertEq(withdrawalManager.totalShares(), sharesLocked); - assertEq(withdrawalManager.requestIds(lp), 1); + assertEq(lastRequestId_, 1); assertEq(withdrawalManager.manualSharesAvailable(lp), sharesLocked / 2); + assertEq(requestIdsLp_.length, 1); + assertEq(sharesLp_.length, 1); + assertEq(requestIdsLp_[0], 1); + assertEq(sharesLp_[0], sharesLocked / 2); assertRequest({ requestId: 1, owner: lp, shares: sharesLocked / 2 }); assertQueue({ nextRequestId: 1, lastRequestId: 1 }); + + assertEq(withdrawalManager.userEscrowedShares(lp), sharesLocked / 2); } function test_processRedemptions_manual_overkill() external { withdrawalManager.__setManualWithdrawal(lp, true); withdrawalManager.__setRequest(1, lp, sharesLocked); withdrawalManager.__setQueue(1, 1); + withdrawalManager.__setUserRequestCount(lp, 1, sharesLocked); + + assertEq(withdrawalManager.userEscrowedShares(lp), sharesLocked); // Add extra liquidity. asset.mint(address(pool), assetsDeposited); @@ -130,18 +213,26 @@ contract ProcessRedemptionsTests is TestBase { vm.prank(poolDelegate); withdrawalManager.processRedemptions(2 * sharesLocked); + (uint256[] memory requestIdsLp_, uint256[] memory sharesLp_) = withdrawalManager.requestsByOwner(lp); + assertEq(withdrawalManager.totalShares(), sharesLocked); - assertEq(withdrawalManager.requestIds(lp), 0); assertEq(withdrawalManager.manualSharesAvailable(lp), sharesLocked); + assertEq(requestIdsLp_.length, 0); + assertEq(sharesLp_.length, 0); assertRequest({ requestId: 1, owner: address(0), shares: 0 }); assertQueue({ nextRequestId: 2, lastRequestId: 1 }); + + assertEq(withdrawalManager.userEscrowedShares(lp), 0); } function test_processRedemptions_automatic_complete() external { withdrawalManager.__setRequest(1, lp, sharesLocked); withdrawalManager.__setQueue(1, 1); + withdrawalManager.__setUserRequestCount(lp, 1, sharesLocked); + + assertEq(withdrawalManager.userEscrowedShares(lp), sharesLocked); vm.expectEmit(); emit RequestProcessed(1, lp, sharesLocked, assetsDeposited); @@ -152,18 +243,25 @@ contract ProcessRedemptionsTests is TestBase { vm.prank(poolDelegate); withdrawalManager.processRedemptions(sharesLocked); - assertEq(withdrawalManager.totalShares(), 0); + (uint256[] memory requestIdsLp_, uint256[] memory sharesLp_) = withdrawalManager.requestsByOwner(lp); - assertEq(withdrawalManager.requestIds(lp), 0); + assertEq(withdrawalManager.totalShares(), 0); + assertEq(requestIdsLp_.length, 0); + assertEq(sharesLp_.length, 0); assertRequest({ requestId: 1, owner: address(0), shares: 0 }); assertQueue({ nextRequestId: 2, lastRequestId: 1 }); + + assertEq(withdrawalManager.userEscrowedShares(lp), 0); } function test_processRedemptions_automatic_partial() external { withdrawalManager.__setRequest(1, lp, sharesLocked); withdrawalManager.__setQueue(1, 1); + withdrawalManager.__setUserRequestCount(lp, 1, sharesLocked); + + assertEq(withdrawalManager.userEscrowedShares(lp), sharesLocked); // Only half of the liquidity is available. asset.burn(address(pool), assetsDeposited / 2); @@ -177,18 +275,29 @@ contract ProcessRedemptionsTests is TestBase { vm.prank(poolDelegate); withdrawalManager.processRedemptions(sharesLocked / 2); - assertEq(withdrawalManager.totalShares(), sharesLocked / 2); + (uint256[] memory requestIdsLp_, uint256[] memory sharesLp_) = withdrawalManager.requestsByOwner(lp); + ( uint256 lastRequestId_, ) = getLastRequestByOwner(lp); - assertEq(withdrawalManager.requestIds(lp), 1); + assertEq(withdrawalManager.totalShares(), sharesLocked / 2); + assertEq(lastRequestId_, 1); + assertEq(requestIdsLp_.length, 1); + assertEq(sharesLp_.length, 1); + assertEq(requestIdsLp_[0], 1); + assertEq(sharesLp_[0], sharesLocked / 2); assertRequest({ requestId: 1, owner: lp, shares: sharesLocked / 2 }); assertQueue({ nextRequestId: 1, lastRequestId: 1 }); + + assertEq(withdrawalManager.userEscrowedShares(lp), sharesLocked / 2); } function test_processRedemptions_automatic_overkill() external { withdrawalManager.__setRequest(1, lp, sharesLocked); withdrawalManager.__setQueue(1, 1); + withdrawalManager.__setUserRequestCount(lp, 1, sharesLocked); + + assertEq(withdrawalManager.userEscrowedShares(lp), sharesLocked); // Add extra liquidity. asset.mint(address(pool), assetsDeposited); @@ -202,13 +311,17 @@ contract ProcessRedemptionsTests is TestBase { vm.prank(poolDelegate); withdrawalManager.processRedemptions(2 * sharesLocked); - assertEq(withdrawalManager.totalShares(), 0); + (uint256[] memory requestIdsLp_, uint256[] memory sharesLp_) = withdrawalManager.requestsByOwner(lp); - assertEq(withdrawalManager.requestIds(lp), 0); + assertEq(withdrawalManager.totalShares(), 0); + assertEq(requestIdsLp_.length, 0); + assertEq(sharesLp_.length, 0); assertRequest({ requestId: 1, owner: address(0), shares: 0 }); assertQueue({ nextRequestId: 2, lastRequestId: 1 }); + + assertEq(withdrawalManager.userEscrowedShares(lp), 0); } function test_processRedemptions_multiple() external { @@ -218,6 +331,11 @@ contract ProcessRedemptionsTests is TestBase { withdrawalManager.__setRequest(1, lp1, 100e18); withdrawalManager.__setRequest(2, lp2, 150e18); withdrawalManager.__setQueue(1, 2); + withdrawalManager.__setUserRequestCount(lp1, 1, 100e18); + withdrawalManager.__setUserRequestCount(lp2, 1, 150e18); + + assertEq(withdrawalManager.userEscrowedShares(lp1), 100e18); + assertEq(withdrawalManager.userEscrowedShares(lp2), 150e18); vm.expectEmit(); emit RequestProcessed(1, lp1, 100e18, 40e18); @@ -234,34 +352,37 @@ contract ProcessRedemptionsTests is TestBase { vm.prank(poolDelegate); withdrawalManager.processRedemptions(sharesLocked); - assertEq(withdrawalManager.totalShares(), 0); + (uint256[] memory requestIdsLp1_, uint256[] memory sharesLp1_) = withdrawalManager.requestsByOwner(lp); + (uint256[] memory requestIdsLp2_, uint256[] memory sharesLp2_) = withdrawalManager.requestsByOwner(lp2); - assertEq(withdrawalManager.requestIds(lp1), 0); - assertEq(withdrawalManager.requestIds(lp2), 0); + assertEq(withdrawalManager.totalShares(), 0); + assertEq(requestIdsLp1_.length, 0); + assertEq(sharesLp1_.length, 0); + assertEq(requestIdsLp2_.length, 0); + assertEq(sharesLp2_.length, 0); assertRequest({ requestId: 1, owner: address(0), shares: 0 }); assertRequest({ requestId: 2, owner: address(0), shares: 0 }); assertQueue({ nextRequestId: 3, lastRequestId: 2 }); + + assertEq(withdrawalManager.userEscrowedShares(lp1), 0); + assertEq(withdrawalManager.userEscrowedShares(lp2), 0); } } contract ComplexRedemptionTests is TestBase { - event RequestDecreased(uint128 indexed requestId, uint256 shares); - event RequestProcessed(uint128 indexed requestId, address indexed owner, uint256 shares, uint256 assets); - event RequestRemoved(uint128 indexed requestId); - function test_processRedemptions_complex() external { - uint256 totalAssets = 100e18; - uint256 totalShares = 250e18; - uint256 sharesToProcess = 200e18; + uint256 totalAssets_ = 100e18; + uint256 totalShares_ = 250e18; + uint256 sharesToProcess_ = 200e18; - asset.mint(address(pool), totalShares); - pool.mint(wm, totalShares); + asset.mint(address(pool), totalShares_); + pool.mint(wm, totalShares_); - poolManager.__setTotalAssets(totalAssets); + poolManager.__setTotalAssets(totalAssets_); withdrawalManager.__setRequest(1, address(0), 0); // Already processed withdrawalManager.__setRequest(2, address(2), 100e18); // Fully processed @@ -271,9 +392,14 @@ contract ComplexRedemptionTests is TestBase { withdrawalManager.__setRequest(6, address(6), 25e18); // Out of shares withdrawalManager.__setManualWithdrawal(address(3), true); - withdrawalManager.__setTotalShares(totalShares); + withdrawalManager.__setTotalShares(totalShares_); withdrawalManager.__setQueue(2, 6); + withdrawalManager.__setUserRequestCount(address(2), 1, 100e18); + withdrawalManager.__setUserRequestCount(address(3), 1, 50e18); + withdrawalManager.__setUserRequestCount(address(5), 1, 75e18); + withdrawalManager.__setUserRequestCount(address(6), 1, 25e18); + vm.expectEmit(); emit RequestProcessed(2, address(2), 100e18, 40e18); @@ -287,13 +413,27 @@ contract ComplexRedemptionTests is TestBase { emit RequestDecreased(5, 50e18); vm.prank(poolDelegate); - withdrawalManager.processRedemptions(sharesToProcess); + withdrawalManager.processRedemptions(sharesToProcess_); + + (uint256[] memory requestIds2_, uint256[] memory shares2_) = withdrawalManager.requestsByOwner(address(2)); + (uint256[] memory requestIds3_, uint256[] memory shares3_) = withdrawalManager.requestsByOwner(address(3)); + (uint256[] memory requestIds5_, uint256[] memory shares5_) = withdrawalManager.requestsByOwner(address(5)); + (uint256[] memory requestIds6_, uint256[] memory shares6_) = withdrawalManager.requestsByOwner(address(6)); + + assertEq(requestIds2_.length, 0); + assertEq(shares2_.length, 0); + assertEq(requestIds3_.length, 0); + assertEq(shares3_.length, 0); + + assertEq(requestIds5_.length, 1); + assertEq(shares5_.length, 1); + assertEq(requestIds5_[0], 5); + assertEq(shares5_[0], 25e18); - assertEq(withdrawalManager.requestIds(address(2)), 0); - assertEq(withdrawalManager.requestIds(address(3)), 0); - assertEq(withdrawalManager.requestIds(address(4)), 0); - assertEq(withdrawalManager.requestIds(address(5)), 5); - assertEq(withdrawalManager.requestIds(address(6)), 6); + assertEq(requestIds6_.length, 1); + assertEq(shares6_.length, 1); + assertEq(requestIds6_[0], 6); + assertEq(shares6_[0], 25e18); assertRequest({ requestId: 1, owner: address(0), shares: 0 }); assertRequest({ requestId: 2, owner: address(0), shares: 0 }); @@ -305,7 +445,7 @@ contract ComplexRedemptionTests is TestBase { assertEq(withdrawalManager.manualSharesAvailable(address(3)), 50e18); // Shares from the manual request are not redeemed. - assertEq(withdrawalManager.totalShares(), totalShares - sharesToProcess + 50e18); + assertEq(withdrawalManager.totalShares(), totalShares_ - sharesToProcess_ + 50e18); // Request `5` is partially processed and becomes the next request. assertQueue({ nextRequestId: 5, lastRequestId: 6 }); diff --git a/tests/unit/RemoveRequest.t.sol b/tests/unit/RemoveRequest.t.sol index 2641cab..50a98ce 100644 --- a/tests/unit/RemoveRequest.t.sol +++ b/tests/unit/RemoveRequest.t.sol @@ -5,36 +5,42 @@ import { TestBase } from "../utils/TestBase.sol"; contract RemoveRequestTests is TestBase { - event RequestRemoved(uint128 indexed requestId); - function setUp() public override { super.setUp(); - pool.mint(pm, 2); + pool.mint(pm, 4); vm.prank(pm); - pool.approve(address(withdrawalManager), 2); + pool.approve(address(withdrawalManager), 4); } function test_removeRequest_protocolPaused() external { globals.__setFunctionPaused(true); vm.expectRevert("WM:PAUSED"); - withdrawalManager.removeRequest(lp); + withdrawalManager.removeRequest(lp, new uint256[](0)); } - function test_removeRequest_notProtocolAdmin() external { - vm.expectRevert("WM:NOT_PD_OR_GOV_OR_OA"); - withdrawalManager.removeRequest(lp); + function test_removeRequest_notAuthorized() external { + globals.__setIsInstanceOf(false); + + vm.expectRevert("WM:NOT_REDEEMER"); + withdrawalManager.removeRequest(lp, new uint256[](0)); } function test_removeRequest_notInQueue() external { + uint256[] memory requestIds_ = new uint256[](1); + requestIds_[0] = 1; + vm.prank(poolDelegate); vm.expectRevert("WM:RR:NOT_IN_QUEUE"); - withdrawalManager.removeRequest(lp); + withdrawalManager.removeRequest(lp, requestIds_); } function test_removeRequest_failedTransfer() external { + uint256[] memory requestIds_ = new uint256[](1); + requestIds_[0] = 1; + vm.prank(pm); withdrawalManager.addShares(1, lp); @@ -42,38 +48,127 @@ contract RemoveRequestTests is TestBase { vm.prank(poolDelegate); vm.expectRevert("WM:RR:TRANSFER_FAIL"); - withdrawalManager.removeRequest(lp); + withdrawalManager.removeRequest(lp, requestIds_); } function test_removeRequest_success() external { + uint256 lastRequestIdLp_; + vm.prank(pm); withdrawalManager.addShares(2, lp); - ( , uint128 lastRequestId ) = withdrawalManager.queue(); + assertEq(withdrawalManager.userEscrowedShares(lp), 2); + + ( , uint256 lastRequestId_ ) = withdrawalManager.queue(); + + ( address owner_, uint256 shares_ ) = withdrawalManager.requests(lastRequestId_); + + ( lastRequestIdLp_, ) = getLastRequestByOwner(lp); + + assertEq(shares_, 2); + assertEq(withdrawalManager.totalShares(), 2); + assertEq(lastRequestId_, 1); + assertEq(lastRequestIdLp_, lastRequestId_); + + uint256[] memory requestIds = new uint256[](1); + requestIds[0] = lastRequestId_; + + vm.expectEmit(); + emit RequestRemoved(1); + + vm.prank(poolDelegate); + withdrawalManager.removeRequest(lp, requestIds); + + ( , lastRequestId_ ) = withdrawalManager.queue(); + + ( owner_, shares_ ) = withdrawalManager.requests(lastRequestId_); + + ( lastRequestIdLp_, ) = getLastRequestByOwner(lp); + ( uint256 lastRequestByOwner_, ) = getLastRequestByOwner(owner_); + + assertEq(lastRequestId_, 1); + assertEq(shares_, 0); + assertEq(owner_, address(0)); + assertEq(lastRequestIdLp_, 0); + assertEq(lastRequestByOwner_, 0); + assertEq(withdrawalManager.totalShares(), 0); + + assertEq(withdrawalManager.userEscrowedShares(lp), 0); + } + + function test_removeRequest_multiple_requests_success() external { + uint256 lastRequestIdLp_; + + vm.startPrank(pm); + withdrawalManager.addShares(1, lp); + withdrawalManager.addShares(1, lp); + withdrawalManager.addShares(1, lp); + vm.stopPrank(); + + ( , uint256 lastRequestId_ ) = withdrawalManager.queue(); - ( address owner_, uint256 shares_ ) = withdrawalManager.requests(lastRequestId); + ( address owner_, uint256 shares_ ) = withdrawalManager.requests(lastRequestId_); - assertEq(shares_, 2); - assertEq(withdrawalManager.totalShares(), 2); - assertEq(lastRequestId, 1); - assertEq(withdrawalManager.requestIds(owner_), lastRequestId); + ( lastRequestIdLp_, ) = getLastRequestByOwner(lp); + + assertEq(shares_, 1); + assertEq(withdrawalManager.totalShares(), 3); + assertEq(lastRequestId_, 3); + assertEq(lastRequestIdLp_, lastRequestId_); + + assertEq(withdrawalManager.userEscrowedShares(lp), 3); + + uint256[] memory requestIds = new uint256[](1); + requestIds[0] = 2; + + vm.expectEmit(); + emit RequestRemoved(2); + + vm.prank(poolDelegate); + withdrawalManager.removeRequest(lp, requestIds); + + ( , lastRequestId_ ) = withdrawalManager.queue(); + + ( owner_, shares_ ) = withdrawalManager.requests(lastRequestId_); + + ( lastRequestIdLp_, ) = getLastRequestByOwner(lp); + ( uint256 lastRequestByOwner_, ) = getLastRequestByOwner(owner_); + + assertEq(lastRequestId_, 3); + assertEq(shares_, 1); + assertEq(owner_, lp); + assertEq(lastRequestIdLp_, 3); + assertEq(lastRequestByOwner_, 3); + assertEq(withdrawalManager.totalShares(), 2); + + assertEq(withdrawalManager.userEscrowedShares(lp), 2); + + requestIds = new uint256[](2); + requestIds[0] = 1; + requestIds[1] = 3; vm.expectEmit(); emit RequestRemoved(1); + emit RequestRemoved(3); vm.prank(poolDelegate); - withdrawalManager.removeRequest(lp); + withdrawalManager.removeRequest(lp, requestIds); + + ( , lastRequestId_ ) = withdrawalManager.queue(); + + ( owner_, shares_ ) = withdrawalManager.requests(lastRequestId_); - ( , lastRequestId ) = withdrawalManager.queue(); + ( lastRequestIdLp_, ) = getLastRequestByOwner(lp); + ( lastRequestByOwner_, ) = getLastRequestByOwner(owner_); - ( owner_, shares_ ) = withdrawalManager.requests(lastRequestId); + assertEq(lastRequestId_, 3); + assertEq(shares_, 0); + assertEq(owner_, address(0)); + assertEq(lastRequestIdLp_, 0); + assertEq(lastRequestByOwner_, 0); + assertEq(withdrawalManager.totalShares(), 0); - assertEq(lastRequestId, 1); - assertEq(shares_, 0); - assertEq(owner_, address(0)); - assertEq(withdrawalManager.requestIds(owner_), 0); - assertEq(withdrawalManager.requestIds(lp), 0); - assertEq(withdrawalManager.totalShares(), 0); + assertEq(withdrawalManager.userEscrowedShares(lp), 0); } } diff --git a/tests/unit/RemoveShares.t.sol b/tests/unit/RemoveShares.t.sol index 43ba227..2b93314 100644 --- a/tests/unit/RemoveShares.t.sol +++ b/tests/unit/RemoveShares.t.sol @@ -4,18 +4,15 @@ pragma solidity ^0.8.7; import { TestBase } from "../utils/TestBase.sol"; contract RemoveSharesTests is TestBase { - - event RequestDecreased(uint128 indexed requestId, uint256 shares); - event RequestRemoved(uint128 indexed requestId); - + function setUp() public override { super.setUp(); // Simulate LP transfer into PM. - pool.mint(pm, 2); + pool.mint(pm, 7); vm.prank(pm); - pool.approve(address(withdrawalManager), 2); + pool.approve(address(withdrawalManager), 7); } function test_removeShares_notPoolManager() external { @@ -30,10 +27,12 @@ contract RemoveSharesTests is TestBase { } function test_removeShares_notInQueue() external { - assertEq(withdrawalManager.requestIds(pm), 0); + ( uint256 lastRequestId_,) = getLastRequestByOwner(pm); + + assertEq(lastRequestId_, 0); vm.prank(pm); - vm.expectRevert("WM:RS:NOT_IN_QUEUE"); + vm.expectRevert("WM:RS:INSUFFICIENT_SHARES"); withdrawalManager.removeShares(1, lp); } @@ -58,62 +57,157 @@ contract RemoveSharesTests is TestBase { } function test_removeShares_success_decreaseRequest() external { + uint256 lastRequestId_; + vm.prank(pm); withdrawalManager.addShares(2, lp); - ( , uint128 lastRequestId ) = withdrawalManager.queue(); - - ( address owner_, uint256 shares_ ) = withdrawalManager.requests(lastRequestId); - - assertEq(shares_, 2); - assertEq(withdrawalManager.totalShares(), 2); - assertEq(lastRequestId, 1); - assertEq(withdrawalManager.requestIds(owner_), lastRequestId); + assertEq(withdrawalManager.userEscrowedShares(lp), 2); vm.expectEmit(); emit RequestDecreased(1, 1); vm.prank(pm); - withdrawalManager.removeShares(1, lp); + uint256 sharesReturned_ = withdrawalManager.removeShares(1, lp); + + assertEq(sharesReturned_ , 1); + assertEq(withdrawalManager.userEscrowedShares(lp), 1); - ( , lastRequestId ) = withdrawalManager.queue(); + ( , lastRequestId_ ) = withdrawalManager.queue(); - ( , shares_ ) = withdrawalManager.requests(lastRequestId); + ( address owner_ , uint256 shares_ ) = withdrawalManager.requests(lastRequestId_); - assertEq(shares_, 1); - assertEq(withdrawalManager.totalShares(), 1); - assertEq(lastRequestId, 1); - assertEq(withdrawalManager.requestIds(owner_), lastRequestId); + ( lastRequestId_, ) = getLastRequestByOwner(owner_); + + assertEq(shares_, 1); + assertEq(withdrawalManager.totalShares(), 1); + assertEq(lastRequestId_, 1); } function test_removeShares_success_cancelRequest() external { + uint256 lastRequestId_; + vm.prank(pm); withdrawalManager.addShares(2, lp); - ( , uint128 lastRequestId ) = withdrawalManager.queue(); - - ( address owner_, uint256 shares_ ) = withdrawalManager.requests(lastRequestId); - - assertEq(shares_, 2); - assertEq(withdrawalManager.totalShares(), 2); - assertEq(lastRequestId, 1); - assertEq(withdrawalManager.requestIds(owner_), lastRequestId); + assertEq(withdrawalManager.userEscrowedShares(lp), 2); vm.expectEmit(); emit RequestRemoved(1); vm.prank(pm); - withdrawalManager.removeShares(2, lp); + uint256 sharesReturned_ = withdrawalManager.removeShares(2, lp); + + assertEq(sharesReturned_, 2); + + ( , lastRequestId_ ) = withdrawalManager.queue(); + + ( address owner_ , uint256 shares_ ) = withdrawalManager.requests(lastRequestId_); + + ( lastRequestId_, ) = getLastRequestByOwner(owner_); + + assertEq(lastRequestId_, 0); + assertEq(owner_, address(0)); + assertEq(shares_, 0); + assertEq(withdrawalManager.totalShares(), 0); + + assertEq(withdrawalManager.userEscrowedShares(lp), 0); + } + + function test_removeShares_success_partial_multipleRequests() external { + vm.startPrank(pm); + withdrawalManager.addShares(2, lp); + withdrawalManager.addShares(4, lp); + withdrawalManager.addShares(1, lp); + vm.stopPrank(); + + assertEq(withdrawalManager.userEscrowedShares(lp), 7); + assertEq(withdrawalManager.totalShares() , 7); + + ( uint256[] memory requestIds_, uint256[] memory shares_ ) = withdrawalManager.requestsByOwner(lp); + + assertEq(requestIds_.length, 3); + assertEq(shares_.length , 3); + + ( uint256 lastRequestId_, uint256 lastShares_ ) = getLastRequestByOwner(lp); + + assertEq(lastRequestId_, 3); + assertEq(lastShares_ , 1); + + vm.prank(pm); + uint256 sharesReturned_ = withdrawalManager.removeShares(3, lp); + + assertEq(sharesReturned_, 3); + assertEq(withdrawalManager.userEscrowedShares(lp), 4); + assertEq(withdrawalManager.totalShares() , 4); + + ( lastRequestId_, lastShares_ ) = getLastRequestByOwner(lp); + + assertEq(lastRequestId_, 2); + assertEq(lastShares_ , 2); + + ( requestIds_, shares_ ) = withdrawalManager.requestsByOwner(lp); + + assertEq(requestIds_.length, 2); + assertEq(shares_.length , 2); + + assertRequest(1, lp, 2); + assertRequest(2, lp, 2); + + vm.prank(pm); + sharesReturned_ = withdrawalManager.removeShares(2, lp); + + assertEq(sharesReturned_, 2); + assertEq(withdrawalManager.userEscrowedShares(lp), 2); + assertEq(withdrawalManager.totalShares() , 2); + + ( requestIds_, shares_ ) = withdrawalManager.requestsByOwner(lp); + + assertEq(requestIds_.length, 1); + assertEq(shares_.length , 1); + + assertRequest(1, lp, 2); + } + + function test_removeShares_success_cancelAllRequests() external { + vm.startPrank(pm); + withdrawalManager.addShares(2, lp); + withdrawalManager.addShares(4, lp); + withdrawalManager.addShares(1, lp); + vm.stopPrank(); + + assertEq(withdrawalManager.userEscrowedShares(lp), 7); + assertEq(withdrawalManager.totalShares() , 7); + + ( uint256[] memory requestIds_, uint256[] memory shares_ ) = withdrawalManager.requestsByOwner(lp); + + assertEq(requestIds_.length, 3); + assertEq(shares_.length , 3); + + ( uint256 lastRequestId_, uint256 lastShares_ ) = getLastRequestByOwner(lp); + + assertEq(lastRequestId_, 3); + assertEq(lastShares_ , 1); + + vm.prank(pm); + uint256 sharesReturned_ = withdrawalManager.removeShares(7, lp); + + assertEq(sharesReturned_, 7); + assertEq(withdrawalManager.userEscrowedShares(lp), 0); + assertEq(withdrawalManager.totalShares() , 0); + + ( requestIds_, shares_ ) = withdrawalManager.requestsByOwner(lp); + + assertEq(requestIds_.length, 0); + assertEq(shares_.length , 0); - ( , lastRequestId ) = withdrawalManager.queue(); + assertEq(withdrawalManager.userEscrowedShares(lp), 0); + assertEq(withdrawalManager.totalShares() , 0); - ( owner_, shares_ ) = withdrawalManager.requests(lastRequestId); + ( lastRequestId_, lastShares_ ) = getLastRequestByOwner(lp); - assertEq(lastRequestId, 1); - assertEq(owner_, address(0)); - assertEq(shares_, 0); - assertEq(withdrawalManager.requestIds(owner_), 0); - assertEq(withdrawalManager.totalShares(), 0); + assertEq(lastRequestId_, 0); + assertEq(lastShares_ , 0); } } diff --git a/tests/unit/RemoveSharesById.t.sol b/tests/unit/RemoveSharesById.t.sol new file mode 100644 index 0000000..ad81aeb --- /dev/null +++ b/tests/unit/RemoveSharesById.t.sol @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.7; + +import { TestBase } from "../utils/TestBase.sol"; + +contract RemoveSharesByIdFailureTests is TestBase { + + function setUp() public override { + super.setUp(); + uint256 mintAmount = 200; + + // Simulate LP transfer into PM. + pool.mint(pm, mintAmount); + + vm.prank(pm); + pool.approve(address(withdrawalManager), mintAmount); + } + + function test_removeSharesById_protocolPaused() external { + globals.__setFunctionPaused(true); + + vm.prank(lp); + vm.expectRevert("WM:PAUSED"); + withdrawalManager.removeSharesById(1, 1); + } + + function test_removeSharesById_maxUint128Exceeded() external { + vm.prank(lp); + vm.expectRevert("WM:TU:UINT256_CAST"); + withdrawalManager.removeSharesById(type(uint256).max, 1); + } + + function test_removeSharesById_invalidRequest() external { + vm.expectRevert("WM:RSBI:INVALID_REQUEST"); + withdrawalManager.removeSharesById(1, 1); + } + + function test_removeSharesById_requestAlreadyRemoved() external { + vm.prank(pm); + withdrawalManager.addShares(1, lp); + + uint256[] memory requestIds = new uint256[](1); + requestIds[0] = 1; + + vm.prank(operationalAdmin); + withdrawalManager.removeRequest(lp, requestIds); + + vm.expectRevert("WM:RSBI:INVALID_REQUEST"); + vm.prank(lp); + withdrawalManager.removeSharesById(1, 100); + } + + function test_removeSharesById_zeroRequestId() external { + vm.prank(lp); + vm.expectRevert("WM:RSBI:INVALID_REQUEST"); + withdrawalManager.removeSharesById(0, 1); // Request ID 0 should be invalid + } + + function test_removeSharesById_notOwner() external { + vm.prank(pm); + uint256 requestId = withdrawalManager.addShares(1, lp); + + vm.prank(address(0x12)); + vm.expectRevert("WM:RSBI:NOT_OWNER"); + withdrawalManager.removeSharesById(requestId, 1); + } + + function test_removeSharesById_noChange() external { + vm.prank(pm); + withdrawalManager.addShares(1, lp); + + vm.prank(lp); + vm.expectRevert("WM:RSBI:NO_CHANGE"); + withdrawalManager.removeSharesById(1, 0); + } + + function test_removeSharesById_insufficientShares() external { + vm.prank(pm); + withdrawalManager.addShares(5, lp); + + // Try to remove 10 shares (more than the 5 available) + vm.prank(lp); + vm.expectRevert("WM:RSBI:INSUFFICIENT_SHARES"); + withdrawalManager.removeSharesById(1, 10); + } + + function test_removeSharesById_transferFail() external { + vm.prank(pm); + withdrawalManager.addShares(5, lp); + + // Burn the tokens from the withdrawal manager to simulate transfer failure + pool.burn(address(withdrawalManager), 5); + + vm.prank(lp); + vm.expectRevert("WM:RS:TRANSFER_FAIL"); + withdrawalManager.removeSharesById(1, 1); + } + +} + +contract RemoveSharesByIdSuccessTests is TestBase { + + address lp2 = makeAddr("lp2"); + + address ownerForRequest1; + address ownerForRequest2; + address ownerForRequest3; + address ownerForRequest4; + + uint256 requestAmount1; + uint256 requestAmount2; + + uint256 sharesForRequest1; + uint256 sharesForRequest2; + uint256 sharesForRequest3; + uint256 sharesForRequest4; + + function setUp() public override { + super.setUp(); + uint256 mintAmount = 200; + + pool.mint(pm, mintAmount); // Simulate LP transfer into PM. + pool.mint(lp, mintAmount); // Give LP some shares to use to increase shares. + + vm.prank(pm); + pool.approve(address(withdrawalManager), mintAmount); + + vm.prank(lp); + pool.approve(address(withdrawalManager), mintAmount); + } + + function test_removeSharesById_remove_request() external{ + vm.prank(pm); + uint256 requestId = withdrawalManager.addShares(1, lp); + + ( address ownerBefore, uint256 sharesBefore ) = withdrawalManager.requests(requestId); + + assertEq(ownerBefore, lp); + assertEq(sharesBefore, 1); + + ( uint256[] memory requestIdsBefore, uint256[] memory sharesPerRequestBefore ) = withdrawalManager.requestsByOwner(lp); + + assertEq(requestIdsBefore.length, 1); + assertEq(sharesPerRequestBefore.length, 1); + assertEq(requestIdsBefore[0], requestId); + assertEq(sharesPerRequestBefore[0], 1); + + assertEq(withdrawalManager.userEscrowedShares(lp), 1); + + vm.prank(lp); + vm.expectEmit(); + emit RequestRemoved(requestId); + + ( uint256 sharesReturned_, uint256 sharesRemaining_ ) = withdrawalManager.removeSharesById(requestId, 1); + + assertEq(sharesReturned_, 1); + assertEq(sharesRemaining_, 0); + + ( address ownerAfter, uint256 sharesAfter ) = withdrawalManager.requests(requestId); + + assertEq(ownerAfter, address(0)); + assertEq(sharesAfter, 0); + + ( uint256[] memory requestIdsAfter, uint256[] memory sharesPerRequestAfter ) = withdrawalManager.requestsByOwner(lp); + + assertEq(requestIdsAfter.length, 0); + assertEq(sharesPerRequestAfter.length, 0); + + assertEq(withdrawalManager.userEscrowedShares(lp), 0); + } + + function test_removeSharesById_decrease() external { + vm.prank(pm); + uint256 requestId = withdrawalManager.addShares(2, lp); + + ( address ownerBefore, uint256 sharesBefore ) = withdrawalManager.requests(requestId); + + assertEq(ownerBefore, lp); + assertEq(sharesBefore, 2); + + ( uint256[] memory requestIdsBefore, uint256[] memory sharesPerRequestBefore ) = withdrawalManager.requestsByOwner(lp); + + assertEq(requestIdsBefore.length, 1); + assertEq(sharesPerRequestBefore.length, 1); + assertEq(requestIdsBefore[0], requestId); + assertEq(sharesPerRequestBefore[0], 2); + + assertEq(withdrawalManager.userEscrowedShares(lp), 2); + + vm.prank(lp); + vm.expectEmit(); + emit RequestDecreased(requestId, 1); + + ( uint256 sharesReturned_, uint256 sharesRemaining_ ) = withdrawalManager.removeSharesById(requestId, 1); + + assertEq(sharesReturned_, 1); + assertEq(sharesRemaining_, 1); + + ( address ownerAfter, uint256 sharesAfter ) = withdrawalManager.requests(requestId); + + assertEq(ownerAfter, lp); + assertEq(sharesAfter, 1); + + ( uint256[] memory requestIdsAfter, uint256[] memory sharesPerRequestAfter ) = withdrawalManager.requestsByOwner(lp); + + assertEq(requestIdsAfter.length, 1); + assertEq(sharesPerRequestAfter.length, 1); + assertEq(requestIdsAfter[0], requestId); + assertEq(sharesPerRequestAfter[0], 1); + + assertEq(withdrawalManager.userEscrowedShares(lp), 1); + } + + function test_removeSharesById_multipleLPsWithMultipleRequests() external { + requestAmount1 = 10; + requestAmount2 = 20; + + pool.mint(pm, 3000); + pool.mint(lp, 1000); + pool.mint(lp2, 1000); + + // Create multiple requests for both LPs + vm.startPrank(pm); + + vm.expectEmit(); + emit RequestCreated(1, lp, requestAmount1); + withdrawalManager.addShares(requestAmount1, lp); + + vm.expectEmit(); + emit RequestCreated(2, lp, requestAmount2); + withdrawalManager.addShares(requestAmount2, lp); + + vm.expectEmit(); + emit RequestCreated(3, lp2, requestAmount1); + withdrawalManager.addShares(requestAmount1, lp2); + + vm.expectEmit(); + emit RequestCreated(4, lp2, requestAmount2); + withdrawalManager.addShares(requestAmount2, lp2); + + vm.stopPrank(); + + assertEq(withdrawalManager.userEscrowedShares(lp), requestAmount1 + requestAmount2); + assertEq(withdrawalManager.userEscrowedShares(lp2), requestAmount1 + requestAmount2); + + ( ownerForRequest1, sharesForRequest1 ) = withdrawalManager.requests(1); + ( ownerForRequest2, sharesForRequest2 ) = withdrawalManager.requests(2); + ( ownerForRequest3, sharesForRequest3 ) = withdrawalManager.requests(3); + ( ownerForRequest4, sharesForRequest4 ) = withdrawalManager.requests(4); + + assertEq(ownerForRequest1, lp); + assertEq(ownerForRequest2, lp); + assertEq(ownerForRequest3, lp2); + assertEq(ownerForRequest4, lp2); + + assertEq(sharesForRequest1, requestAmount1); + assertEq(sharesForRequest2, requestAmount2); + assertEq(sharesForRequest3, requestAmount1); + assertEq(sharesForRequest4, requestAmount2); + + ( uint256[] memory requestIdsLp1Before, uint256[] memory sharesLp1Before ) = withdrawalManager.requestsByOwner(lp); + ( uint256[] memory requestIdsLp2Before, uint256[] memory sharesLp2Before ) = withdrawalManager.requestsByOwner(lp2); + + assertEq(requestIdsLp1Before.length, 2); + assertEq(sharesLp1Before.length, 2); + + assertEq(requestIdsLp2Before.length, 2); + assertEq(sharesLp2Before.length, 2); + + assertEq(requestIdsLp1Before[0], 1); + assertEq(requestIdsLp1Before[1], 2); + + assertEq(requestIdsLp2Before[0], 3); + assertEq(requestIdsLp2Before[1], 4); + + assertEq(sharesLp1Before[0], requestAmount1); + assertEq(sharesLp1Before[1], requestAmount2); + + assertEq(sharesLp2Before[0], requestAmount1); + assertEq(sharesLp2Before[1], requestAmount2); + + // Decrease LP1's first request + vm.prank(lp); + vm.expectEmit(); + emit RequestDecreased(1, 1); + ( uint256 sharesReturnedLp, uint256 sharesRemainingLp ) = withdrawalManager.removeSharesById(1, 1); + + assertEq(sharesReturnedLp, 1); + assertEq(sharesRemainingLp, requestAmount1 - 1); + + // Remove LP2's first request completely + vm.prank(lp2); + vm.expectEmit(); + emit RequestRemoved(3); + ( uint256 sharesReturnedLp2, uint256 sharesRemainingLp2 ) = withdrawalManager.removeSharesById(3, requestAmount1); + + assertEq(sharesReturnedLp2, requestAmount1); + assertEq(sharesRemainingLp2, 0); + + assertEq(withdrawalManager.userEscrowedShares(lp), requestAmount1 - 1 + requestAmount2); + assertEq(withdrawalManager.userEscrowedShares(lp2), requestAmount2); + + ( ownerForRequest1, sharesForRequest1 ) = withdrawalManager.requests(1); + ( ownerForRequest2, sharesForRequest2 ) = withdrawalManager.requests(2); + ( ownerForRequest3, sharesForRequest3 ) = withdrawalManager.requests(3); + ( ownerForRequest4, sharesForRequest4 ) = withdrawalManager.requests(4); + + assertEq(ownerForRequest1, lp); // LP's first request should be decreased but still exist + assertEq(ownerForRequest2, lp); // LP's second request should be unchanged + assertEq(ownerForRequest3, address(0)); // LP2's first request should be removed + assertEq(ownerForRequest4, lp2); // LP2's second request should be unchanged + + assertEq(sharesForRequest1, requestAmount1 - 1); // LP's first request decreased + assertEq(sharesForRequest2, requestAmount2); // LP's second request unchanged + assertEq(sharesForRequest3, 0); // LP2's first request removed + assertEq(sharesForRequest4, requestAmount2); // LP2's second request unchanged + + ( uint256[] memory requestIdsLp1After, uint256[] memory sharesLp1After ) = withdrawalManager.requestsByOwner(lp); + ( uint256[] memory requestIdsLp2After, uint256[] memory sharesLp2After ) = withdrawalManager.requestsByOwner(lp2); + + assertEq(requestIdsLp1After.length, 2); + assertEq(sharesLp1After.length, 2); + + assertEq(requestIdsLp2After.length, 1); + assertEq(sharesLp2After.length, 1); + + assertEq(requestIdsLp1After[0], 1); + assertEq(requestIdsLp1After[1], 2); + + assertEq(requestIdsLp2After[0], 4); + + assertEq(sharesLp1After[0], requestAmount1 - 1); + assertEq(sharesLp1After[1], requestAmount2); + + assertEq(sharesLp2After[0], requestAmount2); + } + +} diff --git a/tests/unit/SetManualWithdrawal.t.sol b/tests/unit/SetManualWithdrawal.t.sol index f36aff4..c946221 100644 --- a/tests/unit/SetManualWithdrawal.t.sol +++ b/tests/unit/SetManualWithdrawal.t.sol @@ -5,8 +5,6 @@ import { TestBase } from "../utils/TestBase.sol"; contract SetManualWithdrawalTests is TestBase { - event ManualWithdrawalSet(address indexed account, bool isManual); - function test_setManualWithdrawal_protocolPaused() external { globals.__setFunctionPaused(true); @@ -14,16 +12,14 @@ contract SetManualWithdrawalTests is TestBase { withdrawalManager.setManualWithdrawal(lp, true); } - function test_setManualWithdrawal_notProtocolAdmin() external { - vm.expectRevert("WM:NOT_PD_OR_GOV_OR_OA"); + function test_setManualWithdrawal_notPoolDelegateOrOpsAdmin() external { + vm.expectRevert("WM:NOT_POOL_DELEG_OR_OPS_ADMIN"); withdrawalManager.setManualWithdrawal(lp, true); } - function test_setManualWithdrawal_existingRequest() external { - withdrawalManager.__setRequest(1, lp, 100e18); - - vm.prank(poolDelegate); - vm.expectRevert("WM:SMW:IN_QUEUE"); + function test_setManualWithdrawal_governotNotAllowed() external { + vm.prank(governor); + vm.expectRevert("WM:NOT_POOL_DELEG_OR_OPS_ADMIN"); withdrawalManager.setManualWithdrawal(lp, true); } diff --git a/tests/unit/SortedArray/Push.t.sol b/tests/unit/SortedArray/Push.t.sol new file mode 100644 index 0000000..7406194 --- /dev/null +++ b/tests/unit/SortedArray/Push.t.sol @@ -0,0 +1,53 @@ + +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.7; + +import { SortedLinkedListTestBase } from "../../utils/TestBase.sol"; + +contract PushTests is SortedLinkedListTestBase { + + function test_push_failed_zeroValue() external { + vm.expectRevert("SLL:P:ZERO_VALUE"); + list.push(0); + } + + function test_push_failed_valueExists() external { + list.push(1); + + vm.expectRevert("SLL:P:VALUE_EXISTS"); + list.push(1); + } + + function test_push_failed_outOfOrder() external { + list.push(3); + + vm.expectRevert("SLL:P:NOT_LARGEST"); + list.push(1); + } + + function test_push_singleValue() external { + list.push(3); + + uint256[] memory expectedValues = new uint256[](1); + expectedValues[0] = 3; + + assertList(expectedValues); + + assertEq(list.length(), 1); + } + + function test_push_multipleValues_inOrder() external { + list.push(1); + list.push(3); + list.push(7); + + uint256[] memory expectedValues = new uint256[](3); + expectedValues[0] = 1; + expectedValues[1] = 3; + expectedValues[2] = 7; + + assertList(expectedValues); + assertEq(list.length(), 3); + } + +} diff --git a/tests/unit/SortedArray/Remove.t.sol b/tests/unit/SortedArray/Remove.t.sol new file mode 100644 index 0000000..2e4ce16 --- /dev/null +++ b/tests/unit/SortedArray/Remove.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.7; + +import { SortedLinkedListTestBase } from "../../utils/TestBase.sol"; + +contract RemoveTests is SortedLinkedListTestBase { + + function test_remove_failed_valueNotExists() external { + vm.expectRevert("SLL:R:VALUE_NOT_EXISTS"); + list.remove(1); + } + + function test_remove_singleValue() external { + list.push(1); + list.remove(1); + + uint256[] memory expectedValues = new uint256[](0); + assertList(expectedValues); + + assertEq(list.length(), 0); + } + + function test_remove_multipleValues() external { + list.push(1); + list.push(2); + list.push(3); + + list.remove(2); + + uint256[] memory expectedValues = new uint256[](2); + expectedValues[0] = 1; + expectedValues[1] = 3; + + assertList(expectedValues); + assertEq(list.length(), 2); + + list.remove(1); + + expectedValues = new uint256[](1); + expectedValues[0] = 3; + + assertList(expectedValues); + assertEq(list.length(), 1); + + list.remove(3); + + assertList(new uint256[](0)); + assertEq(list.length(), 0); + } + +} diff --git a/tests/unit/ViewFunctions.t.sol b/tests/unit/ViewFunctions.t.sol index c5624bb..3c05524 100644 --- a/tests/unit/ViewFunctions.t.sol +++ b/tests/unit/ViewFunctions.t.sol @@ -5,6 +5,22 @@ import { TestBase } from "../utils/TestBase.sol"; contract ViewFunctionsTests is TestBase { + function test_asset() external { + assertEq(withdrawalManager.asset(), address(asset)); + } + + function test_globals() external { + assertEq(withdrawalManager.globals(), address(globals)); + } + + function test_governor() external { + assertEq(withdrawalManager.governor(), governor); + } + + function test_securityAdmin() external { + assertEq(withdrawalManager.securityAdmin(), securityAdmin); + } + function testFuzz_isInExitWindow(address account_) external { assertTrue(withdrawalManager.isInExitWindow(account_)); } @@ -24,4 +40,100 @@ contract ViewFunctionsTests is TestBase { assertEq(redeemableShares, 0); } + function test_requests_by_owner() external { + address lp1 = makeAddr("lp1"); + address lp2 = makeAddr("lp2"); + address lp3 = makeAddr("lp3"); + address lp4 = makeAddr("lp4"); + + uint256 assetsDeposited_ = 100e18; + + withdrawalManager.__setRequest(1, lp1, assetsDeposited_); + withdrawalManager.__setRequest(2, lp2, assetsDeposited_); + withdrawalManager.__setRequest(3, lp3, assetsDeposited_); + withdrawalManager.__setRequest(4, lp4, assetsDeposited_); + withdrawalManager.__setRequest(5, lp1, assetsDeposited_); + + withdrawalManager.__setUserRequestCount(lp1, 2, assetsDeposited_ * 2); + withdrawalManager.__setUserRequestCount(lp2, 1, assetsDeposited_); + withdrawalManager.__setUserRequestCount(lp3, 1, assetsDeposited_); + withdrawalManager.__setUserRequestCount(lp4, 1, assetsDeposited_); + + withdrawalManager.__setQueue(1, 5); + + (uint256[] memory requestIdsLp1_, uint256[] memory sharesLp1_) = withdrawalManager.requestsByOwner(lp1); + (uint256[] memory requestIdsLp2_, uint256[] memory sharesLp2_) = withdrawalManager.requestsByOwner(lp2); + (uint256[] memory requestIdsLp3_, uint256[] memory sharesLp3_) = withdrawalManager.requestsByOwner(lp3); + (uint256[] memory requestIdsLp4_, uint256[] memory sharesLp4_) = withdrawalManager.requestsByOwner(lp4); + + + assertEq(requestIdsLp1_.length, 2); + assertEq(requestIdsLp1_[0], 1); + assertEq(requestIdsLp1_[1], 5); + + assertEq(sharesLp1_.length, 2); + assertEq(sharesLp1_[0], assetsDeposited_); + assertEq(sharesLp1_[1], assetsDeposited_); + + assertEq(requestIdsLp2_.length, 1); + assertEq(requestIdsLp2_[0], 2); + + assertEq(sharesLp2_.length, 1); + assertEq(sharesLp2_[0], assetsDeposited_); + + assertEq(requestIdsLp3_.length, 1); + assertEq(requestIdsLp3_[0], 3); + + assertEq(sharesLp3_.length, 1); + assertEq(sharesLp3_[0], assetsDeposited_); + + assertEq(requestIdsLp4_.length, 1); + assertEq(requestIdsLp4_[0], 4); + + assertEq(sharesLp4_.length, 1); + assertEq(sharesLp4_[0], assetsDeposited_); + } + + function test_requests_by_requestId() external { + address lp2 = makeAddr("lp2"); + address lp3 = makeAddr("lp3"); + address lp4 = makeAddr("lp4"); + + uint256 assetsDeposited_ = 100e18; + + withdrawalManager.__setRequest(1, lp, assetsDeposited_); + withdrawalManager.__setRequest(2, lp2, assetsDeposited_); + withdrawalManager.__setRequest(3, lp3, assetsDeposited_); + withdrawalManager.__setRequest(4, lp4, assetsDeposited_); + withdrawalManager.__setRequest(5, lp, assetsDeposited_); + + withdrawalManager.__setUserRequestCount(lp, 2, assetsDeposited_ * 2); + withdrawalManager.__setUserRequestCount(lp2, 1, assetsDeposited_); + withdrawalManager.__setUserRequestCount(lp3, 1, assetsDeposited_); + withdrawalManager.__setUserRequestCount(lp4, 1, assetsDeposited_); + + withdrawalManager.__setQueue(1, 5); + + (address ownerLp1_, uint256 sharesLp1_) = withdrawalManager.requests(1); + (address ownerLp2_, uint256 sharesLp2_) = withdrawalManager.requests(2); + (address ownerLp3_, uint256 sharesLp3_) = withdrawalManager.requests(3); + (address ownerLp4_, uint256 sharesLp4_) = withdrawalManager.requests(4); + (address ownerLp1_2_, uint256 sharesLp1_2_) = withdrawalManager.requests(5); + + assertEq(ownerLp1_, lp); + assertEq(sharesLp1_, assetsDeposited_); + + assertEq(ownerLp2_, lp2); + assertEq(sharesLp2_, assetsDeposited_); + + assertEq(ownerLp3_, lp3); + assertEq(sharesLp3_, assetsDeposited_); + + assertEq(ownerLp4_, lp4); + assertEq(sharesLp4_, assetsDeposited_); + + assertEq(ownerLp1_2_, lp); + assertEq(sharesLp1_2_, assetsDeposited_); + } + } diff --git a/tests/utils/Harnesses.sol b/tests/utils/Harnesses.sol index ccb252b..acf1b8f 100644 --- a/tests/utils/Harnesses.sol +++ b/tests/utils/Harnesses.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.7; import { MapleWithdrawalManager } from "../../contracts/MapleWithdrawalManager.sol"; +import { SortedLinkedList } from "../../contracts/utils/SortedLinkedList.sol"; + contract MapleWithdrawalManagerHarness is MapleWithdrawalManager { function locked() external view returns (uint256) { @@ -17,22 +19,76 @@ contract MapleWithdrawalManagerHarness is MapleWithdrawalManager { isManualWithdrawal[owner_] = isManual_; } - function __setOwnerRequest(address owner_, uint128 requestId_) external { - requestIds[owner_] = requestId_; + function __setLastRequest(address owner_, uint256 requestId_) external { + SortedLinkedList.push(_userRequests[owner_], _toUint128(requestId_)); + queue.lastRequestId = _toUint128(requestId_); + } + + function __setQueue(uint256 nextRequestId_, uint256 lastRequestId_) external { + queue.nextRequestId = _toUint128(nextRequestId_); + queue.lastRequestId = _toUint128(lastRequestId_); } - function __setQueue(uint128 nextRequestId_, uint128 lastRequestId_) external { - queue.nextRequestId = nextRequestId_; - queue.lastRequestId = lastRequestId_; + function __setRequestLegacy(uint128 requestId_, address owner_, uint256 shares_) external { + // Used to setup the withdrawal manager storage in the old format. + // This function is only used to unit test the migrator so we can setup wm storage without pushing to the sorted array. + queue.lastRequestId = requestId_; + queue.requests[requestId_] = WithdrawalRequest(owner_, shares_); + + __deprecated_requestIds[owner_] = requestId_; } function __setRequest(uint128 requestId_, address owner_, uint256 shares_) external { - requestIds[owner_] = requestId_; + queue.lastRequestId = requestId_; queue.requests[requestId_] = WithdrawalRequest(owner_, shares_); + + SortedLinkedList.push(_userRequests[owner_], _toUint128(requestId_)); } function __setTotalShares(uint256 totalShares_) external { totalShares = totalShares_; } + function __setUserRequestCount(address owner_, uint256 requestCount_, uint256 escrowSharesTotal_) external { + uint256 currentRequestCount_ = SortedLinkedList.length(_userRequests[owner_]); + + if (requestCount_ < currentRequestCount_) { + for (uint256 i = requestCount_; i < currentRequestCount_; i++) { + SortedLinkedList.remove(_userRequests[owner_], _toUint128(i)); + } + } else { + for (uint256 i = currentRequestCount_; i > requestCount_; i++) { + SortedLinkedList.push(_userRequests[owner_], _toUint128(i)); + } + } + + userEscrowedShares[owner_] = escrowSharesTotal_; + } + +} + +contract SortedLinkedListHarness { + + SortedLinkedList.List list; + + function push(uint128 value_) external { + SortedLinkedList.push(list, value_); + } + + function remove(uint128 value_) external { + SortedLinkedList.remove(list, value_); + } + + function length() external view returns (uint256) { + return SortedLinkedList.length(list); + } + + function getAllValues() external view returns (uint128[] memory) { + return SortedLinkedList.getAllValues(list); + } + + function getLast() external view returns (uint128) { + return SortedLinkedList.getLast(list); + } + } diff --git a/tests/utils/Mocks.sol b/tests/utils/Mocks.sol index facfe09..4706c38 100644 --- a/tests/utils/Mocks.sol +++ b/tests/utils/Mocks.sol @@ -108,7 +108,7 @@ contract MockPool is MockERC20 { asset_ = address(_asset); } - function redeem(uint256, address, address) external returns (uint256 assets_) { + function redeem(uint256, address, address) external pure returns (uint256 assets_) { assets_; // Ignore variable } diff --git a/tests/utils/TestBase.sol b/tests/utils/TestBase.sol index 88d813c..e044fc5 100644 --- a/tests/utils/TestBase.sol +++ b/tests/utils/TestBase.sol @@ -10,6 +10,8 @@ import { MapleWithdrawalManagerInitializer } from "../../contracts/proxy/MapleWi import { MapleWithdrawalManagerHarness } from "./Harnesses.sol"; import { MockFactory, MockGlobals, MockPool, MockPoolManager } from "./Mocks.sol"; +import { SortedLinkedListHarness } from "./Harnesses.sol"; + contract TestBase is Test { address internal governor = makeAddr("governor"); @@ -33,6 +35,13 @@ contract TestBase is Test { MapleWithdrawalManagerFactory internal factory; MapleWithdrawalManagerHarness internal withdrawalManager; + event EmptyRedemptionsProcessed(uint256 numberOfRequestsProcessed); + event ManualWithdrawalSet(address indexed account, bool isManual); + event RequestCreated(uint256 indexed requestId, address indexed owner, uint256 shares); + event RequestDecreased(uint256 indexed requestId, uint256 shares); + event RequestProcessed(uint256 indexed requestId, address indexed owner, uint256 shares, uint256 assets); + event RequestRemoved(uint256 indexed requestId); + function setUp() public virtual { // Create all mocks. asset = new MockERC20("Wrapped Ether", "WETH", 18); @@ -71,18 +80,45 @@ contract TestBase is Test { wm = address(withdrawalManager); } - function assertRequest(uint128 requestId, address owner, uint256 shares) internal { + function assertRequest(uint256 requestId, address owner, uint256 shares) internal { ( address owner_, uint256 shares_ ) = withdrawalManager.requests(requestId); assertEq(owner_, owner); assertEq(shares_, shares); } - function assertQueue(uint128 nextRequestId, uint128 lastRequestId) internal { - ( uint128 nextRequestId_, uint128 lastRequestId_ ) = withdrawalManager.queue(); + function assertQueue(uint256 nextRequestId, uint256 lastRequestId) internal { + ( uint256 nextRequestId_, uint256 lastRequestId_ ) = withdrawalManager.queue(); assertEq(nextRequestId_, nextRequestId); assertEq(lastRequestId_, lastRequestId); } + function getLastRequestByOwner(address owner) internal view returns (uint256 lastRequestId, uint256 shares) { + ( uint256[] memory requestIds_, uint256[] memory shares_) = withdrawalManager.requestsByOwner(owner); + + return requestIds_.length == 0 ? (0,0) : (requestIds_[requestIds_.length - 1], shares_[shares_.length - 1]); + } + } + +contract SortedLinkedListTestBase is Test { + + SortedLinkedListHarness public list; + + function setUp() public virtual { + list = new SortedLinkedListHarness(); + } + + function assertList(uint256[] memory values) internal { + assertEq(list.length(), values.length); + + uint128[] memory listValues = list.getAllValues(); + + for (uint256 i = 0; i < values.length; i++) { + assertEq(uint256(listValues[i]), values[i]); + } + } + +} +