Skip to content

Commit 6cf50a2

Browse files
authored
remove preunlink actions from unlinkOwner (#202)
1 parent 085d014 commit 6cf50a2

File tree

11 files changed

+272
-566
lines changed

11 files changed

+272
-566
lines changed

contracts/gas-snapshots/workflow.gas-snapshot

Lines changed: 102 additions & 115 deletions
Large diffs are not rendered by default.

contracts/src/v0.8/workflow/dev/v2/WorkflowRegistry.sol

Lines changed: 45 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ contract WorkflowRegistry is Ownable2StepMsgSender, ITypeAndVersion {
158158
error InvalidSignature(bytes signature, uint8 recoverErrorId, bytes32 recoverErrorArg);
159159
error InvalidOwnershipLink(address owner, uint256 validityTimestamp, bytes32 proof, bytes signature);
160160
error OwnershipProofAlreadyUsed(address caller, bytes32 proof);
161-
error CannotUnlinkWithActiveWorkflows();
161+
162162
error CallerIsNotWorkflowOwner(address caller);
163163
error DonLimitNotSet(string donFamily);
164164
error MaxWorkflowsPerUserDONExceeded(address owner, string donFamily);
@@ -184,13 +184,6 @@ contract WorkflowRegistry is Ownable2StepMsgSender, ITypeAndVersion {
184184
PAUSED
185185
}
186186

187-
enum PreUnlinkAction {
188-
NONE, // No action prior to unlinking.
189-
REMOVE_WORKFLOWS, // Remove all workflows owned by the owner prior to unlinking.
190-
PAUSE_WORKFLOWS // Pause all workflows owned by the owner prior to unlinking.
191-
192-
}
193-
194187
enum LinkingRequestType {
195188
LINK_OWNER, // Request to link an owner address.
196189
UNLINK_OWNER // Request to unlink an owner address.
@@ -661,35 +654,19 @@ contract WorkflowRegistry is Ownable2StepMsgSender, ITypeAndVersion {
661654
emit OwnershipLinkUpdated(msg.sender, proof, true);
662655
}
663656

664-
/// @notice View function to verify if the unlinkOwner() function can be called successfully.
657+
/// @notice Validates whether an owner can be unlinked using the provided proof and signature.
665658
/// @param owner The address of the owner to be unlinked.
666659
/// @param validityTimestamp Validity of the ownership proof.
667660
/// @param signature The signature of the ownership proof metadata.
668-
/// preUnlinkAction Determines what to do with existing workflows owned by the owner before unlinking.
669-
/// @dev If preUnlinkAction is NONE, the function will check if there are any active workflows registered to the
670-
/// owner, and if so, the transaction will revert. If preUnlinkAction is not NONE, then all workflows owned by this
671-
/// owner's address will be removed or paused before unlinking is completed.
672661
/// @dev This function is used to verify if the ownership proof is valid without actually unlinking the owner address.
673662
/// The ownership proof metadata is a combination of the claimed owner address, validity timestamp, and the proof hash.
674-
/// Request will be rejected if the validity timestamp has expired, owner addres is not linked, if the proof does not
663+
/// Request will be rejected if the validity timestamp has expired, owner address is not linked, if the proof does not
675664
/// match the one that was originally submitted, or if the signature is not valid (for different reasons).
676-
/// @dev It is essential to ensure that the unlinking process does not leave any active workflows running because
677-
/// they can't be managed on the registry by anyone else aside from a valid owner. Without this, the workflows
678-
/// would be stuck since they can't be managed or removed by anyone.
679665
/// @dev Important difference between linking and unlinking is that unlinking may be called by any address, as
680666
/// long as the valid proof is provided. The caller does not have to be the owner of the address being unlinked.
681667
/// This is done to ensure that unlinking can be done even in cases when access to the private key of the owner
682668
/// address is lost or compromised, and the owner is not able to submit the unlinking request themselves.
683-
function canUnlinkOwner(
684-
address owner,
685-
uint256 validityTimestamp,
686-
bytes calldata signature,
687-
PreUnlinkAction action
688-
) public view {
689-
if ((action == PreUnlinkAction.NONE) && (s_activeOwnerWorkflowRids[owner].length() != 0)) {
690-
revert CannotUnlinkWithActiveWorkflows();
691-
}
692-
669+
function canUnlinkOwner(address owner, uint256 validityTimestamp, bytes calldata signature) public view {
693670
if (block.timestamp > validityTimestamp) {
694671
revert UnlinkOwnerRequestExpired(owner, block.timestamp, validityTimestamp);
695672
}
@@ -714,36 +691,38 @@ contract WorkflowRegistry is Ownable2StepMsgSender, ITypeAndVersion {
714691
/// @param owner The address of the owner to be unlinked.
715692
/// @param validityTimestamp Validity of the ownership proof.
716693
/// @param signature The signature of the ownership proof metadata.
717-
/// preUnlinkAction Determines what to do with existing workflows owned by the owner before unlinking.
718-
/// @dev If preUnlinkAction is NONE, the function will check if there are any active workflows registered to the
719-
/// owner, and if so, the transaction will revert. If preUnlinkAction is not NONE, then all workflows owned by this
720-
/// owner's address will be removed or paused before unlinking is completed.
721-
/// @dev Run the verification process first by calling canUnlinkOwner() function. If the verification does not result
722-
/// in a revert, then the ownership proof is valid and the owner address can be unlinked.
723-
function unlinkOwner(
724-
address owner,
725-
uint256 validityTimestamp,
726-
bytes calldata signature,
727-
PreUnlinkAction action
728-
) external {
729-
canUnlinkOwner(owner, validityTimestamp, signature, action);
730-
731-
if (action != PreUnlinkAction.NONE) {
732-
EnumerableSet.Bytes32Set storage active = s_activeOwnerWorkflowRids[owner];
733-
// ------------- PAUSE or DELETE -------------
734-
// Iterate from the back since EnumerableSet.remove() swaps-and-pops.
735-
while (active.length() > 0) {
736-
bytes32 rid = active.at(active.length() - 1);
737-
WorkflowMetadata storage rec = _getRecord(owner, rid);
738-
if (action == PreUnlinkAction.PAUSE_WORKFLOWS) {
739-
_applyPause(rid, rec);
740-
} else {
741-
_applyDelete(rid, rec);
742-
}
743-
}
694+
/// @dev The function will automatically delete all workflows owned by the owner before unlinking.
695+
/// Upstream callers are responsible for ensuring this is the intended behavior.
696+
/// @dev The function validates the ownership proof and signature before proceeding with deletion and unlinking.
697+
function unlinkOwner(address owner, uint256 validityTimestamp, bytes calldata signature) external {
698+
// Validate the unlinking request
699+
if (block.timestamp > validityTimestamp) {
700+
revert UnlinkOwnerRequestExpired(owner, block.timestamp, validityTimestamp);
744701
}
745702

703+
if (!s_linkedOwners.contains(owner)) {
704+
revert OwnershipLinkDoesNotExist(owner);
705+
}
706+
707+
// The expectation is that the signature must contain the same proof that was originally used for the linking
746708
bytes32 storedProof = s_linkedOwners.get(owner);
709+
710+
// Request type prevents replay attacks, since the same proof can be used for both linking and unlinking
711+
address signer =
712+
_recoverSigner(uint8(LinkingRequestType.UNLINK_OWNER), owner, validityTimestamp, storedProof, signature);
713+
if (!s_allowedSigners[signer]) {
714+
revert InvalidOwnershipLink(owner, validityTimestamp, storedProof, signature);
715+
}
716+
717+
// Delete all workflows owned by the owner
718+
EnumerableSet.Bytes32Set storage allRids = s_allOwnerRids[owner];
719+
// Iterate from the back since EnumerableSet.remove() swaps-and-pops.
720+
while (allRids.length() > 0) {
721+
bytes32 rid = allRids.at(allRids.length() - 1);
722+
WorkflowMetadata storage rec = s_workflows[rid];
723+
_applyDelete(rid, rec);
724+
}
725+
747726
s_linkedOwners.remove(owner);
748727
emit OwnershipLinkUpdated(owner, storedProof, false);
749728
}
@@ -1139,19 +1118,25 @@ contract WorkflowRegistry is Ownable2StepMsgSender, ITypeAndVersion {
11391118

11401119
/// @notice Permanently delete a workflow owned by the caller.
11411120
/// @dev Sequence:
1142-
/// 1. Resolve the registry-ID (`rid`) from `workflowId` and verify ownership.
1143-
/// 2. If the workflow is **ACTIVE**, remove it from every
1144-
/// “active” index and decrement per-DON counters.
1145-
/// 3. Purge the RID from global owner / DON maps and key-based sets.
1146-
/// 4. Clear the ID→RID map, delete the primary record, and emit an event.
1147-
/// 5. Unlinked owners can still delete their workflows.
1121+
/// 1. Verify the caller (owner) is linked to the registry.
1122+
/// 2. Resolve the registry-ID (`rid`) from `workflowId` and verify ownership.
1123+
/// 3. If the workflow is **ACTIVE**, remove it from every
1124+
/// "active" index and decrement per-DON counters.
1125+
/// 4. Purge the RID from global owner / DON maps and key-based sets.
1126+
/// 5. Clear the ID→RID map, delete the primary record, and emit an event.
11481127
///
11491128
/// @param workflowId The globally-unique identifier to remove.
1129+
/// @custom:reverts OwnershipLinkDoesNotExist If the caller is not linked.
11501130
/// @custom:reverts WorkflowDoesNotExist If the ID is unknown.
11511131
/// @custom:reverts CallerIsNotWorkflowOwner If `msg.sender` is not the owner.
11521132
function deleteWorkflow(
11531133
bytes32 workflowId
11541134
) external {
1135+
// Check that the caller (owner) is linked
1136+
if (!s_linkedOwners.contains(msg.sender)) {
1137+
revert OwnershipLinkDoesNotExist(msg.sender);
1138+
}
1139+
11551140
bytes32 rid = s_idToRid[workflowId];
11561141
WorkflowMetadata storage rec = _getRecord(msg.sender, rid);
11571142
_applyDelete(rid, rec);

contracts/src/v0.8/workflow/dev/v2/test/WorkflowRegistry/WorkflowRegistry.canUnlinkOwner.t.sol

Lines changed: 12 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,17 @@ contract WorkflowRegistry_canUnlinkOwner is WorkflowRegistrySetup {
1111
_;
1212
}
1313

14-
// whenPreUnlinkActionIsNONE
15-
function test_canUnlinkOwner_WhenOwnerHasActiveWorkflows() external whenOwnerIsLinked {
16-
// it should revert with CannotUnlinkWithActiveWorkflows
14+
function test_canUnlinkOwner_WhenOwnerHasExistingWorkflows() external whenOwnerIsLinked {
15+
// it should return successfully since workflows will be automatically deleted
1716
_setDONLimit();
1817
_upsertTestWorklows(WorkflowRegistry.WorkflowStatus.ACTIVE, false, s_owner);
1918

2019
(, bytes memory sig) = _getUnlinkProofSignature(s_owner);
21-
vm.expectRevert(abi.encodeWithSelector(WorkflowRegistry.CannotUnlinkWithActiveWorkflows.selector));
22-
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig, WorkflowRegistry.PreUnlinkAction.NONE);
20+
// Should not revert - workflows will be automatically deleted during unlinking
21+
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig);
2322
}
2423

25-
// whenPreUnlinkActionIsNONE
26-
// whenOwnerHasNoActiveWorkflows
24+
// whenOwnerHasNoExistingWorkflows
2725
function test_canUnlinkOwner_WhenValidTimestampIsLessThanBlockTimestamp() external whenOwnerIsLinked {
2826
// it should revert with UnlinkOwnerRequestExpired
2927
// block time has advanced by 24 hours so the validity timestamp is in the past
@@ -34,23 +32,21 @@ contract WorkflowRegistry_canUnlinkOwner is WorkflowRegistrySetup {
3432
WorkflowRegistry.UnlinkOwnerRequestExpired.selector, s_owner, block.timestamp, s_validityTimestamp
3533
)
3634
);
37-
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig, WorkflowRegistry.PreUnlinkAction.NONE);
35+
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig);
3836
assertTrue(s_registry.isOwnerLinked(s_owner), "Owner should be linked");
3937
}
4038

41-
// whenPreUnlinkActionIsNONE
42-
// whenOwnerHasNoActiveWorkflows
39+
// whenOwnerHasNoExistingWorkflows
4340
// whenValidTimestampGreaterThanOrEqualToBlockTimestamp
4441
function test_canUnlinkOwner_WhenOwnerIsNotYetLinked() external {
4542
// it should revert with OwnershipLinkDoesNotExist
4643
(, bytes memory sig) = _getUnlinkProofSignature(s_user);
4744
vm.expectRevert(abi.encodeWithSelector(WorkflowRegistry.OwnershipLinkDoesNotExist.selector, s_owner));
48-
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig, WorkflowRegistry.PreUnlinkAction.NONE);
45+
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig);
4946
assertFalse(s_registry.isOwnerLinked(s_owner), "Owner should not be linked");
5047
}
5148

52-
// whenPreUnlinkActionIsNONE
53-
// whenOwnerHasNoActiveWorkflows
49+
// whenOwnerHasNoExistingWorkflows
5450
// whenValidTimestampGreaterThanOrEqualToBlockTimestamp
5551
function test_canUnlinkOwner_WhenTheSignatureDoesNotRecoverAnAllowedSigner() external whenOwnerIsLinked {
5652
// it should revert with InvalidOwnershipLink
@@ -69,68 +65,14 @@ contract WorkflowRegistry_canUnlinkOwner is WorkflowRegistrySetup {
6965
WorkflowRegistry.InvalidOwnershipLink.selector, s_owner, s_validityTimestamp, ownerProof, sig
7066
)
7167
);
72-
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig, WorkflowRegistry.PreUnlinkAction.NONE);
68+
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig);
7369
}
7470

75-
// whenPreUnlinkActionIsNONE
76-
// whenOwnerHasNoActiveWorkflows
71+
// whenOwnerHasNoExistingWorkflows
7772
// whenValidTimestampGreaterThanOrEqualToBlockTimestamp
7873
function test_canUnlinkOwner_WhenTheSignatureRecoversAnAllowedSigner() external whenOwnerIsLinked {
7974
// it should return (no revert)
8075
(, bytes memory sig) = _getUnlinkProofSignature(s_owner);
81-
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig, WorkflowRegistry.PreUnlinkAction.NONE);
82-
}
83-
84-
// whenPreUnlinkeActionIsPAUSE_WORKFLOWS_Or_REMOVE_WORKFLOWS
85-
function test_canUnlinkOwner_WhenValidityTimestampIsLessThanBlockTimestamp() external {
86-
// it should revert with UnlinkOwnerRequestExpired
87-
// block time has advanced by 24 hours so the validity timestamp is in the past
88-
vm.warp(block.timestamp + 24 hours);
89-
(, bytes memory sig) = _getUnlinkProofSignature(s_owner);
90-
vm.expectRevert(
91-
abi.encodeWithSelector(
92-
WorkflowRegistry.UnlinkOwnerRequestExpired.selector, s_owner, block.timestamp, s_validityTimestamp
93-
)
94-
);
95-
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig, WorkflowRegistry.PreUnlinkAction.PAUSE_WORKFLOWS);
96-
}
97-
98-
// whenPreUnlinkeActionIsPAUSE_WORKFLOWSOrREMOVE_WORKFLOWS
99-
// whenValidityTimestampGreaterThanOrEqualToBlockTimestamp
100-
function test_canUnlinkOwner_WhenOwnerIsNotLinked() external {
101-
// it should revert with OwnershipLinkDoesNotExist(owner)
102-
(, bytes memory sig) = _getUnlinkProofSignature(s_user);
103-
vm.expectRevert(abi.encodeWithSelector(WorkflowRegistry.OwnershipLinkDoesNotExist.selector, s_owner));
104-
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig, WorkflowRegistry.PreUnlinkAction.PAUSE_WORKFLOWS);
105-
}
106-
107-
// whenPreUnlinkeActionIsPAUSE_WORKFLOWSOrREMOVE_WORKFLOWS
108-
// whenValidityTimestampGreaterThanOrEqualToBlockTimestamp
109-
function test_canUnlinkOwner_WhenSignatureDoesNotRecoverAnAllowedSigner() external whenOwnerIsLinked {
110-
// it should revert with InvalidOwnershipLink
111-
(bytes32 ownerProof,) = _getUnlinkProofSignature(s_owner);
112-
uint256 randomPrivateKey = 0x7f3c2a9b5d4e1f8c0b2d3a4e5f6c7d8e9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d;
113-
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
114-
randomPrivateKey,
115-
LinkingUtils.getMessageHash(
116-
LinkingUtils.REQUEST_TYPE_LINK, address(s_registry), s_owner, s_validityTimestamp, ownerProof
117-
)
118-
);
119-
120-
bytes memory sig = abi.encodePacked(r, s, v);
121-
vm.expectRevert(
122-
abi.encodeWithSelector(
123-
WorkflowRegistry.InvalidOwnershipLink.selector, s_owner, s_validityTimestamp, ownerProof, sig
124-
)
125-
);
126-
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig, WorkflowRegistry.PreUnlinkAction.PAUSE_WORKFLOWS);
127-
}
128-
129-
// whenPreUnlinkeActionIsPAUSE_WORKFLOWSOrREMOVE_WORKFLOWS
130-
// whenValidityTimestampGreaterThanOrEqualToBlockTimestamp
131-
function test_canUnlinkOwner_WhenSignatureRecoversAnAllowedSigner() external whenOwnerIsLinked {
132-
// it should return with no errors
133-
(, bytes memory sig) = _getUnlinkProofSignature(s_owner);
134-
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig, WorkflowRegistry.PreUnlinkAction.REMOVE_WORKFLOWS);
76+
s_registry.canUnlinkOwner(s_owner, s_validityTimestamp, sig);
13577
}
13678
}
Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,14 @@
11
WorkflowRegistry.canUnlinkOwner
2-
├── when pre unlink action is NONE
3-
│ ├── when owner has active workflows
4-
│ │ └── it should revert with CannotUnlinkWithActiveWorkflows
5-
│ └── when owner has no active workflows
6-
│ ├── when valid timestamp is less than block timestamp
7-
│ │ └── it should revert with UnlinkOwnerRequestExpired
8-
│ └── when valid timestamp greater than or equal to block timestamp
9-
│ ├── when owner is not yet linked
10-
│ │ └── it should revert with OwnershipLinkDoesNotExist
11-
│ └── when owner is already linked
12-
│ ├── when the signature does not recover an allowed signer
13-
│ │ └── it should revert with InvalidOwnershipLink
14-
│ └── when the signature recovers an allowed signer
15-
│ └── it should return (no revert)
16-
└── when pre unlinke action is PAUSE_WORKFLOWS or REMOVE_WORKFLOWS
17-
├── when validityTimestamp is less than block timestamp
2+
├── when owner has existing workflows
3+
│ └── it should return successfully (workflows will be auto-deleted)
4+
└── when owner has no existing workflows
5+
├── when valid timestamp is less than block timestamp
186
│ └── it should revert with UnlinkOwnerRequestExpired
19-
└── when validityTimestamp greater than or equal to block timestamp
20-
├── when owner is not linked
21-
│ └── it should revert with OwnershipLinkDoesNotExist(owner)
22-
└── when owner is linked
23-
├── when signature does not recover an allowed signer
7+
└── when valid timestamp greater than or equal to block timestamp
8+
├── when owner is not yet linked
9+
│ └── it should revert with OwnershipLinkDoesNotExist
10+
└── when owner is already linked
11+
├── when the signature does not recover an allowed signer
2412
│ └── it should revert with InvalidOwnershipLink
25-
└── when signature recovers an allowed signer
26-
└── it should return with no errors
13+
└── when the signature recovers an allowed signer
14+
└── it should return (no revert)

