Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rfq-relayer): add multicall support for prove / claim [SLT-355] #3261

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
4 changes: 3 additions & 1 deletion packages/contracts-rfq/contracts/FastBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {UniversalTokenLib} from "./libs/UniversalToken.sol";
import {Admin} from "./Admin.sol";
import {IFastBridge} from "./interfaces/IFastBridge.sol";

contract FastBridge is IFastBridge, Admin {
import {MulticallTarget} from "./utils/MulticallTarget.sol";

contract FastBridge is IFastBridge, MulticallTarget, Admin {
using SafeERC20 for IERC20;
using UniversalTokenLib for address;

Expand Down
283 changes: 283 additions & 0 deletions packages/contracts-rfq/test/FastBridge.MulticallTarget.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import {FastBridge, IFastBridge} from "../contracts/FastBridge.sol";
import {IMulticallTarget} from "../contracts/interfaces/IMulticallTarget.sol";
import {DisputePeriodNotPassed} from "../contracts/libs/Errors.sol";

import {MockERC20} from "./MockERC20.sol";

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

// solhint-disable func-name-mixedcase, ordering
contract FastBridgeMulticallTargetTest 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;

FastBridge 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;

bytes32 internal bridgedTokenTxId;
bytes32 internal provenEthTxId;
bytes32 internal remoteTokenTxId;

function setUp() public {
vm.chainId(LOCAL_CHAIN_ID);
fastBridge = new FastBridge(address(this));
fastBridge.grantRole(fastBridge.RELAYER_ROLE(), relayer);
token = new MockERC20("Token", 18);
dealTokens(user);
dealTokens(relayer);
createFixtures();
bridge(bridgedTokenTx);
bridge(provenEthTx);
prove(provenEthTx);
skip(SKIP_PERIOD);
// Sanity checks
checkStatus(bridgedTokenTxId, FastBridge.BridgeStatus.REQUESTED);
checkStatus(provenEthTxId, FastBridge.BridgeStatus.RELAYER_PROVED);
checkStatus(remoteTokenTxId, FastBridge.BridgeStatus.NULL);
assertEq(token.balanceOf(user), 0);
assertEq(user.balance, 0 ether);
assertEq(token.balanceOf(relayer), 1 ether);
assertEq(relayer.balance, 1 ether);
}

function dealTokens(address to) public {
token.mint(to, 1 ether);
deal(to, 1 ether);
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: userRemote,
destRecipient: user,
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
});

bridgedTokenTxId = keccak256(abi.encode(bridgedTokenTx));
provenEthTxId = keccak256(abi.encode(provenEthTx));
remoteTokenTxId = keccak256(abi.encode(remoteTokenTx));
}

function bridge(IFastBridge.BridgeTransaction memory bridgeTx) public {
uint256 msgValue = bridgeTx.originToken == ETH_ADDRESS ? bridgeTx.originAmount : 0;
vm.prank(user);
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(IFastBridge.BridgeTransaction memory bridgeTx) public {
bytes memory request = abi.encode(bridgeTx);
vm.prank(relayer);
fastBridge.prove(request, hex"01");
}

function getData() public view returns (bytes[] memory data) {
data = new bytes[](3);
data[0] = abi.encodeCall(fastBridge.prove, (abi.encode(bridgedTokenTx), hex"02"));
data[1] = abi.encodeCall(fastBridge.claim, (abi.encode(provenEthTx), claimTo));
data[2] = abi.encodeCall(fastBridge.relay, (abi.encode(remoteTokenTx)));
}

function checkStatus(bytes32 txId, FastBridge.BridgeStatus expected) public view {
FastBridge.BridgeStatus status = fastBridge.bridgeStatuses(txId);
assertEq(uint8(status), uint8(expected));
}

function test_sequentialExecution() public {
vm.startPrank(relayer);
fastBridge.prove(abi.encode(bridgedTokenTx), hex"02");
fastBridge.claim(abi.encode(provenEthTx), claimTo);
fastBridge.relay(abi.encode(remoteTokenTx));
vm.stopPrank();
checkHappyPath();
}

// ════════════════════════════════════════════════ NO RESULTS ═════════════════════════════════════════════════════

function checkHappyPath() public view {
// Check statuses
checkStatus(bridgedTokenTxId, FastBridge.BridgeStatus.RELAYER_PROVED);
checkStatus(provenEthTxId, FastBridge.BridgeStatus.RELAYER_CLAIMED);
assertTrue(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, FastBridge.BridgeStatus.RELAYER_PROVED);
checkStatus(provenEthTxId, FastBridge.BridgeStatus.RELAYER_PROVED);
assertTrue(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);
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);
fastBridge.multicallNoResults({data: data, ignoreReverts: true});
checkHappyPathNoClaim();
}

function test_multicallNoResults_dontIgnoreReverts_success() public {
bytes[] memory data = getData();
vm.prank(relayer);
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);
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 = 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 = 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 = 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);
fastBridge.multicallWithResults({data: data, ignoreReverts: false});
}
}
2 changes: 1 addition & 1 deletion services/rfq/contracts/fastbridge/bridgestatus_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions services/rfq/contracts/fastbridge/events.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package fastbridge
package fastbridgemulti

import (
"bytes"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"strings"
)

// TODO: consider not exporting to avoid accidental mutation.
Expand Down
2 changes: 1 addition & 1 deletion services/rfq/contracts/fastbridge/eventtype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion services/rfq/contracts/fastbridge/export_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package fastbridge
package fastbridgemulti

// GetAllBridgeStatuses exports all bridge statuses for testing.
func GetAllBridgeStatuses() []BridgeStatus {
Expand Down

This file was deleted.

Loading
Loading