Skip to content

Commit

Permalink
feat(contracts-rfq): Multicall in FastBridgeV2 [SLT-369] (#3315)
Browse files Browse the repository at this point in the history
* refactor: dedupe integration test template

* test: V2 multicall integration

* feat: add multicall features to FastBridgeV2

* Parod/fb v2 multicall add test (#3335)

* add manyActions multicall test

* increase manyAction count

* comment cleanup

---------

Co-authored-by: parodime <[email protected]>
  • Loading branch information
ChiTimesChi and parodime authored Oct 28, 2024
1 parent 89ec9d4 commit 1daa5f4
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 26 deletions.
4 changes: 3 additions & 1 deletion packages/contracts-rfq/contracts/FastBridgeV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import {IFastBridgeV2} from "./interfaces/IFastBridgeV2.sol";
import {IFastBridgeV2Errors} from "./interfaces/IFastBridgeV2Errors.sol";
import {IZapRecipient} from "./interfaces/IZapRecipient.sol";

import {MulticallTarget} from "./utils/MulticallTarget.sol";

/// @notice FastBridgeV2 is a contract for bridging tokens across chains.
contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
contract FastBridgeV2 is Admin, MulticallTarget, IFastBridgeV2, IFastBridgeV2Errors {
using BridgeTransactionV2Lib for bytes;
using SafeERC20 for IERC20;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {FastBridgeV2, IFastBridgeV2} from "../../contracts/FastBridgeV2.sol";
import {BridgeTransactionV2Lib} from "../../contracts/libs/BridgeTransactionV2.sol";

import {MulticallTargetIntegrationTest, IFastBridge} from "./MulticallTarget.t.sol";

contract FastBridgeV2MulticallTargetTest is MulticallTargetIntegrationTest {
function deployAndConfigureFastBridge() public override returns (address) {
FastBridgeV2 fastBridge = new FastBridgeV2(address(this));
fastBridge.grantRole(fastBridge.RELAYER_ROLE(), relayer);
return address(fastBridge);
}

function getEncodedBridgeTx(IFastBridge.BridgeTransaction memory bridgeTx)
public
view
override
returns (bytes memory)
{
IFastBridgeV2.BridgeTransactionV2 memory bridgeTxV2;
bridgeTxV2.originChainId = bridgeTx.originChainId;
bridgeTxV2.destChainId = bridgeTx.destChainId;
bridgeTxV2.originSender = bridgeTx.originSender;
bridgeTxV2.destRecipient = bridgeTx.destRecipient;
bridgeTxV2.originToken = bridgeTx.originToken;
bridgeTxV2.destToken = bridgeTx.destToken;
bridgeTxV2.originAmount = bridgeTx.originAmount;
bridgeTxV2.destAmount = bridgeTx.destAmount;
bridgeTxV2.originFeeAmount = bridgeTx.originFeeAmount;
bridgeTxV2.deadline = bridgeTx.deadline;
bridgeTxV2.nonce = bridgeTx.nonce;
bridgeTxV2.exclusivityEndTime = block.timestamp;
return BridgeTransactionV2Lib.encodeV2(bridgeTxV2);
}
}
267 changes: 242 additions & 25 deletions packages/contracts-rfq/test/integration/MulticallTarget.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {MockERC20} from "../MockERC20.sol";

import {Test} from "forge-std/Test.sol";

// solhint-disable func-name-mixedcase, ordering
// 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;
Expand All @@ -33,6 +33,10 @@ abstract contract MulticallTargetIntegrationTest is Test {
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;
Expand All @@ -41,12 +45,12 @@ abstract contract MulticallTargetIntegrationTest is Test {
vm.chainId(LOCAL_CHAIN_ID);
fastBridge = deployAndConfigureFastBridge();
token = new MockERC20("Token", 18);
dealTokens(user);
dealTokens(relayer);
dealTokens(user, 1);
dealTokens(relayer, 1);
createFixtures();
bridge(bridgedTokenTx);
bridge(provenEthTx);
prove(provenEthTx);
bridge(bridgedTokenTx, user);
bridge(provenEthTx, user);
prove(encodedProvenEthTx);
skip(SKIP_PERIOD);
// Sanity checks
checkStatus(bridgedTokenTxId, IFastBridgeV2.BridgeStatus.REQUESTED);
Expand All @@ -66,9 +70,10 @@ abstract contract MulticallTargetIntegrationTest is Test {
virtual
returns (bytes memory);

function dealTokens(address to) public {
token.mint(to, 1 ether);
deal(to, 1 ether);
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);
}
Expand All @@ -91,8 +96,8 @@ abstract contract MulticallTargetIntegrationTest is Test {
provenEthTx = IFastBridge.BridgeTransaction({
originChainId: LOCAL_CHAIN_ID,
destChainId: REMOTE_CHAIN_ID,
originSender: userRemote,
destRecipient: user,
originSender: user,
destRecipient: userRemote,
originToken: ETH_ADDRESS,
destToken: ETH_ADDRESS,
originAmount: 1 ether,
Expand All @@ -116,14 +121,82 @@ abstract contract MulticallTargetIntegrationTest is Test {
deadline: block.timestamp + SKIP_PERIOD,
nonce: 420
});
bridgedTokenTxId = keccak256(getEncodedBridgeTx(bridgedTokenTx));
provenEthTxId = keccak256(getEncodedBridgeTx(provenEthTx));
remoteTokenTxId = keccak256(getEncodedBridgeTx(remoteTokenTx));
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) public {
function bridge(IFastBridge.BridgeTransaction memory bridgeTx, address bridgeUser) public {
uint256 msgValue = bridgeTx.originToken == ETH_ADDRESS ? bridgeTx.originAmount : 0;
vm.prank(user);
vm.prank(bridgeUser);
IFastBridge(fastBridge).bridge{value: msgValue}(
IFastBridge.BridgeParams({
dstChainId: bridgeTx.destChainId,
Expand All @@ -139,17 +212,64 @@ abstract contract MulticallTargetIntegrationTest is Test {
);
}

function prove(IFastBridge.BridgeTransaction memory bridgeTx) public {
bytes memory request = getEncodedBridgeTx(bridgeTx);
function prove(bytes memory encodedBridgeTx) public {
vm.prank(relayer);
IFastBridge(fastBridge).prove(request, hex"01");
IFastBridge(fastBridge).prove(encodedBridgeTx, hex"01");
}

function getData() public view returns (bytes[] memory data) {
data = new bytes[](3);
data[0] = abi.encodeCall(IFastBridge.prove, (getEncodedBridgeTx(bridgedTokenTx), hex"02"));
data[1] = abi.encodeCall(IFastBridge.claim, (getEncodedBridgeTx(provenEthTx), claimTo));
data[2] = abi.encodeCall(IFastBridge.relay, (getEncodedBridgeTx(remoteTokenTx)));
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 {
Expand All @@ -159,13 +279,110 @@ abstract contract MulticallTargetIntegrationTest is Test {

function test_sequentialExecution() public {
vm.startPrank(relayer);
IFastBridge(fastBridge).prove(getEncodedBridgeTx(bridgedTokenTx), hex"02");
IFastBridge(fastBridge).claim(getEncodedBridgeTx(provenEthTx), claimTo);
IFastBridge(fastBridge).relay(getEncodedBridgeTx(remoteTokenTx));
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 {
Expand Down

0 comments on commit 1daa5f4

Please sign in to comment.