Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion solidity/src/FlowYieldVaultsRequests.sol
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,12 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
/// @notice YieldVault Id is invalid or not owned by user
error InvalidYieldVaultId(uint64 yieldVaultId, address user);

/// @notice YieldVault Id has already been registered
error YieldVaultIdAlreadyRegistered(uint64 yieldVaultId);

/// @notice YieldVault Id does not match the request's value
error YieldVaultIdMismatch(uint64 expectedId, uint64 providedId);

/// @notice YieldVault token is not set
error YieldVaultTokenNotSet(uint64 yieldVaultId);

Expand Down Expand Up @@ -1113,7 +1119,14 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
: RequestStatus.FAILED;
request.status = newStatus;
request.message = message;
request.yieldVaultId = yieldVaultId;

// Enforce strong ID binding for DEPOSIT/WITHDRAW/CLOSE by requiring the
// supplied yieldVaultId matches the request's stored yieldVaultId
if (request.requestType == RequestType.CREATE_YIELDVAULT) {
request.yieldVaultId = yieldVaultId;
} else if (request.yieldVaultId != yieldVaultId) {
revert YieldVaultIdMismatch(request.yieldVaultId, yieldVaultId);
}

// === HANDLE REFUNDS FOR FAILED CREATE/DEPOSIT ===
// COA must return the funds that were transferred in startProcessing
Expand Down Expand Up @@ -1548,6 +1561,10 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
address tokenAddress,
uint256 requestId
) internal {
// Uniqueness guard, reject registering an already-valid yieldVaultId
if (validYieldVaultIds[yieldVaultId]) {
revert YieldVaultIdAlreadyRegistered(yieldVaultId);
}
// Reject sentinel value to prevent corruption of "no yieldvault" semantics
if (yieldVaultId == NO_YIELDVAULT_ID) revert CannotRegisterSentinelYieldVaultId();

Expand Down Expand Up @@ -1576,6 +1593,11 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
* @param requestId The CLOSE_YIELDVAULT request ID that closed this YieldVault.
*/
function _unregisterYieldVault(uint64 yieldVaultId, address user, uint256 requestId) internal {
// Prove the yieldVaultId is actually registered under the provided user
if (yieldVaultOwners[yieldVaultId] != user) {
revert InvalidYieldVaultId(yieldVaultId, user);
}

uint64[] storage userYieldVaults = yieldVaultsByUser[user];
uint256 indexToRemove = _yieldVaultIndexInUserArray[user][yieldVaultId];

Expand Down
64 changes: 64 additions & 0 deletions solidity/test/FlowYieldVaultsRequests.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1335,4 +1335,68 @@ contract FlowYieldVaultsRequestsTest is Test {
(uint256[] memory userIds, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user);
assertEq(userIds.length, 2);
}

// Test that duplicate registration is blocked
function test_RegisterYieldVault_RevertAlreadyRegistered() public {
vm.prank(user);
uint256 reqId = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID);

vm.startPrank(coa);
c.startProcessing(reqId);
c.completeProcessing(reqId, true, 100, "Created");

// Try to register same ID again (simulate COA bug)
uint256 reqId2 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID);
c.startProcessing(reqId2);
vm.expectRevert(abi.encodeWithSelector(
FlowYieldVaultsRequests.YieldVaultIdAlreadyRegistered.selector,
100
));
c.completeProcessing(reqId2, true, 100, "Duplicate");
vm.stopPrank();
}

// Test that ID mismatch on DEPOSIT is blocked
function test_CompleteProcessing_RevertDepositIdMismatch() public {
vm.prank(user);
uint256 reqId = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID);

vm.startPrank(coa);
c.startProcessing(reqId);
c.completeProcessing(reqId, true, 100, "Created");
vm.stopPrank();

// Try to deposit but COA provides wrong ID
vm.prank(user);
uint256 depositReq = c.depositToYieldVault{value: 1 ether}(100, NATIVE_FLOW, 1 ether);

vm.startPrank(coa);
c.startProcessing(depositReq);
vm.expectRevert(abi.encodeWithSelector(
FlowYieldVaultsRequests.YieldVaultIdMismatch.selector,
100, // expected
101 // provided (wrong)
));
c.completeProcessing(depositReq, true, 101, "Wrong ID");
vm.stopPrank();
}

// Test for unregister with wrong user ownership
function test_CompleteProcessing_RevertInvalidYieldVaultId() public {
vm.prank(user);
uint256 reqId = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID);

vm.startPrank(coa);
c.startProcessing(reqId);
c.completeProcessing(reqId, true, 100, "Created");
vm.stopPrank();

vm.prank(user2);
vm.expectRevert(abi.encodeWithSelector(
FlowYieldVaultsRequests.InvalidYieldVaultId.selector,
100,
user2
));
uint256 closeReq = c.closeYieldVault(100);
}
}