|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity 0.8.26; |
| 3 | + |
| 4 | +import {Test, console} from "forge-std/Test.sol"; |
| 5 | +import {CCIPLocalSimulatorFork, Register} from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol"; |
| 6 | +import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol"; |
| 7 | +import {BurnMintERC677Helper, IERC20} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol"; |
| 8 | +import {Receiver} from "src/Receiver.sol"; |
| 9 | +import {Sender} from "src/Sender.sol"; |
| 10 | +import {MockCheckBalance} from "test/mocks/MockCheckBalance.sol"; |
| 11 | +import {Subscription} from "src/Subscription.sol"; |
| 12 | +import {Relayer} from "src/Relayer.sol"; |
| 13 | +import {ReceiverSignedMessage} from "src/library/ReceiverSignedMessage.sol"; |
| 14 | +import {Vm} from "forge-std/Vm.sol"; |
| 15 | + |
| 16 | +/// @title SubscriptionTestFork |
| 17 | +/// @author Luo Yingjie |
| 18 | +/// @notice This is the fork local test of the Subscription contract |
| 19 | +/// @dev The contracts on the source chain are Sender, CheckBalance(Mock one), Subscription |
| 20 | +/// @dev The contracts on the destination chain are Receiver and Relayer |
| 21 | +contract SubscriptionTestFork is Test { |
| 22 | + /*////////////////////////////////////////////////////////////// |
| 23 | + STATE VARIABLES |
| 24 | + //////////////////////////////////////////////////////////////*/ |
| 25 | + |
| 26 | + CCIPLocalSimulatorFork public ccipLocalSimulatorFork; |
| 27 | + uint256 public sourceFork; |
| 28 | + uint256 public destinationFork; |
| 29 | + |
| 30 | + IRouterClient public sourceRouter; |
| 31 | + IERC20 public sourceLinkToken; |
| 32 | + BurnMintERC677Helper public sourceCCIPBnM; |
| 33 | + |
| 34 | + IRouterClient public destinationRouter; |
| 35 | + IERC20 public destinationLinkToken; |
| 36 | + BurnMintERC677Helper public destinationCCIPBnM; |
| 37 | + uint64 public destinationChainSelector; |
| 38 | + |
| 39 | + Receiver public receiver; |
| 40 | + Sender public sender; |
| 41 | + MockCheckBalance public checkBalance; |
| 42 | + Subscription public subscription; |
| 43 | + Relayer public relayer; |
| 44 | + |
| 45 | + address user; |
| 46 | + uint256 userPrivateKey; |
| 47 | + |
| 48 | + uint256 constant AMOUNT_DESTINATION_CCIPBNM = 1e18; |
| 49 | + // The amount of LINK required to make a request |
| 50 | + uint256 constant AMOUNT_LINK_REQUEST = 20 ether; |
| 51 | + uint256 constant AMOUNT_CCIPBNM_TO_TRANSFER = 1e16; |
| 52 | + |
| 53 | + /*////////////////////////////////////////////////////////////// |
| 54 | + SET UP FUNCTION |
| 55 | + //////////////////////////////////////////////////////////////*/ |
| 56 | + |
| 57 | + function setUp() public { |
| 58 | + (user, userPrivateKey) = makeAddrAndKey("user"); |
| 59 | + |
| 60 | + string memory SOURCE_RPC_URL = vm.envString("AMOY_RPC_URL"); |
| 61 | + string memory DESTINATION_RPC_URL = vm.envString("SEPOLIA_RPC_URL"); |
| 62 | + sourceFork = vm.createFork(SOURCE_RPC_URL); |
| 63 | + destinationFork = vm.createSelectFork(DESTINATION_RPC_URL); |
| 64 | + |
| 65 | + ccipLocalSimulatorFork = new CCIPLocalSimulatorFork(); |
| 66 | + vm.makePersistent(address(ccipLocalSimulatorFork)); |
| 67 | + |
| 68 | + // First deploy the destination chain contracts => Receiver |
| 69 | + vm.selectFork(destinationFork); |
| 70 | + Register.NetworkDetails |
| 71 | + memory destinationNetworkDetails = ccipLocalSimulatorFork |
| 72 | + .getNetworkDetails(block.chainid); |
| 73 | + destinationRouter = IRouterClient( |
| 74 | + destinationNetworkDetails.routerAddress |
| 75 | + ); |
| 76 | + destinationLinkToken = IERC20(destinationNetworkDetails.linkAddress); |
| 77 | + destinationCCIPBnM = BurnMintERC677Helper( |
| 78 | + destinationNetworkDetails.ccipBnMAddress |
| 79 | + ); |
| 80 | + destinationChainSelector = destinationNetworkDetails.chainSelector; |
| 81 | + |
| 82 | + // deal some CCIPBnM to the user |
| 83 | + deal(address(destinationCCIPBnM), user, AMOUNT_DESTINATION_CCIPBNM); |
| 84 | + |
| 85 | + vm.prank(user); |
| 86 | + receiver = new Receiver( |
| 87 | + address(destinationRouter), |
| 88 | + address(destinationLinkToken) |
| 89 | + ); |
| 90 | + |
| 91 | + // Then deploy the source chain contracts => Sender, CheckBalance, Subscription |
| 92 | + vm.selectFork(sourceFork); |
| 93 | + Register.NetworkDetails |
| 94 | + memory sourceNetworkDetails = ccipLocalSimulatorFork |
| 95 | + .getNetworkDetails(block.chainid); |
| 96 | + sourceRouter = IRouterClient(sourceNetworkDetails.routerAddress); |
| 97 | + sourceLinkToken = IERC20(sourceNetworkDetails.linkAddress); |
| 98 | + sourceCCIPBnM = BurnMintERC677Helper( |
| 99 | + sourceNetworkDetails.ccipBnMAddress |
| 100 | + ); |
| 101 | + |
| 102 | + vm.startPrank(user); |
| 103 | + sender = new Sender(address(sourceRouter), address(sourceLinkToken)); |
| 104 | + checkBalance = new MockCheckBalance(); |
| 105 | + |
| 106 | + uint64[] memory destinationChainSelectors = new uint64[](1); |
| 107 | + destinationChainSelectors[0] = destinationChainSelector; |
| 108 | + subscription = new Subscription( |
| 109 | + destinationChainSelectors, |
| 110 | + address(sourceCCIPBnM), |
| 111 | + address(destinationCCIPBnM), |
| 112 | + address(sourceRouter), |
| 113 | + address(checkBalance), |
| 114 | + address(sender), |
| 115 | + address(receiver) |
| 116 | + ); |
| 117 | + // transfer the ownership to subscription contract first as the set up |
| 118 | + checkBalance.setSubscriptionAsOwner(address(subscription)); |
| 119 | + sender.setSubscriptionAsOwner(address(subscription)); |
| 120 | + vm.stopPrank(); |
| 121 | + |
| 122 | + // And lastly deploy the Relay contract on destination chain |
| 123 | + vm.selectFork(destinationFork); |
| 124 | + vm.prank(user); |
| 125 | + relayer = new Relayer( |
| 126 | + address(destinationRouter), |
| 127 | + address(destinationLinkToken), |
| 128 | + address(subscription) |
| 129 | + ); |
| 130 | + } |
| 131 | + |
| 132 | + /*////////////////////////////////////////////////////////////// |
| 133 | + TESTS |
| 134 | + //////////////////////////////////////////////////////////////*/ |
| 135 | + |
| 136 | + function testPaySubscriptionFeeforOptionalChainSuccessUpdateTheMappingFork() |
| 137 | + public |
| 138 | + { |
| 139 | + // First, we need to request some LINK from the faucet |
| 140 | + vm.selectFork(sourceFork); |
| 141 | + ccipLocalSimulatorFork.requestLinkFromFaucet( |
| 142 | + address(sender), |
| 143 | + AMOUNT_LINK_REQUEST |
| 144 | + ); |
| 145 | + |
| 146 | + vm.selectFork(destinationFork); |
| 147 | + ccipLocalSimulatorFork.requestLinkFromFaucet( |
| 148 | + address(receiver), |
| 149 | + AMOUNT_LINK_REQUEST |
| 150 | + ); |
| 151 | + ccipLocalSimulatorFork.requestLinkFromFaucet( |
| 152 | + address(relayer), |
| 153 | + AMOUNT_LINK_REQUEST |
| 154 | + ); |
| 155 | + |
| 156 | + // approve the Receiver to spend the user's CCIPBnM |
| 157 | + vm.prank(user); |
| 158 | + destinationCCIPBnM.approve( |
| 159 | + address(receiver), |
| 160 | + AMOUNT_CCIPBNM_TO_TRANSFER |
| 161 | + ); |
| 162 | + |
| 163 | + // Sign the message with the user's private key |
| 164 | + ReceiverSignedMessage.SignedMessage memory signedMessage = ReceiverSignedMessage |
| 165 | + .SignedMessage({ |
| 166 | + chainSelector: destinationChainSelector, |
| 167 | + user: user, |
| 168 | + token: address(destinationCCIPBnM), |
| 169 | + amount: AMOUNT_CCIPBNM_TO_TRANSFER, |
| 170 | + transferContract: address(receiver), |
| 171 | + router: address(destinationRouter), |
| 172 | + // For test just set the nonce to 0 |
| 173 | + nonce: 0, |
| 174 | + // Set the expiry to 1 day later from now |
| 175 | + expiry: block.timestamp + 1 days |
| 176 | + }); |
| 177 | + |
| 178 | + bytes32 digest = receiver.getMessageHash(signedMessage); |
| 179 | + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); |
| 180 | + |
| 181 | + bytes memory encodedSignedMessage = abi.encode( |
| 182 | + user, |
| 183 | + signedMessage, |
| 184 | + v, |
| 185 | + r, |
| 186 | + s |
| 187 | + ); |
| 188 | + |
| 189 | + // call the paySubscriptionFeeforOptionalChain function |
| 190 | + // it will first call the checkBalance contract to check the balance |
| 191 | + // then it will call the send function in Sender to route the message to the Receiver |
| 192 | + vm.selectFork(sourceFork); |
| 193 | + vm.startPrank(user); |
| 194 | + vm.recordLogs(); |
| 195 | + subscription.paySubscriptionFeeforOptionalChain( |
| 196 | + address(destinationCCIPBnM), |
| 197 | + destinationChainSelector, |
| 198 | + encodedSignedMessage |
| 199 | + ); |
| 200 | + |
| 201 | + // switch the chain and route the message |
| 202 | + ccipLocalSimulatorFork.switchChainAndRouteMessage(destinationFork); |
| 203 | + |
| 204 | + Vm.Log[] memory entries = vm.getRecordedLogs(); |
| 205 | + console.log(entries.length); |
| 206 | + // The event MessageReceived is the 16th event |
| 207 | + uint64 optionalChain = uint64(uint256(entries[15].topics[1])); |
| 208 | + address paymentTokenForOptionalChain = bytes32ToAddress( |
| 209 | + entries[15].topics[2] |
| 210 | + ); |
| 211 | + address signer = bytes32ToAddress(entries[15].topics[3]); |
| 212 | + |
| 213 | + assertEq(optionalChain, destinationChainSelector); |
| 214 | + assertEq(paymentTokenForOptionalChain, address(destinationCCIPBnM)); |
| 215 | + assertEq(signer, user); |
| 216 | + vm.stopPrank(); |
| 217 | + |
| 218 | + // 2. The Relayer listen for MessageReceived event and send the message to the Subscription contract |
| 219 | + vm.selectFork(destinationFork); |
| 220 | + bytes memory performData = abi.encode( |
| 221 | + optionalChain, |
| 222 | + paymentTokenForOptionalChain, |
| 223 | + signer |
| 224 | + ); |
| 225 | + relayer.performUpkeep(performData); |
| 226 | + |
| 227 | + // switch the chain and route the message |
| 228 | + ccipLocalSimulatorFork.switchChainAndRouteMessage(sourceFork); |
| 229 | + |
| 230 | + // 3. The Subscription contract receives the message and update the s_subscriberToSubscription mapping |
| 231 | + vm.selectFork(sourceFork); |
| 232 | + assertEq( |
| 233 | + subscription.getSubscriberToSubscription(user).optionalChain, |
| 234 | + destinationChainSelector |
| 235 | + ); |
| 236 | + assertEq( |
| 237 | + subscription |
| 238 | + .getSubscriberToSubscription(user) |
| 239 | + .paymentTokenForOptionalChain, |
| 240 | + address(destinationCCIPBnM) |
| 241 | + ); |
| 242 | + } |
| 243 | + |
| 244 | + /*////////////////////////////////////////////////////////////// |
| 245 | + HELPER FUNCTIONS |
| 246 | + //////////////////////////////////////////////////////////////*/ |
| 247 | + function bytes32ToAddress(bytes32 _address) public pure returns (address) { |
| 248 | + return address(uint160(uint256(_address))); |
| 249 | + } |
| 250 | +} |
0 commit comments