Skip to content

Commit de847b5

Browse files
committed
Merge branch 'main' into fix/randomness-sender-bug-fix
2 parents 7f84baa + b846977 commit de847b5

5 files changed

Lines changed: 288 additions & 14 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"publishConfig": {
66
"access": "public"
77
},
8-
"version": "0.0.7",
8+
"version": "0.0.9",
99
"directories": {
1010
"lib": "lib",
1111
"test": "test"

src/chainlink_compatible/ChainlinkVRFCoordinatorV2_5Adapter.sol

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ contract ChainlinkVRFCoordinatorV2_5Adapter is
3636

3737
event WrapperFulfillmentFailed(uint256 indexed requestId, address indexed consumer);
3838
event WrapperGasOverheadUpdated(uint32 newWrapperGasOverhead);
39+
event SubscriptionOwnerTransferRequested(uint256 indexed subId, address from, address to);
40+
event SubscriptionOwnerTransferred(uint256 indexed subId, address from, address to);
3941

4042
/// @notice The contract responsible for providing randomness.
4143
/// @dev This is an immutable reference set at deployment.
@@ -50,6 +52,7 @@ contract ChainlinkVRFCoordinatorV2_5Adapter is
5052
uint256 public lastRequestId;
5153

5254
mapping(uint256 => address) private subscriptionOwners;
55+
mapping(uint256 => address) private pendingSubscriptionOwners;
5356
mapping(uint256 => Callback) /* requestID */ /* callback */ public s_callbacks;
5457

5558
/// @notice Ensures that only the designated randomness sender can call the function.
@@ -241,22 +244,57 @@ contract ChainlinkVRFCoordinatorV2_5Adapter is
241244
/// @param subId - ID of the subscription
242245
/// @param to - Where to send the remaining native tokens to, e.g., Ether.
243246
function cancelSubscription(uint256 subId, address to) external override onlySubscriptionOwner(subId) {
244-
randomnessSender.cancelSubscription(subId, to);
247+
// Get the subscription balance before cancellation
248+
(uint96 nativeBalance,,,) = randomnessSender.getSubscription(subId);
249+
250+
// Cancel the subscription - funds will come to this contract since it's the actual owner
251+
randomnessSender.cancelSubscription(subId, address(this));
252+
253+
// Forward the funds to the intended recipient
254+
if (nativeBalance > 0) {
255+
_transferNative(to, nativeBalance);
256+
}
257+
258+
// Clear the subscription owner mapping since subscription is cancelled
259+
delete subscriptionOwners[subId];
260+
delete pendingSubscriptionOwners[subId];
245261
}
246262

247263
/// @notice Accept subscription owner transfer.
248264
/// @param subId - ID of the subscription
249-
/// @dev will revert if original owner of subId has
250-
/// not requested that msg.sender become the new owner.
251-
function acceptSubscriptionOwnerTransfer(uint256 subId) external override onlySubscriptionOwner(subId) {
252-
randomnessSender.acceptSubscriptionOwnerTransfer(subId);
265+
/// @dev will accept ownership transfer if msg.sender is the pending owner.
266+
function acceptSubscriptionOwnerTransfer(uint256 subId) external override {
267+
address pendingOwner = pendingSubscriptionOwners[subId];
268+
require(pendingOwner != address(0), "No pending ownership transfer");
269+
require(msg.sender == pendingOwner, "Caller is not the pending owner");
270+
271+
address previousOwner = subscriptionOwners[subId];
272+
273+
// Update the subscription owner mapping to the new owner
274+
subscriptionOwners[subId] = pendingOwner;
275+
276+
// Clear the pending transfer
277+
delete pendingSubscriptionOwners[subId];
278+
279+
emit SubscriptionOwnerTransferred(subId, previousOwner, pendingOwner);
253280
}
254281

255282
/// @notice Request subscription owner transfer.
283+
/// @notice The onlySubscriptionOwner modifier is used to ensure proper access control is implemented for this function to restrict usage to authorized accounts only.
256284
/// @param subId - ID of the subscription
257285
/// @param newOwner - proposed new owner of the subscription
258-
function requestSubscriptionOwnerTransfer(uint256 subId, address newOwner) external override {
259-
randomnessSender.requestSubscriptionOwnerTransfer(subId, newOwner);
286+
function requestSubscriptionOwnerTransfer(uint256 subId, address newOwner)
287+
external
288+
override
289+
onlySubscriptionOwner(subId)
290+
{
291+
require(newOwner != address(0), "New owner cannot be zero address");
292+
require(newOwner != subscriptionOwners[subId], "New owner is the same as current owner");
293+
294+
// Only update the pending owner in the adapter, don't call the underlying contract
295+
pendingSubscriptionOwners[subId] = newOwner;
296+
297+
emit SubscriptionOwnerTransferRequested(subId, subscriptionOwners[subId], newOwner);
260298
}
261299

262300
/// @notice Create a VRF subscription.
@@ -316,4 +354,28 @@ contract ChainlinkVRFCoordinatorV2_5Adapter is
316354
function fundSubscriptionWithNative(uint256 subId) external payable override {
317355
randomnessSender.fundSubscriptionWithNative{value: msg.value}(subId);
318356
}
357+
358+
/// @notice Get the pending owner for a subscription
359+
/// @param subId - ID of the subscription
360+
/// @return The address of the pending owner, or zero address if no transfer pending
361+
function getPendingSubscriptionOwner(uint256 subId) external view returns (address) {
362+
return pendingSubscriptionOwners[subId];
363+
}
364+
365+
/// @notice Allows contract to receive native tokens when subscription is cancelled
366+
receive() external payable {
367+
// This function allows the contract to receive ETH when cancelSubscription
368+
// sends the subscription balance to the adapter contract
369+
}
370+
371+
/// @notice Internal function to transfer native tokens with proper validation
372+
/// @param to Address to send tokens to
373+
/// @param amount Amount of native tokens to transfer
374+
function _transferNative(address to, uint256 amount) internal {
375+
require(to != address(0), "Invalid transfer address");
376+
require(amount > 0, "Amount must be greater than 0");
377+
378+
(bool success,) = to.call{value: amount}("");
379+
require(success, "Transfer failed");
380+
}
319381
}

src/signature-requests/SignatureSender.sol

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {AccessControlEnumerableUpgradeable} from
66
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
77
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
88
import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
9-
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
9+
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
1010
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
1111

1212
import {TypesLib} from "../libraries/TypesLib.sol";
@@ -30,7 +30,7 @@ contract SignatureSender is
3030
Initializable,
3131
UUPSUpgradeable,
3232
AccessControlEnumerableUpgradeable,
33-
ReentrancyGuardUpgradeable
33+
ReentrancyGuard
3434
{
3535
using BytesLib for bytes;
3636
using EnumerableSet for EnumerableSet.UintSet;
@@ -93,7 +93,6 @@ contract SignatureSender is
9393
function initialize(address owner, address _signatureSchemeAddressProvider) public initializer {
9494
__UUPSUpgradeable_init();
9595
__AccessControlEnumerable_init();
96-
__ReentrancyGuard_init();
9796

9897
require(_grantRole(ADMIN_ROLE, owner), "Grant role failed");
9998
require(_grantRole(DEFAULT_ADMIN_ROLE, owner), "Grant role reverts");
@@ -165,13 +164,14 @@ contract SignatureSender is
165164

166165
require(sigScheme.verifySignature(request.messageHash, signature), "Signature verification failed");
167166

167+
// Apply CEI pattern: Update state before external interaction
168+
requests[requestID].isFulfilled = true;
169+
unfulfilledRequestIds.remove(requestID);
170+
168171
(bool success,) = request.callback.call(
169172
abi.encodeWithSelector(ISignatureReceiver.receiveSignature.selector, requestID, signature)
170173
);
171174

172-
requests[requestID].isFulfilled = true;
173-
unfulfilledRequestIds.remove(requestID);
174-
175175
if (!success) {
176176
erroredRequestIds.add(requestID);
177177
emit SignatureCallbackFailed(requestID);

test/forge/ChainlinkVRFV2_5Integration_Subscription.t.sol

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,212 @@ contract ChainlinkVRFV2_5Integration_SubscriptionTest is Deployment {
132132

133133
console.log("received randomness", consumer.getRandomWords(requestId)[0]);
134134
}
135+
136+
/// @notice Tests successful subscription ownership transfer flow
137+
function test_subscriptionOwnershipTransfer_successful() public {
138+
// Deploy wrapper adapter
139+
ChainlinkVRFCoordinatorV2_5Adapter wrapper =
140+
new ChainlinkVRFCoordinatorV2_5Adapter(admin, address(randomnessSender));
141+
142+
// Create subscription as alice
143+
vm.prank(alice);
144+
uint256 subId = wrapper.createSubscription();
145+
146+
// Verify initial state
147+
assertTrue(wrapper.getPendingSubscriptionOwner(subId) == address(0), "No pending owner initially");
148+
149+
// Request ownership transfer to bob
150+
vm.prank(alice);
151+
vm.expectEmit(true, false, false, true);
152+
emit ChainlinkVRFCoordinatorV2_5Adapter.SubscriptionOwnerTransferRequested(subId, alice, bob);
153+
wrapper.requestSubscriptionOwnerTransfer(subId, bob);
154+
155+
// Verify pending state
156+
assertTrue(wrapper.getPendingSubscriptionOwner(subId) == bob, "Bob should be pending owner");
157+
158+
// Accept ownership transfer as bob
159+
vm.prank(bob);
160+
vm.expectEmit(true, false, false, true);
161+
emit ChainlinkVRFCoordinatorV2_5Adapter.SubscriptionOwnerTransferred(subId, alice, bob);
162+
wrapper.acceptSubscriptionOwnerTransfer(subId);
163+
164+
// Verify ownership transfer completed
165+
assertTrue(wrapper.getPendingSubscriptionOwner(subId) == address(0), "No pending owner after transfer");
166+
167+
// Verify bob can now manage the subscription
168+
vm.prank(bob);
169+
wrapper.addConsumer(subId, makeAddr("new-consumer"));
170+
171+
// Verify alice can no longer manage the subscription
172+
vm.prank(alice);
173+
vm.expectRevert("Caller is not subscription owner");
174+
wrapper.addConsumer(subId, makeAddr("another-consumer"));
175+
}
176+
177+
/// @notice Tests that non-owner cannot request ownership transfer
178+
function test_subscriptionOwnershipTransfer_onlyOwnerCanRequest() public {
179+
ChainlinkVRFCoordinatorV2_5Adapter wrapper =
180+
new ChainlinkVRFCoordinatorV2_5Adapter(admin, address(randomnessSender));
181+
182+
vm.prank(alice);
183+
uint256 subId = wrapper.createSubscription();
184+
185+
// Try to request transfer as non-owner (bob)
186+
vm.prank(bob);
187+
vm.expectRevert("Caller is not subscription owner");
188+
wrapper.requestSubscriptionOwnerTransfer(subId, bob);
189+
}
190+
191+
/// @notice Tests that only pending owner can accept transfer
192+
function test_subscriptionOwnershipTransfer_onlyPendingOwnerCanAccept() public {
193+
ChainlinkVRFCoordinatorV2_5Adapter wrapper =
194+
new ChainlinkVRFCoordinatorV2_5Adapter(admin, address(randomnessSender));
195+
196+
vm.prank(alice);
197+
uint256 subId = wrapper.createSubscription();
198+
199+
// Request transfer to bob
200+
vm.prank(alice);
201+
wrapper.requestSubscriptionOwnerTransfer(subId, bob);
202+
203+
// Try to accept as charlie (not the pending owner)
204+
vm.prank(charlie);
205+
vm.expectRevert("Caller is not the pending owner");
206+
wrapper.acceptSubscriptionOwnerTransfer(subId);
207+
208+
// Try to accept as alice (current owner, not pending owner)
209+
vm.prank(alice);
210+
vm.expectRevert("Caller is not the pending owner");
211+
wrapper.acceptSubscriptionOwnerTransfer(subId);
212+
}
213+
214+
/// @notice Tests that accepting transfer without pending transfer fails
215+
function test_subscriptionOwnershipTransfer_noPendingTransfer() public {
216+
ChainlinkVRFCoordinatorV2_5Adapter wrapper =
217+
new ChainlinkVRFCoordinatorV2_5Adapter(admin, address(randomnessSender));
218+
219+
vm.prank(alice);
220+
uint256 subId = wrapper.createSubscription();
221+
222+
// Try to accept transfer without requesting it first
223+
vm.prank(bob);
224+
vm.expectRevert("No pending ownership transfer");
225+
wrapper.acceptSubscriptionOwnerTransfer(subId);
226+
}
227+
228+
/// @notice Tests that requesting transfer to zero address fails
229+
function test_subscriptionOwnershipTransfer_zeroAddressRevert() public {
230+
ChainlinkVRFCoordinatorV2_5Adapter wrapper =
231+
new ChainlinkVRFCoordinatorV2_5Adapter(admin, address(randomnessSender));
232+
233+
vm.prank(alice);
234+
uint256 subId = wrapper.createSubscription();
235+
236+
// Try to request transfer to zero address
237+
vm.prank(alice);
238+
vm.expectRevert("New owner cannot be zero address");
239+
wrapper.requestSubscriptionOwnerTransfer(subId, address(0));
240+
}
241+
242+
/// @notice Tests that requesting transfer to same owner fails
243+
function test_subscriptionOwnershipTransfer_sameOwnerRevert() public {
244+
ChainlinkVRFCoordinatorV2_5Adapter wrapper =
245+
new ChainlinkVRFCoordinatorV2_5Adapter(admin, address(randomnessSender));
246+
247+
vm.prank(alice);
248+
uint256 subId = wrapper.createSubscription();
249+
250+
// Try to request transfer to same owner
251+
vm.prank(alice);
252+
vm.expectRevert("New owner is the same as current owner");
253+
wrapper.requestSubscriptionOwnerTransfer(subId, alice);
254+
}
255+
256+
/// @notice Tests that pending transfer is cleared when subscription is cancelled
257+
function test_subscriptionOwnershipTransfer_clearedOnCancellation() public {
258+
ChainlinkVRFCoordinatorV2_5Adapter wrapper =
259+
new ChainlinkVRFCoordinatorV2_5Adapter(admin, address(randomnessSender));
260+
261+
vm.prank(alice);
262+
uint256 subId = wrapper.createSubscription();
263+
264+
// Fund subscription
265+
vm.prank(alice);
266+
wrapper.fundSubscriptionWithNative{value: 1 ether}(subId);
267+
268+
// Request ownership transfer
269+
vm.prank(alice);
270+
wrapper.requestSubscriptionOwnerTransfer(subId, bob);
271+
272+
// Verify pending transfer exists
273+
assertTrue(wrapper.getPendingSubscriptionOwner(subId) == bob, "Bob should be pending owner");
274+
275+
// Cancel subscription
276+
uint96 preBalance = uint96(alice.balance);
277+
(, uint96 subBalance,,,) = wrapper.getSubscription(subId);
278+
vm.prank(alice);
279+
wrapper.cancelSubscription(subId, alice);
280+
281+
// Check that alice received the subscription balance
282+
assertTrue(uint96(alice.balance) >= preBalance + subBalance, "Alice should receive the subscription balance");
283+
284+
// Verify pending transfer is cleared
285+
assertTrue(wrapper.getPendingSubscriptionOwner(subId) == address(0), "Pending owner should be cleared");
286+
}
287+
288+
/// @notice Tests that adapter remains owner in underlying SubscriptionAPI after transfer
289+
function test_subscriptionOwnershipTransfer_adapterRemainsUnderlyingOwner() public {
290+
ChainlinkVRFCoordinatorV2_5Adapter wrapper =
291+
new ChainlinkVRFCoordinatorV2_5Adapter(admin, address(randomnessSender));
292+
293+
vm.prank(alice);
294+
uint256 subId = wrapper.createSubscription();
295+
296+
// Transfer ownership to bob
297+
vm.prank(alice);
298+
wrapper.requestSubscriptionOwnerTransfer(subId, bob);
299+
300+
vm.prank(bob);
301+
wrapper.acceptSubscriptionOwnerTransfer(subId);
302+
303+
// Verify that the wrapper is still the owner in the underlying SubscriptionAPI
304+
(,, address underlyingOwner,) = randomnessSender.getSubscription(subId);
305+
assertTrue(underlyingOwner == address(wrapper), "Wrapper should remain the owner in underlying SubscriptionAPI");
306+
307+
// Verify that bob can manage the subscription through the wrapper
308+
vm.prank(bob);
309+
wrapper.addConsumer(subId, makeAddr("new-consumer"));
310+
311+
// Verify management functions work (no revert)
312+
vm.prank(bob);
313+
wrapper.removeConsumer(subId, makeAddr("new-consumer"));
314+
}
315+
316+
/// @notice Tests that multiple ownership transfers can be requested (overwriting pending)
317+
function test_subscriptionOwnershipTransfer_overwritePending() public {
318+
ChainlinkVRFCoordinatorV2_5Adapter wrapper =
319+
new ChainlinkVRFCoordinatorV2_5Adapter(admin, address(randomnessSender));
320+
321+
vm.prank(alice);
322+
uint256 subId = wrapper.createSubscription();
323+
324+
// Request transfer to bob
325+
vm.prank(alice);
326+
wrapper.requestSubscriptionOwnerTransfer(subId, bob);
327+
assertTrue(wrapper.getPendingSubscriptionOwner(subId) == bob, "Bob should be pending owner");
328+
329+
// Request transfer to charlie (should overwrite)
330+
vm.prank(alice);
331+
wrapper.requestSubscriptionOwnerTransfer(subId, charlie);
332+
assertTrue(wrapper.getPendingSubscriptionOwner(subId) == charlie, "Charlie should be pending owner");
333+
334+
// Bob can no longer accept (since pending was overwritten)
335+
vm.prank(bob);
336+
vm.expectRevert("Caller is not the pending owner");
337+
wrapper.acceptSubscriptionOwnerTransfer(subId);
338+
339+
// Charlie can accept
340+
vm.prank(charlie);
341+
wrapper.acceptSubscriptionOwnerTransfer(subId);
342+
}
135343
}

0 commit comments

Comments
 (0)