Skip to content

Commit

Permalink
Fix redeem queue struct changes - PE-50 (#289)
Browse files Browse the repository at this point in the history
* fix: update redeem queue structure in state to prevent storage corruption

* chore: update redeemManager test to reflect changes

* chore: add migration test on mainnet and testnet fork

* chore: added some logs for testing

* chore: some code cleanup

* chore: add deployment file for redeemManager upgrade

* chore: natspec upgrade

* fix: introduce redeemQueueV1_2 structure for migrstion on dev

* chore: update redeemManager migration test

* chore: update deployment file for redeemManager upgrade

* chore: fix formatting

* chore: add unit tests for InitializeRedeemManagerV1_2

* chore: update InitializeRedeemManagerV1_2 test

* chore: improve formatting
  • Loading branch information
koderholic authored Sep 11, 2024
1 parent 7f7a245 commit 77d0670
Show file tree
Hide file tree
Showing 17 changed files with 1,179 additions and 70 deletions.
76 changes: 62 additions & 14 deletions contracts/src/RedeemManager.1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import "./libraries/LibUint256.sol";
import "./Initializable.sol";

import "./state/shared/RiverAddress.sol";
import "./state/redeemManager/RedeemQueue.sol";
import "./state/redeemManager/RedeemQueue.1.sol";
import "./state/redeemManager/RedeemQueue.2.sol";
import "./state/redeemManager/RedeemQueue.1.2.sol";
import "./state/redeemManager/WithdrawalStack.sol";
import "./state/redeemManager/BufferedExceedingEth.sol";
import "./state/redeemManager/RedeemDemand.sol";
Expand Down Expand Up @@ -64,23 +66,69 @@ contract RedeemManagerV1 is Initializable, IRedeemManagerV1, IProtocolVersion {
emit SetRiver(_river);
}

function initializeRedeemManagerV1_2(address[] calldata _prevInitiators) external init(1) {
_redeemQueueMigrationV1_2(_prevInitiators);
}

function _redeemQueueMigrationV1_2(address[] memory _prevInitiators) internal {
RedeemQueueV1.RedeemRequest[] memory initialQueue = RedeemQueueV1.get();
RedeemQueueV1_2.RedeemRequest[] memory currentQueue = RedeemQueueV1_2.get(); //TODO: Remove after dev upgrade, not needed for staging/prod
uint256 currentQueueLen = currentQueue.length;
RedeemQueueV2.RedeemRequest[] storage newQueue = RedeemQueueV2.get();

//TODO: Remove after dev upgrade, not needed for staging/prod
if (_prevInitiators.length != 7) {
revert IncompatibleArrayLengths();
}

//TODO: Remove after dev upgrade, not needed for staging/prod
for (uint256 i = 0; i < 7;) {
newQueue[i] = RedeemQueueV2.RedeemRequest({
amount: initialQueue[i].amount,
maxRedeemableEth: initialQueue[i].maxRedeemableEth,
recipient: initialQueue[i].recipient,
height: initialQueue[i].height,
initiator: _prevInitiators[i] // Assign the provided initiators
});

unchecked {
++i;
}
}

uint256 heightDeficit = initialQueue[6].height + initialQueue[6].amount;
for (uint256 i = 7; i < currentQueueLen;) {
newQueue[i] = RedeemQueueV2.RedeemRequest({
amount: currentQueue[i].amount,
maxRedeemableEth: currentQueue[i].maxRedeemableEth,
recipient: currentQueue[i].recipient,
height: currentQueue[i].height + heightDeficit,
initiator: currentQueue[i].initiator // Reuse the initiator from the current queue
});

unchecked {
++i;
}
}
}

/// @inheritdoc IRedeemManagerV1
function getRiver() external view returns (address) {
return RiverAddress.get();
}

/// @inheritdoc IRedeemManagerV1
function getRedeemRequestCount() external view returns (uint256) {
return RedeemQueue.get().length;
return RedeemQueueV2.get().length;
}

/// @inheritdoc IRedeemManagerV1
function getRedeemRequestDetails(uint32 _redeemRequestId)
external
view
returns (RedeemQueue.RedeemRequest memory)
returns (RedeemQueueV2.RedeemRequest memory)
{
return RedeemQueue.get()[_redeemRequestId];
return RedeemQueueV2.get()[_redeemRequestId];
}

/// @inheritdoc IRedeemManagerV1
Expand Down Expand Up @@ -206,7 +254,7 @@ contract RedeemManagerV1 is Initializable, IRedeemManagerV1, IProtocolVersion {
/// @param _withdrawalEvent The load withdrawal event
/// @return True if matching
function _isMatch(
RedeemQueue.RedeemRequest memory _redeemRequest,
RedeemQueueV2.RedeemRequest memory _redeemRequest,
WithdrawalStack.WithdrawalEvent memory _withdrawalEvent
) internal pure returns (bool) {
return (
Expand All @@ -218,7 +266,7 @@ contract RedeemManagerV1 is Initializable, IRedeemManagerV1, IProtocolVersion {
/// @notice Internal utility to perform a dichotomic search of the withdrawal event to use to claim the redeem request
/// @param _redeemRequest The redeem request to resolve
/// @return The matching withdrawal event
function _performDichotomicResolution(RedeemQueue.RedeemRequest memory _redeemRequest)
function _performDichotomicResolution(RedeemQueueV2.RedeemRequest memory _redeemRequest)
internal
view
returns (int64)
Expand Down Expand Up @@ -266,12 +314,12 @@ contract RedeemManagerV1 is Initializable, IRedeemManagerV1, IProtocolVersion {
uint32 _redeemRequestId,
WithdrawalStack.WithdrawalEvent memory _lastWithdrawalEvent
) internal view returns (int64 withdrawalEventId) {
RedeemQueue.RedeemRequest[] storage redeemRequests = RedeemQueue.get();
RedeemQueueV2.RedeemRequest[] storage redeemRequests = RedeemQueueV2.get();
// if the redeem request id is >= than the size of requests, we know it's out of bounds and doesn't exist
if (_redeemRequestId >= redeemRequests.length) {
return RESOLVE_OUT_OF_BOUNDS;
}
RedeemQueue.RedeemRequest memory redeemRequest = redeemRequests[_redeemRequestId];
RedeemQueueV2.RedeemRequest memory redeemRequest = redeemRequests[_redeemRequestId];
// if the redeem request remaining amount is 0, we know that the request has been entirely claimed
if (redeemRequest.amount == 0) {
return RESOLVE_FULLY_CLAIMED;
Expand Down Expand Up @@ -301,18 +349,18 @@ contract RedeemManagerV1 is Initializable, IRedeemManagerV1, IProtocolVersion {
if (!_castedRiver().transferFrom(msg.sender, address(this), _lsETHAmount)) {
revert TransferError();
}
RedeemQueue.RedeemRequest[] storage redeemRequests = RedeemQueue.get();
RedeemQueueV2.RedeemRequest[] storage redeemRequests = RedeemQueueV2.get();
redeemRequestId = uint32(redeemRequests.length);
uint256 height = 0;
if (redeemRequestId != 0) {
RedeemQueue.RedeemRequest memory previousRedeemRequest = redeemRequests[redeemRequestId - 1];
RedeemQueueV2.RedeemRequest memory previousRedeemRequest = redeemRequests[redeemRequestId - 1];
height = previousRedeemRequest.height + previousRedeemRequest.amount;
}

uint256 maxRedeemableEth = _castedRiver().underlyingBalanceFromShares(_lsETHAmount);

redeemRequests.push(
RedeemQueue.RedeemRequest({
RedeemQueueV2.RedeemRequest({
height: height,
amount: _lsETHAmount,
recipient: _recipient,
Expand All @@ -329,7 +377,7 @@ contract RedeemManagerV1 is Initializable, IRedeemManagerV1, IProtocolVersion {
/// @notice Internal structure used to optimize stack usage in _claimRedeemRequest
struct ClaimRedeemRequestParameters {
/// @custom:attribute The structure of the redeem request to claim
RedeemQueue.RedeemRequest redeemRequest;
RedeemQueueV2.RedeemRequest redeemRequest;
/// @custom:attribute The structure of the withdrawal event to use to claim the redeem request
WithdrawalStack.WithdrawalEvent withdrawalEvent;
/// @custom:attribute The id of the redeem request to claim
Expand Down Expand Up @@ -359,7 +407,7 @@ contract RedeemManagerV1 is Initializable, IRedeemManagerV1, IProtocolVersion {
/// @notice Internal utility to save a redeem request to storage
/// @param _params The parameters of the claim redeem request call
function _saveRedeemRequest(ClaimRedeemRequestParameters memory _params) internal {
RedeemQueue.RedeemRequest[] storage redeemRequests = RedeemQueue.get();
RedeemQueueV2.RedeemRequest[] storage redeemRequests = RedeemQueueV2.get();
redeemRequests[_params.redeemRequestId].height = _params.redeemRequest.height;
redeemRequests[_params.redeemRequestId].amount = _params.redeemRequest.amount;
redeemRequests[_params.redeemRequestId].maxRedeemableEth = _params.redeemRequest.maxRedeemableEth;
Expand Down Expand Up @@ -465,7 +513,7 @@ contract RedeemManagerV1 is Initializable, IRedeemManagerV1, IProtocolVersion {
}
claimStatuses = new uint8[](redeemRequestIdsLength);

RedeemQueue.RedeemRequest[] storage redeemRequests = RedeemQueue.get();
RedeemQueueV2.RedeemRequest[] storage redeemRequests = RedeemQueueV2.get();
WithdrawalStack.WithdrawalEvent[] storage withdrawalEvents = WithdrawalStack.get();

ClaimRedeemRequestParameters memory params;
Expand Down
7 changes: 5 additions & 2 deletions contracts/src/interfaces/IRedeemManager.1.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.20;

import "../state/redeemManager/RedeemQueue.sol";
import "../state/redeemManager/RedeemQueue.2.sol";
import "../state/redeemManager/WithdrawalStack.sol";

/// @title Redeem Manager Interface (v1)
Expand Down Expand Up @@ -112,6 +112,9 @@ interface IRedeemManagerV1 {
/// @param _river The address of the River contract
function initializeRedeemManagerV1(address _river) external;

/// @param _prevInitiators The address of initiators for existing requests
function initializeRedeemManagerV1_2(address[] calldata _prevInitiators) external;

/// @notice Retrieve River address
/// @return The address of River
function getRiver() external view returns (address);
Expand All @@ -125,7 +128,7 @@ interface IRedeemManagerV1 {
function getRedeemRequestDetails(uint32 _redeemRequestId)
external
view
returns (RedeemQueue.RedeemRequest memory);
returns (RedeemQueueV2.RedeemRequest memory);

/// @notice Retrieve the global count of withdrawal events
function getWithdrawalEventCount() external view returns (uint256);
Expand Down
32 changes: 32 additions & 0 deletions contracts/src/state/redeemManager/RedeemQueue.1.2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.20;

/// @title Redeem Manager Redeem Queue storage
/// @notice Utility to manage the Redeem Queue in the Redeem Manager
library RedeemQueueV1_2 {
/// @notice Storage slot of the Redeem Queue
bytes32 internal constant REDEEM_QUEUE_ID_SLOT = bytes32(uint256(keccak256("river.state.redeemQueue")) - 1);

/// @notice The updated V1_2 Redeemer structure represents the redeem request made by a user
struct RedeemRequest {
/// @custom:attribute The amount of the redeem request in LsETH
uint256 amount;
/// @custom:attribute The maximum amount of ETH redeemable by this request
uint256 maxRedeemableEth;
/// @custom:attribute The recipient of the redeem request
address recipient;
/// @custom:attribute The initiator of the redeem request
address initiator;
/// @custom:attribute The height is the cumulative sum of all the sizes of preceding redeem requests
uint256 height;
}

/// @notice Retrieve the Redeem Queue array storage pointer
/// @return data The Redeem Queue array storage pointer
function get() internal pure returns (RedeemRequest[] storage data) {
bytes32 position = REDEEM_QUEUE_ID_SLOT;
assembly {
data.slot := position
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity 0.8.20;

/// @title Redeem Manager Redeem Queue storage
/// @notice Utility to manage the Redeem Queue in the Redeem Manager
library RedeemQueue {
library RedeemQueueV1 {
/// @notice Storage slot of the Redeem Queue
bytes32 internal constant REDEEM_QUEUE_ID_SLOT = bytes32(uint256(keccak256("river.state.redeemQueue")) - 1);

Expand All @@ -15,8 +15,6 @@ library RedeemQueue {
uint256 maxRedeemableEth;
/// @custom:attribute The recipient of the redeem request
address recipient;
/// @custom:attribute The initiator of the redeem request
address initiator;
/// @custom:attribute The height is the cumulative sum of all the sizes of preceding redeem requests
uint256 height;
}
Expand Down
32 changes: 32 additions & 0 deletions contracts/src/state/redeemManager/RedeemQueue.2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.20;

/// @title Redeem Manager Redeem Queue storage
/// @notice Utility to manage the Redeem Queue in the Redeem Manager
library RedeemQueueV2 {
/// @notice Storage slot of the Redeem Queue
bytes32 internal constant REDEEM_QUEUE_ID_SLOT = bytes32(uint256(keccak256("river.state.redeemQueue")) - 1);

/// @notice The updated V1_2 Redeemer structure represents the redeem request made by a user
struct RedeemRequest {
/// @custom:attribute The amount of the redeem request in LsETH
uint256 amount;
/// @custom:attribute The maximum amount of ETH redeemable by this request
uint256 maxRedeemableEth;
/// @custom:attribute The recipient of the redeem request
address recipient;
/// @custom:attribute The height is the cumulative sum of all the sizes of preceding redeem requests
uint256 height;
/// @custom:attribute The initiator of the redeem request
address initiator;
}

/// @notice Retrieve the Redeem Queue array storage pointer
/// @return data The Redeem Queue array storage pointer
function get() internal pure returns (RedeemRequest[] storage data) {
bytes32 position = REDEEM_QUEUE_ID_SLOT;
assembly {
data.slot := position
}
}
}
Loading

0 comments on commit 77d0670

Please sign in to comment.