// SPDX-License-Identifier: MIT pragma solidity 0.8.19; import {MockLinkToken} from "../../functions/tests/v1_X/testhelpers/MockLinkToken.sol"; import {MockV3Aggregator} from "../../shared/mocks/MockV3Aggregator.sol"; import {SubscriptionAPI} from "../dev/SubscriptionAPI.sol"; import {VRFV2PlusWrapper} from "../dev/VRFV2PlusWrapper.sol"; import {ExposedVRFCoordinatorV2_5} from "../dev/testhelpers/ExposedVRFCoordinatorV2_5.sol"; import {VRFCoordinatorV2Plus_V2Example} from "../dev/testhelpers/VRFCoordinatorV2Plus_V2Example.sol"; import {VRFV2PlusWrapperConsumerExample} from "../dev/testhelpers/VRFV2PlusWrapperConsumerExample.sol"; import {BaseTest} from "./BaseTest.t.sol"; contract VRFV2PlusWrapper_MigrationTest is BaseTest { address internal constant LINK_WHALE = 0xD883a6A1C22fC4AbFE938a5aDF9B2Cc31b1BF18B; uint256 internal constant DEFAULT_NATIVE_FUNDING = 7 ether; // 7 ETH uint256 internal constant DEFAULT_LINK_FUNDING = 10 ether; // 10 ETH bytes32 private vrfKeyHash = hex"9f2353bde94264dbc3d554a94cceba2d7d2b4fdce4304d3e09a1fea9fbeb1528"; uint32 private wrapperGasOverhead = 10_000; uint32 private coordinatorGasOverheadNative = 20_000; uint32 private coordinatorGasOverheadLink = 40_000; uint256 private s_wrapperSubscriptionId; ExposedVRFCoordinatorV2_5 private s_testCoordinator; MockLinkToken private s_linkToken; MockV3Aggregator private s_linkNativeFeed; VRFV2PlusWrapper private s_wrapper; VRFV2PlusWrapperConsumerExample private s_consumer; VRFCoordinatorV2Plus_V2Example private s_newCoordinator; event CoordinatorRegistered(address coordinatorAddress); event MigrationCompleted(address newCoordinator, uint256 subId); event WrapperRequestMade(uint256 indexed requestId, uint256 paid); function setUp() public override { BaseTest.setUp(); // Fund our users. vm.roll(1); vm.deal(LINK_WHALE, 10_000 ether); changePrank(LINK_WHALE); // Deploy link token and link/native feed. s_linkToken = new MockLinkToken(); s_linkNativeFeed = new MockV3Aggregator(18, 500_000_000_000_000_000); // .5 ETH (good for testing) // Deploy coordinator. s_testCoordinator = new ExposedVRFCoordinatorV2_5(address(0)); // Create subscription for all future wrapper contracts. s_wrapperSubscriptionId = s_testCoordinator.createSubscription(); // Deploy wrapper. s_wrapper = new VRFV2PlusWrapper( address(s_linkToken), address(s_linkNativeFeed), address(s_testCoordinator), uint256(s_wrapperSubscriptionId) ); // Add wrapper as a consumer to the wrapper's subscription. s_testCoordinator.addConsumer(uint256(s_wrapperSubscriptionId), address(s_wrapper)); // Deploy consumer. s_consumer = new VRFV2PlusWrapperConsumerExample(address(s_wrapper)); // Configure the coordinator. s_testCoordinator.setLINKAndLINKNativeFeed(address(s_linkToken), address(s_linkNativeFeed)); setConfigCoordinator(); setConfigWrapper(); s_testCoordinator.s_config(); // Data structures for Migrateable Wrapper s_newCoordinator = new VRFCoordinatorV2Plus_V2Example(address(0), address(s_testCoordinator)); vm.expectEmit( false, // no first indexed topic false, // no second indexed topic false, // no third indexed topic true // check data (target coordinator address) ); address newCoordinatorAddr = address(s_newCoordinator); emit CoordinatorRegistered(newCoordinatorAddr); s_testCoordinator.registerMigratableCoordinator(newCoordinatorAddr); assertTrue(s_testCoordinator.isTargetRegisteredExternal(newCoordinatorAddr)); } function setConfigCoordinator() internal { s_testCoordinator.setConfig( 0, // minRequestConfirmations 2_500_000, // maxGasLimit 1, // stalenessSeconds 50_000, // gasAfterPaymentCalculation 50_000_000_000_000_000, // fallbackWeiPerUnitLink 500_000, // fulfillmentFlatFeeNativePPM 100_000, // fulfillmentFlatFeeLinkDiscountPPM 15, // nativePremiumPercentage 10 // linkPremiumPercentage ); } function setConfigWrapper() internal { s_wrapper.setConfig( wrapperGasOverhead, // wrapper gas overhead coordinatorGasOverheadNative, // coordinator gas overhead native coordinatorGasOverheadLink, // coordinator gas overhead link 0, // coordinator gas overhead per word 0, // native premium percentage, 0, // link premium percentage vrfKeyHash, // keyHash 10, // max number of words, 1, // stalenessSeconds 50_000_000_000_000_000, // fallbackWeiPerUnitLink 0, // fulfillmentFlatFeeNativePPM 0 // fulfillmentFlatFeeLinkDiscountPPM ); ( , , , , uint32 _wrapperGasOverhead, uint32 _coordinatorGasOverheadNative, uint32 _coordinatorGasOverheadLink, uint16 _coordinatorGasOverheadPerWord, uint8 _coordinatorNativePremiumPercentage, uint8 _coordinatorLinkPremiumPercentage, bytes32 _keyHash, uint8 _maxNumWords ) = s_wrapper.getConfig(); assertEq(_wrapperGasOverhead, wrapperGasOverhead); assertEq(_coordinatorGasOverheadNative, coordinatorGasOverheadNative); assertEq(_coordinatorGasOverheadLink, coordinatorGasOverheadLink); assertEq(0, _coordinatorGasOverheadPerWord); assertEq(0, _coordinatorNativePremiumPercentage); assertEq(0, _coordinatorLinkPremiumPercentage); assertEq(vrfKeyHash, _keyHash); assertEq(10, _maxNumWords); } event RandomWordsRequested( bytes32 indexed keyHash, uint256 requestId, uint256 preSeed, uint256 indexed subId, uint16 minimumRequestConfirmations, uint32 callbackGasLimit, uint32 numWords, bytes extraArgs, address indexed sender ); // IVRFV2PlusWrapper events event Withdrawn(address indexed to, uint256 amount); event NativeWithdrawn(address indexed to, uint256 amount); // IVRFMigratableConsumerV2Plus events event CoordinatorSet(address vrfCoordinator); function testMigrateWrapperLINKPayment() public { s_linkToken.transfer(address(s_consumer), DEFAULT_LINK_FUNDING); assertEq(uint256(s_wrapperSubscriptionId), uint256(s_wrapper.SUBSCRIPTION_ID())); address oldCoordinatorAddr = address(s_testCoordinator); assertEq(address(oldCoordinatorAddr), address(s_wrapper.s_vrfCoordinator())); // Fund subscription with native and LINK payment to check // if funds are transferred to new subscription after call // migration to new coordinator s_linkToken.transferAndCall(oldCoordinatorAddr, DEFAULT_LINK_FUNDING, abi.encode(s_wrapperSubscriptionId)); s_testCoordinator.fundSubscriptionWithNative{value: DEFAULT_NATIVE_FUNDING}(s_wrapperSubscriptionId); // subscription exists in V1 coordinator before migration (uint96 balance, uint96 nativeBalance, uint64 reqCount, address owner, address[] memory consumers) = s_testCoordinator.getSubscription(s_wrapperSubscriptionId); assertEq(reqCount, 0); assertEq(balance, DEFAULT_LINK_FUNDING); assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); assertEq(owner, address(LINK_WHALE)); assertEq(consumers.length, 1); assertEq(consumers[0], address(s_wrapper)); // Update wrapper to point to the new coordinator vm.expectEmit( false, // no first indexed field false, // no second indexed field false, // no third indexed field true // check data fields ); address newCoordinatorAddr = address(s_newCoordinator); emit MigrationCompleted(newCoordinatorAddr, s_wrapperSubscriptionId); // old coordinator has to migrate wrapper's subscription to the new coordinator s_testCoordinator.migrate(s_wrapperSubscriptionId, newCoordinatorAddr); assertEq(address(newCoordinatorAddr), address(s_wrapper.s_vrfCoordinator())); // subscription no longer exists in v1 coordinator after migration vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); s_testCoordinator.getSubscription(s_wrapperSubscriptionId); assertEq(s_testCoordinator.s_totalBalance(), 0); assertEq(s_testCoordinator.s_totalNativeBalance(), 0); assertEq(s_linkToken.balanceOf(oldCoordinatorAddr), 0); assertEq(oldCoordinatorAddr.balance, 0); // subscription exists in v2 coordinator (balance, nativeBalance, reqCount, owner, consumers) = s_newCoordinator.getSubscription(s_wrapperSubscriptionId); assertEq(owner, address(LINK_WHALE)); assertEq(consumers.length, 1); assertEq(consumers[0], address(s_wrapper)); assertEq(reqCount, 0); assertEq(balance, DEFAULT_LINK_FUNDING); assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); assertEq(s_newCoordinator.s_totalLinkBalance(), DEFAULT_LINK_FUNDING); assertEq(s_newCoordinator.s_totalNativeBalance(), DEFAULT_NATIVE_FUNDING); assertEq(s_linkToken.balanceOf(newCoordinatorAddr), DEFAULT_LINK_FUNDING); assertEq(newCoordinatorAddr.balance, DEFAULT_NATIVE_FUNDING); // calling migrate again on V1 coordinator should fail vm.expectRevert(); s_testCoordinator.migrate(s_wrapperSubscriptionId, newCoordinatorAddr); // Request randomness from wrapper. uint32 callbackGasLimit = 1_000_000; uint256 wrapperCost = s_wrapper.calculateRequestPrice(callbackGasLimit, 0); vm.expectEmit(true, true, true, true); emit WrapperRequestMade(1, wrapperCost); uint256 requestId = s_consumer.makeRequest(callbackGasLimit, 0, 1); assertEq(requestId, 1); (uint256 paid, bool fulfilled, bool native) = s_consumer.s_requests(requestId); uint32 expectedPaid = (callbackGasLimit + wrapperGasOverhead + coordinatorGasOverheadLink) * 2; uint256 wrapperCostEstimate = s_wrapper.estimateRequestPrice(callbackGasLimit, 0, tx.gasprice); uint256 wrapperCostCalculation = s_wrapper.calculateRequestPrice(callbackGasLimit, 0); assertEq(paid, expectedPaid); // 1_030_000 * 2 for link/native ratio assertEq(uint256(paid), wrapperCostEstimate); assertEq(wrapperCostEstimate, wrapperCostCalculation); assertEq(fulfilled, false); assertEq(native, false); assertEq(s_linkToken.balanceOf(address(s_consumer)), DEFAULT_LINK_FUNDING - expectedPaid); (, uint256 gasLimit,) = s_wrapper.s_callbacks(requestId); assertEq(gasLimit, callbackGasLimit); vm.stopPrank(); vm.startPrank(newCoordinatorAddr); uint256[] memory words = new uint256[](1); words[0] = 123; s_wrapper.rawFulfillRandomWords(requestId, words); (, bool nowFulfilled, uint256[] memory storedWords) = s_consumer.getRequestStatus(requestId); assertEq(nowFulfilled, true); assertEq(storedWords[0], 123); vm.stopPrank(); /// Withdraw funds from wrapper. vm.startPrank(LINK_WHALE); uint256 priorWhaleBalance = s_linkToken.balanceOf(LINK_WHALE); vm.expectEmit(true, false, false, true, address(s_wrapper)); emit Withdrawn(LINK_WHALE, paid); s_wrapper.withdraw(LINK_WHALE); assertEq(s_linkToken.balanceOf(LINK_WHALE), priorWhaleBalance + paid); assertEq(s_linkToken.balanceOf(address(s_wrapper)), 0); vm.stopPrank(); } function testMigrateWrapperNativePayment() public { vm.deal(address(s_consumer), DEFAULT_NATIVE_FUNDING); assertEq(uint256(s_wrapperSubscriptionId), uint256(s_wrapper.SUBSCRIPTION_ID())); address oldCoordinatorAddr = address(s_testCoordinator); assertEq(address(oldCoordinatorAddr), address(s_wrapper.s_vrfCoordinator())); // Fund subscription with native and LINK payment to check // if funds are transferred to new subscription after call // migration to new coordinator s_linkToken.transferAndCall(oldCoordinatorAddr, DEFAULT_LINK_FUNDING, abi.encode(s_wrapperSubscriptionId)); s_testCoordinator.fundSubscriptionWithNative{value: DEFAULT_NATIVE_FUNDING}(s_wrapperSubscriptionId); // subscription exists in V1 coordinator before migration (uint96 balance, uint96 nativeBalance, uint64 reqCount, address owner, address[] memory consumers) = s_testCoordinator.getSubscription(s_wrapperSubscriptionId); assertEq(reqCount, 0); assertEq(balance, DEFAULT_LINK_FUNDING); assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); assertEq(owner, address(LINK_WHALE)); assertEq(consumers.length, 1); assertEq(consumers[0], address(s_wrapper)); // Update wrapper to point to the new coordinator vm.expectEmit( false, // no first indexed field false, // no second indexed field false, // no third indexed field true // check data fields ); address newCoordinatorAddr = address(s_newCoordinator); emit MigrationCompleted(newCoordinatorAddr, s_wrapperSubscriptionId); // old coordinator has to migrate wrapper's subscription to the new coordinator s_testCoordinator.migrate(s_wrapperSubscriptionId, newCoordinatorAddr); assertEq(address(newCoordinatorAddr), address(s_wrapper.s_vrfCoordinator())); // subscription no longer exists in v1 coordinator after migration vm.expectRevert(SubscriptionAPI.InvalidSubscription.selector); s_testCoordinator.getSubscription(s_wrapperSubscriptionId); assertEq(s_testCoordinator.s_totalBalance(), 0); assertEq(s_testCoordinator.s_totalNativeBalance(), 0); assertEq(s_linkToken.balanceOf(oldCoordinatorAddr), 0); assertEq(oldCoordinatorAddr.balance, 0); // subscription exists in v2 coordinator (balance, nativeBalance, reqCount, owner, consumers) = s_newCoordinator.getSubscription(s_wrapperSubscriptionId); assertEq(owner, address(LINK_WHALE)); assertEq(consumers.length, 1); assertEq(consumers[0], address(s_wrapper)); assertEq(reqCount, 0); assertEq(balance, DEFAULT_LINK_FUNDING); assertEq(nativeBalance, DEFAULT_NATIVE_FUNDING); assertEq(s_newCoordinator.s_totalLinkBalance(), DEFAULT_LINK_FUNDING); assertEq(s_newCoordinator.s_totalNativeBalance(), DEFAULT_NATIVE_FUNDING); assertEq(s_linkToken.balanceOf(newCoordinatorAddr), DEFAULT_LINK_FUNDING); assertEq(newCoordinatorAddr.balance, DEFAULT_NATIVE_FUNDING); // calling migrate again on V1 coordinator should fail vm.expectRevert(); s_testCoordinator.migrate(s_wrapperSubscriptionId, newCoordinatorAddr); // Request randomness from wrapper. uint32 callbackGasLimit = 1_000_000; uint32 numWords = 1; vm.expectEmit(true, true, true, true); uint256 wrapperCost = s_wrapper.calculateRequestPriceNative(callbackGasLimit, numWords); emit WrapperRequestMade(1, wrapperCost); uint256 requestId = s_consumer.makeRequestNative(callbackGasLimit, 0, numWords); assertEq(requestId, 1); (uint256 paid, bool fulfilled, bool native) = s_consumer.s_requests(requestId); uint32 expectedPaid = callbackGasLimit + wrapperGasOverhead + coordinatorGasOverheadNative; uint256 wrapperNativeCostEstimate = s_wrapper.estimateRequestPriceNative(callbackGasLimit, 0, tx.gasprice); uint256 wrapperCostCalculation = s_wrapper.calculateRequestPriceNative(callbackGasLimit, 0); assertEq(paid, expectedPaid); assertEq(uint256(paid), wrapperNativeCostEstimate); assertEq(wrapperNativeCostEstimate, wrapperCostCalculation); assertEq(fulfilled, false); assertEq(native, true); assertEq(address(s_consumer).balance, DEFAULT_NATIVE_FUNDING - expectedPaid); (, uint256 gasLimit,) = s_wrapper.s_callbacks(requestId); assertEq(gasLimit, callbackGasLimit); vm.stopPrank(); vm.startPrank(newCoordinatorAddr); uint256[] memory words = new uint256[](1); words[0] = 123; s_wrapper.rawFulfillRandomWords(requestId, words); (, bool nowFulfilled, uint256[] memory storedWords) = s_consumer.getRequestStatus(requestId); assertEq(nowFulfilled, true); assertEq(storedWords[0], 123); vm.stopPrank(); // Withdraw funds from wrapper. vm.startPrank(LINK_WHALE); uint256 priorWhaleBalance = LINK_WHALE.balance; vm.expectEmit(true, false, false, true, address(s_wrapper)); emit NativeWithdrawn(LINK_WHALE, paid); s_wrapper.withdrawNative(LINK_WHALE); assertEq(LINK_WHALE.balance, priorWhaleBalance + paid); assertEq(address(s_wrapper).balance, 0); vm.stopPrank(); } }