diff --git a/src/DelegationContract.sol b/src/DelegationContract.sol index 4bf58d2..bb894fb 100644 --- a/src/DelegationContract.sol +++ b/src/DelegationContract.sol @@ -97,11 +97,18 @@ contract DelegationContract is IDelegationContract, IERC1271, IERC5313, IERC165 /// @inheritdoc IDelegationContract function revokeDelegate() external onlyOwner notTerminated { - address revoked = getDelegate(); + _settle(); + + address pending = _pendingDelegate; + if (pending != address(0)) { + _pendingDelegate = address(0); + _pendingActiveFrom = 0; + emit NominationRevoked(pending); + } + + address revoked = _currentDelegate; _currentDelegate = address(0); - _pendingDelegate = address(0); - _pendingActiveFrom = 0; emit DelegateRevoked(revoked); } diff --git a/src/interfaces/IDelegationContract.sol b/src/interfaces/IDelegationContract.sol index 674a54f..a765c27 100644 --- a/src/interfaces/IDelegationContract.sol +++ b/src/interfaces/IDelegationContract.sol @@ -56,6 +56,8 @@ interface IDelegationContract { /// @notice Immediately remove the current and pending delegate. /// Only callable by owner. + /// Emits NominationRevoked if a not-yet-effective nomination is + /// dropped, in addition to DelegateRevoked. /// Reverts if the contract is terminated. function revokeDelegate() external; diff --git a/test/unit/DelegationContract.t.sol b/test/unit/DelegationContract.t.sol index fa14aae..ed0400c 100644 --- a/test/unit/DelegationContract.t.sol +++ b/test/unit/DelegationContract.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.35; -import { Test } from "forge-std/Test.sol"; +import { Test, Vm } from "forge-std/Test.sol"; import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; @@ -377,6 +377,51 @@ contract DelegationContractRevokeDelegateTest is DelegationContractBaseTestWithD assertEq(activeFrom, 0); } + function test_revokeDelegate_emitsNominationRevokedForPending() public { + address newDelegate = nextAddress("NEW_DELEGATE"); + + vm.prank(owner); + delegationContract.nominateDelegate(newDelegate); + + vm.expectEmit(); + emit IDelegationContract.NominationRevoked(newDelegate); + vm.expectEmit(); + emit IDelegationContract.DelegateRevoked(delegate); + + vm.prank(owner); + delegationContract.revokeDelegate(); + } + + function test_revokeDelegate_noNominationRevokedWhenNoPending() public { + vm.recordLogs(); + + vm.prank(owner); + delegationContract.revokeDelegate(); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertEq(logs.length, 1, "Only DelegateRevoked should be emitted"); + assertEq(logs[0].topics[0], IDelegationContract.DelegateRevoked.selector); + } + + function test_revokeDelegate_noNominationRevokedWhenPendingMatured() public { + address newDelegate = nextAddress("NEW_DELEGATE"); + + vm.prank(owner); + delegationContract.nominateDelegate(newDelegate); + + vm.warp(block.timestamp + cooldown); + + vm.recordLogs(); + + vm.prank(owner); + delegationContract.revokeDelegate(); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertEq(logs.length, 1, "Matured pending is the effective delegate: only DelegateRevoked"); + assertEq(logs[0].topics[0], IDelegationContract.DelegateRevoked.selector); + assertEq(address(uint160(uint256(logs[0].topics[1]))), newDelegate); + } + function test_revokeDelegate_noOpWhenNoDelegate() public { vm.prank(owner); delegationContract.revokeDelegate();