@@ -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);
0 commit comments