contracts/src/v0.8/workflow/dev/v2/test/WorkflowRegistry/WorkflowRegistry.deleteWorkflow.t.sol

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,20 @@ import {WorkflowRegistry} from "../../WorkflowRegistry.sol";
55
import {WorkflowRegistrySetup} from "./WorkflowRegistrySetup.t.sol";
66

77
contract WorkflowRegistrydeleteWorkflow is WorkflowRegistrySetup {
8-
function test_WhenTheWorkflowDoesNotExist() external {
9-
// It should revert with WorkflowDoesNotExist
8+
function test_WhenTheOwnerIsNotLinked() external {
9+
// It should revert with OwnershipLinkDoesNotExist
10+
vm.prank(s_owner);
11+
vm.expectRevert(abi.encodeWithSelector(WorkflowRegistry.OwnershipLinkDoesNotExist.selector, s_owner));
12+
s_registry.deleteWorkflow(s_workflowId);
13+
}
14+
15+
modifier whenTheOwnerIsLinked() {
1016
_linkOwner(s_owner);
17+
_;
18+
}
19+
20+
function test_WhenTheWorkflowDoesNotExist() external whenTheOwnerIsLinked {
21+
// It should revert with WorkflowDoesNotExist
1122
vm.prank(s_owner);
1223
vm.expectRevert(abi.encodeWithSelector(WorkflowRegistry.WorkflowDoesNotExist.selector, s_workflowId));
1324
s_registry.deleteWorkflow(s_workflowId);
@@ -17,9 +28,9 @@ contract WorkflowRegistrydeleteWorkflow is WorkflowRegistrySetup {
1728
_;
1829
}
1930

20-
function test_WhenCallerIsNotTheOwner() external whenTheWorkflowExists {
31+
function test_WhenCallerIsNotTheOwner() external whenTheOwnerIsLinked whenTheWorkflowExists {
2132
// It should revert with CallerIsNotWorkflowOwner
22-
_linkOwner(s_owner);
33+
_linkOwner(s_user);
2334
vm.prank(s_owner);
2435
s_registry.upsertWorkflow(
2536
s_workflowName,
@@ -37,9 +48,8 @@ contract WorkflowRegistrydeleteWorkflow is WorkflowRegistrySetup {
3748
s_registry.deleteWorkflow(s_workflowId);
3849
}
3950

40-
function test_WhenCallerIsTheOwner() external whenTheWorkflowExists {
51+
function test_WhenCallerIsTheOwner() external whenTheOwnerIsLinked whenTheWorkflowExists {
4152
// It should delete the workflow and emit WorkflowDeleted
42-
_linkOwner(s_owner);
4353
vm.startPrank(s_owner);
4454
s_registry.upsertWorkflow(
4555
s_workflowName,
@@ -53,7 +63,7 @@ contract WorkflowRegistrydeleteWorkflow is WorkflowRegistrySetup {
5363
false
5464
);
5565
WorkflowRegistry.WorkflowMetadataView[] memory wrs = s_registry.getWorkflowListByOwner(s_owner, 0, 100);
56-
assertEq(wrs.length, 1, "There should be 0 workflows for the s_owner");
66+
assertEq(wrs.length, 1, "There should be 1 workflow for the s_owner");
5767

5868
s_registry.deleteWorkflow(s_workflowId);
5969
vm.stopPrank();

0 commit comments

Comments
 (0)