packages/contracts-rfq/test/integration/MulticallTarget.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IFastBridge} from "../../contracts/interfaces/IFastBridge.sol";
import {IFastBridgeV2} from "../../contracts/interfaces/IFastBridgeV2.sol";
import {IMulticallTarget} from "../../contracts/interfaces/IMulticallTarget.sol";
import {DisputePeriodNotPassed} from "../../contracts/libs/Errors.sol";
import {MockERC20} from "../mocks/MockERC20.sol";
import {Test} from "forge-std/Test.sol";
// solhint-disable func-name-mixedcase, max-states-count, ordering
abstract contract MulticallTargetIntegrationTest is Test {
address internal constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
uint256 internal constant DEADLINE_PERIOD = 1 days;
uint256 internal constant SKIP_PERIOD = 1 hours;
uint32 internal constant LOCAL_CHAIN_ID = 1337;
uint32 internal constant REMOTE_CHAIN_ID = 7331;
address internal fastBridge;
address internal user = makeAddr("User");
address internal userRemote = makeAddr("User Remote");
address internal relayer = makeAddr("Relayer");
address internal claimTo = makeAddr("Claim To");
MockERC20 internal token;
address internal remoteToken = makeAddr("Remote Token");
IFastBridge.BridgeTransaction internal bridgedTokenTx;
IFastBridge.BridgeTransaction internal provenEthTx;
IFastBridge.BridgeTransaction internal remoteTokenTx;
bytes internal encodedBridgedTokenTx;
bytes internal encodedProvenEthTx;
bytes internal encodedRemoteTokenTx;
bytes32 internal bridgedTokenTxId;
bytes32 internal provenEthTxId;
bytes32 internal remoteTokenTxId;
function setUp() public {
vm.chainId(LOCAL_CHAIN_ID);
fastBridge = deployAndConfigureFastBridge();
token = new MockERC20("Token", 18);
dealTokens(user, 1);
dealTokens(relayer, 1);
createFixtures();
bridge(bridgedTokenTx, user);
bridge(provenEthTx, user);
prove(encodedProvenEthTx);
skip(SKIP_PERIOD);
// Sanity checks
checkStatus(bridgedTokenTxId, IFastBridgeV2.BridgeStatus.REQUESTED);
checkStatus(provenEthTxId, IFastBridgeV2.BridgeStatus.RELAYER_PROVED);
checkStatus(remoteTokenTxId, IFastBridgeV2.BridgeStatus.NULL);
assertEq(token.balanceOf(user), 0);
assertEq(user.balance, 0 ether);
assertEq(token.balanceOf(relayer), 1 ether);
assertEq(relayer.balance, 1 ether);
}
function deployAndConfigureFastBridge() public virtual returns (address);
function getEncodedBridgeTx(IFastBridge.BridgeTransaction memory bridgeTx)
public
view
virtual
returns (bytes memory);
function dealTokens(address to, uint8 amountUnits) public {
uint256 amountWei = amountUnits * 1 ether;
token.mint(to, amountWei);
deal(to, amountWei);
vm.prank(to);
token.approve(address(fastBridge), type(uint256).max);
}
function createFixtures() public {
bridgedTokenTx = IFastBridge.BridgeTransaction({
originChainId: LOCAL_CHAIN_ID,
destChainId: REMOTE_CHAIN_ID,
originSender: user,
destRecipient: userRemote,
originToken: address(token),
destToken: remoteToken,
originAmount: 1 ether,
destAmount: 0.98 ether,
originFeeAmount: 0,
sendChainGas: false,
deadline: block.timestamp + DEADLINE_PERIOD,
nonce: 0
});
provenEthTx = IFastBridge.BridgeTransaction({
originChainId: LOCAL_CHAIN_ID,
destChainId: REMOTE_CHAIN_ID,
originSender: user,
destRecipient: userRemote,
originToken: ETH_ADDRESS,
destToken: ETH_ADDRESS,
originAmount: 1 ether,
destAmount: 0.99 ether,
originFeeAmount: 0,
sendChainGas: false,
deadline: block.timestamp + DEADLINE_PERIOD,
nonce: 1
});
remoteTokenTx = IFastBridge.BridgeTransaction({
originChainId: REMOTE_CHAIN_ID,
destChainId: LOCAL_CHAIN_ID,
originSender: userRemote,
destRecipient: user,
originToken: remoteToken,
destToken: address(token),
originAmount: 1.01 ether,
destAmount: 1 ether,
originFeeAmount: 0,
sendChainGas: false,
deadline: block.timestamp + SKIP_PERIOD,
nonce: 420
});
encodedBridgedTokenTx = getEncodedBridgeTx(bridgedTokenTx);
encodedProvenEthTx = getEncodedBridgeTx(provenEthTx);
encodedRemoteTokenTx = getEncodedBridgeTx(remoteTokenTx);
bridgedTokenTxId = keccak256(encodedBridgedTokenTx);
provenEthTxId = keccak256(encodedProvenEthTx);
remoteTokenTxId = keccak256(encodedRemoteTokenTx);
}
struct TestBridgeTransactionWithMetadata {
IFastBridge.BridgeTransaction transaction;
bytes encodedData;
bytes32 txId;
}
function createMany(uint8 countOfTxnsToCreate)
public
returns (
TestBridgeTransactionWithMetadata[] memory toRelay,
TestBridgeTransactionWithMetadata[] memory toBridgeProveClaimData
)
{
toRelay = new TestBridgeTransactionWithMetadata[](countOfTxnsToCreate);
toBridgeProveClaimData = new TestBridgeTransactionWithMetadata[](countOfTxnsToCreate);
// fund user & relayer
dealTokens(user, countOfTxnsToCreate);
dealTokens(relayer, countOfTxnsToCreate);
for (uint8 i = 0; i < countOfTxnsToCreate; i++) {
IFastBridge.BridgeTransaction memory toRelayTx = IFastBridge.BridgeTransaction({
originChainId: REMOTE_CHAIN_ID,
destChainId: LOCAL_CHAIN_ID,
originSender: userRemote,
destRecipient: user,
originToken: remoteToken,
destToken: address(token),
originAmount: 1.01 ether,
destAmount: 1 ether,
originFeeAmount: 0,
sendChainGas: false,
deadline: block.timestamp + SKIP_PERIOD,
nonce: remoteTokenTx.nonce + i
});
bytes memory encoded = getEncodedBridgeTx(toRelayTx);
bytes32 txId = keccak256(encoded);
toRelay[i] = TestBridgeTransactionWithMetadata({transaction: toRelayTx, encodedData: encoded, txId: txId});
IFastBridge.BridgeTransaction memory toBridgeProveClaimTx = IFastBridge.BridgeTransaction({
originChainId: LOCAL_CHAIN_ID,
destChainId: REMOTE_CHAIN_ID,
originSender: user,
destRecipient: userRemote,
originToken: address(token),
destToken: remoteToken,
originAmount: 1 ether,
destAmount: 0.98 ether,
originFeeAmount: 0,
sendChainGas: false,
deadline: block.timestamp + DEADLINE_PERIOD,
nonce: 2 + i
});
encoded = getEncodedBridgeTx(toBridgeProveClaimTx);
txId = keccak256(encoded);
toBridgeProveClaimData[i] =
// solhint-disable-next-line max-line-length
TestBridgeTransactionWithMetadata({transaction: toBridgeProveClaimTx, encodedData: encoded, txId: txId});
}
}
function bridge(IFastBridge.BridgeTransaction memory bridgeTx, address bridgeUser) public {
uint256 msgValue = bridgeTx.originToken == ETH_ADDRESS ? bridgeTx.originAmount : 0;
vm.prank(bridgeUser);
IFastBridge(fastBridge).bridge{value: msgValue}(
IFastBridge.BridgeParams({
dstChainId: bridgeTx.destChainId,
sender: bridgeTx.originSender,
to: bridgeTx.destRecipient,
originToken: bridgeTx.originToken,
destToken: bridgeTx.destToken,
originAmount: bridgeTx.originAmount,
destAmount: bridgeTx.destAmount,
sendChainGas: bridgeTx.sendChainGas,
deadline: bridgeTx.deadline
})
);
}
function prove(bytes memory encodedBridgeTx) public {
vm.prank(relayer);
IFastBridge(fastBridge).prove(encodedBridgeTx, hex"01");
}
function getData() public view returns (bytes[] memory data) {
data = new bytes[](3);
data[0] = abi.encodeCall(IFastBridge.prove, (encodedBridgedTokenTx, hex"02"));
data[1] = abi.encodeCall(IFastBridge.claim, (encodedProvenEthTx, claimTo));
data[2] = abi.encodeCall(IFastBridge.relay, (encodedRemoteTokenTx));
}
enum TestAction {
bridge,
prove,
claim,
relay
}
function getDataMany(
TestBridgeTransactionWithMetadata[] memory testBridgeTxns,
TestAction action
)
public
view
returns (bytes[] memory data)
{
data = new bytes[](testBridgeTxns.length);
for (uint8 i = 0; i < testBridgeTxns.length; i++) {
if (action == TestAction.bridge) {
data[i] = abi.encodeCall(
IFastBridge.bridge,
(
IFastBridge.BridgeParams({
dstChainId: testBridgeTxns[i].transaction.destChainId,
sender: testBridgeTxns[i].transaction.originSender,
to: testBridgeTxns[i].transaction.destRecipient,
originToken: testBridgeTxns[i].transaction.originToken,
destToken: testBridgeTxns[i].transaction.destToken,
originAmount: testBridgeTxns[i].transaction.originAmount,
destAmount: testBridgeTxns[i].transaction.destAmount,
sendChainGas: testBridgeTxns[i].transaction.sendChainGas,
deadline: testBridgeTxns[i].transaction.deadline
})
)
);
}
if (action == TestAction.prove) {
data[i] = abi.encodeCall(IFastBridge.prove, (testBridgeTxns[i].encodedData, hex"02"));
}
if (action == TestAction.claim) {
data[i] = abi.encodeCall(IFastBridge.claim, (testBridgeTxns[i].encodedData, claimTo));
}
if (action == TestAction.relay) {
data[i] = abi.encodeCall(IFastBridge.relay, (testBridgeTxns[i].encodedData));
}
}
}
function checkStatus(bytes32 txId, IFastBridgeV2.BridgeStatus expected) public view {
IFastBridgeV2.BridgeStatus status = IFastBridgeV2(fastBridge).bridgeStatuses(txId);
assertEq(uint8(status), uint8(expected));
}
function test_sequentialExecution() public {
vm.startPrank(relayer);
IFastBridge(fastBridge).prove(encodedBridgedTokenTx, hex"02");
IFastBridge(fastBridge).claim(encodedProvenEthTx, claimTo);
IFastBridge(fastBridge).relay(encodedRemoteTokenTx);
vm.stopPrank();
checkHappyPath();
}
enum TestExecutionMode {
Sequential_NonMulticall,
Multicall
}
function test_manyActions_sequentialNonMulticall() public {
// send X contiguous bridges, proofs, and claims -- and X non-contiguous relays to the same bridger
manyActions_flow(15, TestExecutionMode.Sequential_NonMulticall);
}
function test_manyActions_multicall() public {
// send X contiguous bridges, proofs, and claims -- and X non-contiguous relays to the same bridger
manyActions_flow(15, TestExecutionMode.Multicall);
}
// will either execute a single batched multicall, or many sequential txns
function manyActions_execute(
TestBridgeTransactionWithMetadata[] memory transactions,
TestAction action,
TestExecutionMode mode,
address pranker
)
internal
{
bytes[] memory data = getDataMany(transactions, action);
if (mode == TestExecutionMode.Multicall) {
vm.prank(pranker);
IMulticallTarget(fastBridge).multicallNoResults({data: data, ignoreReverts: false});
} else {
executeSequentialTransactions(transactions, action, pranker);
}
}
function executeSequentialTransactions(
TestBridgeTransactionWithMetadata[] memory transactions,
TestAction action,
address pranker
)
internal
{
for (uint8 i = 0; i < transactions.length; i++) {
if (action == TestAction.bridge) {
// bridge already pranks as user, no need to prank
bridge(transactions[i].transaction, user);
} else if (action == TestAction.prove) {
vm.prank(pranker);
IFastBridge(fastBridge).prove(transactions[i].encodedData, hex"02");
} else if (action == TestAction.claim) {
vm.prank(pranker);
IFastBridge(fastBridge).claim(transactions[i].encodedData, claimTo);
} else if (action == TestAction.relay) {
vm.prank(pranker);
IFastBridge(fastBridge).relay(transactions[i].encodedData);
}
// Check status after each action
manyActions_checkStatusAfter(transactions[i].txId, action);
}
}
function manyActions_checkStatusAfter(bytes32 txId, TestAction action) internal view {
if (action == TestAction.bridge) {
checkStatus(txId, IFastBridgeV2.BridgeStatus.REQUESTED);
} else if (action == TestAction.prove) {
checkStatus(txId, IFastBridgeV2.BridgeStatus.RELAYER_PROVED);
} else if (action == TestAction.claim) {
checkStatus(txId, IFastBridgeV2.BridgeStatus.RELAYER_CLAIMED);
}
// No status check needed for relay action
}
// Shared flow & asserts regardless of whether executing manyAction test via MC or a sequence of non-MC txns
function manyActions_flow(uint8 countOfTxnsToCreate, TestExecutionMode testExecutionMode) internal {
uint256 relayerOriginalBalWei = token.balanceOf(relayer);
uint256 userOriginalBalWei = token.balanceOf(user);
(
TestBridgeTransactionWithMetadata[] memory toRelay,
TestBridgeTransactionWithMetadata[] memory toBridgeProveClaim
) = createMany(countOfTxnsToCreate);
manyActions_execute(toBridgeProveClaim, TestAction.bridge, testExecutionMode, user);
assertEq(token.balanceOf(user), 0 ether);
manyActions_execute(toBridgeProveClaim, TestAction.prove, testExecutionMode, relayer);
skip(SKIP_PERIOD);
manyActions_execute(toBridgeProveClaim, TestAction.claim, testExecutionMode, relayer);
assertEq(token.balanceOf(relayer), relayerOriginalBalWei + (countOfTxnsToCreate * 1 ether));
manyActions_execute(toRelay, TestAction.relay, testExecutionMode, relayer);
assertEq(token.balanceOf(relayer), relayerOriginalBalWei);
assertEq(token.balanceOf(user), userOriginalBalWei + (countOfTxnsToCreate * 1 ether));
}
// ════════════════════════════════════════════════ NO RESULTS ═════════════════════════════════════════════════════
function checkHappyPath() public view {
// Check statuses
checkStatus(bridgedTokenTxId, IFastBridgeV2.BridgeStatus.RELAYER_PROVED);
checkStatus(provenEthTxId, IFastBridgeV2.BridgeStatus.RELAYER_CLAIMED);
assertTrue(IFastBridgeV2(fastBridge).bridgeRelays(remoteTokenTxId));
// Check balances
assertEq(token.balanceOf(user), 1 ether);
assertEq(user.balance, 0 ether);
assertEq(token.balanceOf(relayer), 0);
assertEq(relayer.balance, 1 ether);
assertEq(claimTo.balance, 1 ether);
}
function checkHappyPathNoClaim() public view {
// Check statuses
checkStatus(bridgedTokenTxId, IFastBridgeV2.BridgeStatus.RELAYER_PROVED);
checkStatus(provenEthTxId, IFastBridgeV2.BridgeStatus.RELAYER_PROVED);
assertTrue(IFastBridgeV2(fastBridge).bridgeRelays(remoteTokenTxId));
// Check balances
assertEq(token.balanceOf(user), 1 ether);
assertEq(user.balance, 0 ether);
assertEq(token.balanceOf(relayer), 0);
assertEq(relayer.balance, 1 ether);
assertEq(claimTo.balance, 0);
}
function test_multicallNoResults_ignoreReverts_success() public {
bytes[] memory data = getData();
vm.prank(relayer);
IMulticallTarget(fastBridge).multicallNoResults({data: data, ignoreReverts: true});
checkHappyPath();
}
function test_multicallNoResults_ignoreReverts_withFailedClaim() public {
// Rewind time to make claim fail
rewind(SKIP_PERIOD - 15 minutes);
bytes[] memory data = getData();
vm.prank(relayer);
IMulticallTarget(fastBridge).multicallNoResults({data: data, ignoreReverts: true});
checkHappyPathNoClaim();
}
function test_multicallNoResults_dontIgnoreReverts_success() public {
bytes[] memory data = getData();
vm.prank(relayer);
IMulticallTarget(fastBridge).multicallNoResults({data: data, ignoreReverts: false});
checkHappyPath();
}
function test_multicallNoResults_dontIgnoreReverts_revert() public {
// Rewind time to make claim fail
rewind(SKIP_PERIOD - 15 minutes);
bytes[] memory data = getData();
vm.expectRevert(DisputePeriodNotPassed.selector);
vm.prank(relayer);
IMulticallTarget(fastBridge).multicallNoResults({data: data, ignoreReverts: false});
}
// ═══════════════════════════════════════════════ WITH RESULTS ════════════════════════════════════════════════════
function assertEq(IMulticallTarget.Result memory result, IMulticallTarget.Result memory expected) public pure {
assertEq(result.success, expected.success);
assertEq(result.returnData, expected.returnData);
}
function checkHappyPathResults(IMulticallTarget.Result[] memory results) public pure {
assertEq(results.length, 3);
assertEq(results[0], IMulticallTarget.Result({success: true, returnData: ""}));
assertEq(results[1], IMulticallTarget.Result({success: true, returnData: ""}));
assertEq(results[2], IMulticallTarget.Result({success: true, returnData: ""}));
}
function checkHappyPathNoClaimResults(IMulticallTarget.Result[] memory results) public pure {
assertEq(results.length, 3);
assertEq(results[0], IMulticallTarget.Result({success: true, returnData: ""}));
assertEq(
results[1],
IMulticallTarget.Result({
success: false,
returnData: abi.encodeWithSelector(DisputePeriodNotPassed.selector)
})
);
assertEq(results[2], IMulticallTarget.Result({success: true, returnData: ""}));
}
function test_multicallWithResults_ignoreReverts_success() public {
bytes[] memory data = getData();
vm.prank(relayer);
IMulticallTarget.Result[] memory results =
IMulticallTarget(fastBridge).multicallWithResults({data: data, ignoreReverts: true});
checkHappyPath();
checkHappyPathResults(results);
}
function test_multicallWithResults_ignoreReverts_withFailedClaim() public {
// Rewind time to make claim fail
rewind(SKIP_PERIOD - 15 minutes);
bytes[] memory data = getData();
vm.prank(relayer);
IMulticallTarget.Result[] memory results =
IMulticallTarget(fastBridge).multicallWithResults({data: data, ignoreReverts: true});
checkHappyPathNoClaim();
checkHappyPathNoClaimResults(results);
}
function test_multicallWithResults_dontIgnoreReverts_success() public {
bytes[] memory data = getData();
vm.prank(relayer);
IMulticallTarget.Result[] memory results =
IMulticallTarget(fastBridge).multicallWithResults({data: data, ignoreReverts: false});
checkHappyPath();
checkHappyPathResults(results);
}
function test_multicallWithResults_dontIgnoreReverts_revert() public {
// Rewind time to make claim fail
rewind(SKIP_PERIOD - 15 minutes);
bytes[] memory data = getData();
vm.expectRevert(DisputePeriodNotPassed.selector);
vm.prank(relayer);
IMulticallTarget(fastBridge).multicallWithResults({data: data, ignoreReverts: false});
}
}