Skip to content

Commit 5398766

Browse files
committed
feat: add SubscriptionTestFork contract for local testing of Subscription functionality on fork chains
1 parent 9cea972 commit 5398766

File tree

1 file changed

+250
-0
lines changed

1 file changed

+250
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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

Comments
 (0)