synapsecns/sanguine

View on GitHub
packages/contracts-rfq/test/integration/MulticallTarget.t.sol

Summary

Maintainability
Test Coverage
// 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});
    }
}