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
-
+
[](https://maplefinance.gitbook.io/maple/maple-for-developers/protocol-overview)
[![Foundry][foundry-badge]][foundry]
-[](https://github.com/maple-labs/withdrawal-manager-queue-private/blob/main/LICENSE)
+[](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]);
+ }
+ }
+
+}
+