From 743e859e3274ed449c6410441bd664ff2aaf9740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CF=87=C2=B2?= <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:51:14 +0000 Subject: [PATCH 01/12] feat(contracts-rfq): Token Zap [SLT-389] (#3352) * feat: scaffold `ZapData` library * test: define expected behavior for ZapDataV1 * feat: encoding, validation * feat: decoding * feat: scaffold `TokenZap` * test: add coverage for TokenZap * feat: expose encoding/decoding * feat: implement `zap` * fix: noAmount test, slight refactor * test: scenarios where target contract reverts * test: extra/missing funds scenarios * refactor: TokenZap -> TokenZapV1 * test: FastBridgeV2 + TokenZapV1 integration * fix: should revert when zero target in encoding * chore: docs * added target != addr 0 assumptions * added one more target != addr 0 assumption * refactor: relax ZapData pragma * docs: improve grammar in ZapDataV1 comments * test: adapt to #3382 * docs: NatSpec, fixing errors --------- Co-authored-by: parodime --- .../contracts/libs/ZapDataV1.sol | 119 ++++++++++ .../contracts/zaps/TokenZapV1.sol | 113 +++++++++ .../test/harnesses/ZapDataV1Harness.sol | 34 +++ .../FastBridgeV2.TokenZapV1.Dst.t.sol | 119 ++++++++++ .../FastBridgeV2.TokenZapV1.Src.t.sol | 111 +++++++++ .../test/integration/TokenZapV1.t.sol | 160 +++++++++++++ .../contracts-rfq/test/libs/ZapDataV1.t.sol | 151 ++++++++++++ .../test/mocks/VaultManyArguments.sol | 37 +++ .../contracts-rfq/test/mocks/VaultMock.sol | 24 ++ .../contracts-rfq/test/zaps/TokenZapV1.t.sol | 221 ++++++++++++++++++ 10 files changed, 1089 insertions(+) create mode 100644 packages/contracts-rfq/contracts/libs/ZapDataV1.sol create mode 100644 packages/contracts-rfq/contracts/zaps/TokenZapV1.sol create mode 100644 packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol create mode 100644 packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol create mode 100644 packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol create mode 100644 packages/contracts-rfq/test/integration/TokenZapV1.t.sol create mode 100644 packages/contracts-rfq/test/libs/ZapDataV1.t.sol create mode 100644 packages/contracts-rfq/test/mocks/VaultManyArguments.sol create mode 100644 packages/contracts-rfq/test/mocks/VaultMock.sol create mode 100644 packages/contracts-rfq/test/zaps/TokenZapV1.t.sol diff --git a/packages/contracts-rfq/contracts/libs/ZapDataV1.sol b/packages/contracts-rfq/contracts/libs/ZapDataV1.sol new file mode 100644 index 0000000000..0b7c13a9d1 --- /dev/null +++ b/packages/contracts-rfq/contracts/libs/ZapDataV1.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +// solhint-disable no-inline-assembly +library ZapDataV1 { + /// @notice Version of the Zap Data struct. + uint16 internal constant VERSION = 1; + + /// @notice Value that indicates the amount is not present in the target function's payload. + uint16 internal constant AMOUNT_NOT_PRESENT = 0xFFFF; + + // Offsets of the fields in the packed ZapData struct + // uint16 version [000 .. 002) + // uint16 amountPosition [002 .. 004) + // address target [004 .. 024) + // bytes payload [024 .. ***) + + // forgefmt: disable-start + uint256 private constant OFFSET_AMOUNT_POSITION = 2; + uint256 private constant OFFSET_TARGET = 4; + uint256 private constant OFFSET_PAYLOAD = 24; + // forgefmt: disable-end + + error ZapDataV1__InvalidEncoding(); + error ZapDataV1__TargetZeroAddress(); + error ZapDataV1__UnsupportedVersion(uint16 version); + + /// @notice Validates that encodedZapData is a tightly packed encoded payload for ZapData struct. + /// @dev Checks that all the required fields are present and the version is correct. + function validateV1(bytes calldata encodedZapData) internal pure { + // Check the minimum length: must at least include all static fields. + if (encodedZapData.length < OFFSET_PAYLOAD) revert ZapDataV1__InvalidEncoding(); + // Once we validated the length, we can be sure that the version field is present. + uint16 version_ = version(encodedZapData); + if (version_ != VERSION) revert ZapDataV1__UnsupportedVersion(version_); + } + + /// @notice Encodes the ZapData struct by tightly packing the fields. + /// Note: we don't know the exact amount of tokens that will be used for the Zap at the time of encoding, + /// so we provide the reference index where the token amount is encoded within `payload_`. This allows us to + /// hot-swap the token amount in the payload, when the Zap is performed. + /// @dev `abi.decode` will not work as a result of the tightly packed fields. Use `decodeZapData` instead. + /// @param amountPosition_ Position (start index) where the token amount is encoded within `payload_`. + /// This will usually be `4 + 32 * n`, where `n` is the position of the token amount in + /// the list of parameters of the target function (starting from 0). + /// Or `AMOUNT_NOT_PRESENT` if the token amount is not encoded within `payload_`. + /// @param target_ Address of the target contract. + /// @param payload_ ABI-encoded calldata to be used for the `target_` contract call. + /// If the target function has the token amount as an argument, any placeholder amount value + /// can be used for the original ABI encoding of `payload_`. The placeholder amount will + /// be replaced with the actual amount, when the Zap Data is decoded. + function encodeV1( + uint16 amountPosition_, + address target_, + bytes memory payload_ + ) + internal + pure + returns (bytes memory encodedZapData) + { + if (target_ == address(0)) revert ZapDataV1__TargetZeroAddress(); + // Amount is encoded in [amountPosition_ .. amountPosition_ + 32), which should be within the payload. + if (amountPosition_ != AMOUNT_NOT_PRESENT && (uint256(amountPosition_) + 32 > payload_.length)) { + revert ZapDataV1__InvalidEncoding(); + } + return abi.encodePacked(VERSION, amountPosition_, target_, payload_); + } + + /// @notice Extracts the version from the encoded Zap Data. + function version(bytes calldata encodedZapData) internal pure returns (uint16 version_) { + // Load 32 bytes from the start and shift it 240 bits to the right to get the highest 16 bits. + assembly { + version_ := shr(240, calldataload(encodedZapData.offset)) + } + } + + /// @notice Extracts the target address from the encoded Zap Data. + function target(bytes calldata encodedZapData) internal pure returns (address target_) { + // Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits. + assembly { + target_ := shr(96, calldataload(add(encodedZapData.offset, OFFSET_TARGET))) + } + } + + /// @notice Extracts the payload from the encoded Zap Data. Replaces the token amount with the provided value, + /// if it was present in the original data (if amountPosition is not AMOUNT_NOT_PRESENT). + /// @dev This payload will be used as a calldata for the target contract. + function payload(bytes calldata encodedZapData, uint256 amount) internal pure returns (bytes memory) { + // The original payload is located at encodedZapData[OFFSET_PAYLOAD:]. + uint16 amountPosition = _amountPosition(encodedZapData); + // If the amount was not present in the original payload, return the payload as is. + if (amountPosition == AMOUNT_NOT_PRESENT) { + return encodedZapData[OFFSET_PAYLOAD:]; + } + // Calculate the start and end indexes of the amount in ZapData from its position within the payload. + // Note: we use inclusive start and exclusive end indexes for easier slicing of the ZapData. + uint256 amountStartIndexIncl = OFFSET_PAYLOAD + amountPosition; + uint256 amountEndIndexExcl = amountStartIndexIncl + 32; + // Check that the amount is within the ZapData. + if (amountEndIndexExcl > encodedZapData.length) revert ZapDataV1__InvalidEncoding(); + // Otherwise we need to replace the amount in the payload with the provided value. + return abi.encodePacked( + // Copy the original payload up to the amount + encodedZapData[OFFSET_PAYLOAD:amountStartIndexIncl], + // Replace the originally encoded amount with the provided value + amount, + // Copy the rest of the payload after the amount + encodedZapData[amountEndIndexExcl:] + ); + } + + /// @notice Extracts the amount position from the encoded Zap Data. + function _amountPosition(bytes calldata encodedZapData) private pure returns (uint16 amountPosition) { + // Load 32 bytes from the offset and shift it 240 bits to the right to get the highest 16 bits. + assembly { + amountPosition := shr(240, calldataload(add(encodedZapData.offset, OFFSET_AMOUNT_POSITION))) + } + } +} diff --git a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol new file mode 100644 index 0000000000..a17d3d9bb1 --- /dev/null +++ b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {IZapRecipient} from "../interfaces/IZapRecipient.sol"; +import {ZapDataV1} from "../libs/ZapDataV1.sol"; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @title TokenZapV1 +/// @notice Facilitates atomic token operations known as "Zaps," allowing to execute predefined actions +/// on behalf of users like deposits or swaps. Supports ERC20 tokens and native gas tokens (e.g., ETH). +/// @dev Tokens must be pre-transferred to the contract for execution, with native tokens sent as msg.value. +/// This contract is stateless and does not hold assets between Zaps; leftover tokens can be claimed by anyone. +/// Ensure Zaps fully utilize tokens or revert to prevent fund loss. +contract TokenZapV1 is IZapRecipient { + using SafeERC20 for IERC20; + using ZapDataV1 for bytes; + + address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + error TokenZapV1__AmountIncorrect(); + error TokenZapV1__PayloadLengthAboveMax(); + + /// @notice Performs a Zap action using the specified token and amount. This amount must be previously + /// transferred to this contract (or supplied as msg.value if the token is native gas token). + /// @dev The provided ZapData contains the target address and calldata for the Zap action, and must be + /// encoded using the encodeZapData function. + /// @param token Address of the token to be used for the Zap action. + /// @param amount Amount of the token to be used for the Zap action. + /// Must match msg.value if the token is a native gas token. + /// @param zapData Encoded Zap Data containing the target address and calldata for the Zap action. + /// @return selector Selector of this function to signal the caller about the success of the Zap action. + function zap(address token, uint256 amount, bytes calldata zapData) external payable returns (bytes4) { + // Validate the ZapData format and extract the target address. + zapData.validateV1(); + address target = zapData.target(); + if (token == NATIVE_GAS_TOKEN) { + // For native gas token (e.g., ETH), verify msg.value matches the expected amount. + // No approval needed since native token doesn't use allowances. + if (msg.value != amount) revert TokenZapV1__AmountIncorrect(); + } else { + // For ERC20 tokens, grant unlimited approval to the target if the current allowance is insufficient. + // This is safe since the contract doesn't custody tokens between zaps. + if (IERC20(token).allowance(address(this), target) < amount) { + IERC20(token).forceApprove(target, type(uint256).max); + } + // Note: balance check is omitted as the target contract will revert if there are insufficient funds. + } + // Construct the payload for the target contract call with the Zap action. + // The payload is modified to replace the placeholder amount with the actual amount. + bytes memory payload = zapData.payload(amount); + // Perform the Zap action, forwarding full msg.value to the target contract. + // Note: this will bubble up any revert from the target contract. + Address.functionCallWithValue({target: target, data: payload, value: msg.value}); + // Return function selector to indicate successful execution + return this.zap.selector; + } + + /// @notice Encodes the ZapData for a Zap action. + /// @dev At the time of encoding, we don't know the exact amount of tokens that will be used for the Zap, + /// as we don't have a quote for performing a Zap. Therefore, a placeholder value for the amount must be used + /// when ABI-encoding the payload. A reference index where the actual amount is encoded within the payload + /// must be provided in order to replace the placeholder with the actual amount when the Zap is performed. + /// @param target Address of the target contract. + /// @param payload ABI-encoded calldata to be used for the `target` contract call. + /// If the target function has the token amount as an argument, any placeholder amount value + /// can be used for the original ABI encoding of `payload`. The placeholder amount will + /// be replaced with the actual amount when the Zap Data is decoded. + /// @param amountPosition Position (start index) where the token amount is encoded within `payload`. + /// This will usually be `4 + 32 * n`, where `n` is the position of the token amount in + /// the list of parameters of the target function (starting from 0). + /// Any value greater than or equal to `payload.length` can be used if the token amount is + /// not an argument of the target function. + function encodeZapData( + address target, + bytes memory payload, + uint256 amountPosition + ) + external + pure + returns (bytes memory) + { + if (payload.length > ZapDataV1.AMOUNT_NOT_PRESENT) { + revert TokenZapV1__PayloadLengthAboveMax(); + } + // External integrations do not need to understand the specific `AMOUNT_NOT_PRESENT` semantics. + // Therefore, they can specify any value greater than or equal to `payload.length` to indicate + // that the amount is not present in the payload. + if (amountPosition >= payload.length) { + amountPosition = ZapDataV1.AMOUNT_NOT_PRESENT; + } + // At this point, we have checked that both `amountPosition` and `payload.length` fit in uint16. + return ZapDataV1.encodeV1(uint16(amountPosition), target, payload); + } + + /// @notice Decodes the ZapData for a Zap action. Replaces the placeholder amount with the actual amount, + /// if it was present in the original `payload`. Otherwise, returns the original `payload` as is. + /// @param zapData Encoded Zap Data containing the target address and calldata for the Zap action. + /// @param amount Actual amount of the token to be used for the Zap action. + function decodeZapData( + bytes calldata zapData, + uint256 amount + ) + public + pure + returns (address target, bytes memory payload) + { + zapData.validateV1(); + target = zapData.target(); + payload = zapData.payload(amount); + } +} diff --git a/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol b/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol new file mode 100644 index 0000000000..b1b5cef18e --- /dev/null +++ b/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {ZapDataV1} from "../../contracts/libs/ZapDataV1.sol"; + +contract ZapDataV1Harness { + function validateV1(bytes calldata encodedZapData) public pure { + ZapDataV1.validateV1(encodedZapData); + } + + function encodeV1( + uint16 amountPosition_, + address target_, + bytes memory payload_ + ) + public + pure + returns (bytes memory encodedZapData) + { + return ZapDataV1.encodeV1(amountPosition_, target_, payload_); + } + + function version(bytes calldata encodedZapData) public pure returns (uint16) { + return ZapDataV1.version(encodedZapData); + } + + function target(bytes calldata encodedZapData) public pure returns (address) { + return ZapDataV1.target(encodedZapData); + } + + function payload(bytes calldata encodedZapData, uint256 amount) public pure returns (bytes memory) { + return ZapDataV1.payload(encodedZapData, amount); + } +} diff --git a/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol new file mode 100644 index 0000000000..9a32c1a259 --- /dev/null +++ b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {TokenZapV1IntegrationTest, VaultManyArguments, IFastBridge, IFastBridgeV2} from "./TokenZapV1.t.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract FastBridgeV2TokenZapV1DstTest is TokenZapV1IntegrationTest { + event BridgeRelayed( + bytes32 indexed transactionId, + address indexed relayer, + address indexed to, + uint32 originChainId, + address originToken, + address destToken, + uint256 originAmount, + uint256 destAmount, + uint256 chainGasAmount + ); + + function setUp() public virtual override { + vm.chainId(DST_CHAIN_ID); + super.setUp(); + } + + function mintTokens() public virtual override { + deal(relayer, DST_AMOUNT); + dstToken.mint(relayer, DST_AMOUNT); + vm.prank(relayer); + dstToken.approve(address(fastBridge), type(uint256).max); + } + + function relay( + IFastBridge.BridgeParams memory params, + IFastBridgeV2.BridgeParamsV2 memory paramsV2, + bool isToken + ) + public + { + bytes memory encodedBridgeTx = encodeBridgeTx(params, paramsV2); + vm.prank({msgSender: relayer, txOrigin: relayer}); + fastBridge.relay{value: isToken ? paramsV2.zapNative : DST_AMOUNT}(encodedBridgeTx); + } + + function expectEventBridgeRelayed( + IFastBridge.BridgeParams memory params, + IFastBridgeV2.BridgeParamsV2 memory paramsV2, + bool isToken + ) + public + { + bytes32 txId = keccak256(encodeBridgeTx(params, paramsV2)); + vm.expectEmit(address(fastBridge)); + emit BridgeRelayed({ + transactionId: txId, + relayer: relayer, + to: address(dstZap), + originChainId: SRC_CHAIN_ID, + originToken: isToken ? address(srcToken) : NATIVE_GAS_TOKEN, + destToken: isToken ? address(dstToken) : NATIVE_GAS_TOKEN, + originAmount: SRC_AMOUNT, + destAmount: DST_AMOUNT, + chainGasAmount: paramsV2.zapNative + }); + } + + function checkBalances(bool isToken) public view { + if (isToken) { + assertEq(dstToken.balanceOf(user), 0); + assertEq(dstToken.balanceOf(relayer), 0); + assertEq(dstToken.balanceOf(address(fastBridge)), 0); + assertEq(dstToken.balanceOf(address(dstZap)), 0); + assertEq(dstToken.balanceOf(address(dstVault)), DST_AMOUNT); + assertEq(dstVault.balanceOf(user, address(dstToken)), DST_AMOUNT); + } else { + assertEq(address(user).balance, 0); + assertEq(address(relayer).balance, 0); + assertEq(address(fastBridge).balance, 0); + assertEq(address(dstZap).balance, 0); + assertEq(address(dstVault).balance, DST_AMOUNT); + assertEq(dstVault.balanceOf(user, NATIVE_GAS_TOKEN), DST_AMOUNT); + } + } + + function test_relay_depositTokenParams() public { + expectEventBridgeRelayed({params: tokenParams, paramsV2: depositTokenParams, isToken: true}); + relay({params: tokenParams, paramsV2: depositTokenParams, isToken: true}); + checkBalances({isToken: true}); + } + + function test_relay_depositTokenWithZapNativeParams() public { + expectEventBridgeRelayed({params: tokenParams, paramsV2: depositTokenWithZapNativeParams, isToken: true}); + relay({params: tokenParams, paramsV2: depositTokenWithZapNativeParams, isToken: true}); + checkBalances({isToken: true}); + // Extra ETH will be also custodied by the Vault + assertEq(address(dstVault).balance, ZAP_NATIVE); + } + + function test_relay_depositTokenRevertParams_revert() public { + vm.expectRevert(VaultManyArguments.VaultManyArguments__SomeError.selector); + relay({params: tokenParams, paramsV2: depositTokenRevertParams, isToken: true}); + } + + function test_relay_depositNativeParams() public { + expectEventBridgeRelayed({params: nativeParams, paramsV2: depositNativeParams, isToken: false}); + relay({params: nativeParams, paramsV2: depositNativeParams, isToken: false}); + checkBalances({isToken: false}); + } + + function test_relay_depositNativeNoAmountParams() public { + expectEventBridgeRelayed({params: nativeParams, paramsV2: depositNativeNoAmountParams, isToken: false}); + relay({params: nativeParams, paramsV2: depositNativeNoAmountParams, isToken: false}); + checkBalances({isToken: false}); + } + + function test_relay_depositNativeRevertParams_revert() public { + vm.expectRevert(VaultManyArguments.VaultManyArguments__SomeError.selector); + relay({params: nativeParams, paramsV2: depositNativeRevertParams, isToken: false}); + } +} diff --git a/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol new file mode 100644 index 0000000000..40541c6056 --- /dev/null +++ b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {TokenZapV1IntegrationTest, IFastBridge, IFastBridgeV2} from "./TokenZapV1.t.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract FastBridgeV2TokenZapV1SrcTest is TokenZapV1IntegrationTest { + event BridgeRequested( + bytes32 indexed transactionId, + address indexed sender, + bytes request, + uint32 destChainId, + address originToken, + address destToken, + uint256 originAmount, + uint256 destAmount, + bool sendChainGas + ); + + function setUp() public virtual override { + vm.chainId(SRC_CHAIN_ID); + super.setUp(); + } + + function mintTokens() public virtual override { + deal(user, SRC_AMOUNT); + srcToken.mint(user, SRC_AMOUNT); + vm.prank(user); + srcToken.approve(address(fastBridge), type(uint256).max); + } + + function bridge( + IFastBridge.BridgeParams memory params, + IFastBridgeV2.BridgeParamsV2 memory paramsV2, + bool isToken + ) + public + { + vm.prank({msgSender: user, txOrigin: user}); + fastBridge.bridge{value: isToken ? 0 : SRC_AMOUNT}(params, paramsV2); + } + + function expectEventBridgeRequested( + IFastBridge.BridgeParams memory params, + IFastBridgeV2.BridgeParamsV2 memory paramsV2, + bool isToken + ) + public + { + bytes memory encodedBridgeTx = encodeBridgeTx(params, paramsV2); + bytes32 txId = keccak256(encodedBridgeTx); + vm.expectEmit(address(fastBridge)); + emit BridgeRequested({ + transactionId: txId, + sender: user, + request: encodedBridgeTx, + destChainId: DST_CHAIN_ID, + originToken: isToken ? address(srcToken) : NATIVE_GAS_TOKEN, + destToken: isToken ? address(dstToken) : NATIVE_GAS_TOKEN, + originAmount: SRC_AMOUNT, + destAmount: DST_AMOUNT, + sendChainGas: paramsV2.zapNative > 0 + }); + } + + function checkBalances(bool isToken) public view { + if (isToken) { + assertEq(srcToken.balanceOf(user), 0); + assertEq(srcToken.balanceOf(address(fastBridge)), SRC_AMOUNT); + } else { + assertEq(address(user).balance, 0); + assertEq(address(fastBridge).balance, SRC_AMOUNT); + } + } + + function test_bridge_depositTokenParams() public { + expectEventBridgeRequested({params: tokenParams, paramsV2: depositTokenParams, isToken: true}); + bridge({params: tokenParams, paramsV2: depositTokenParams, isToken: true}); + checkBalances({isToken: true}); + } + + function test_bridge_depositTokenWithZapNativeParams() public { + expectEventBridgeRequested({params: tokenParams, paramsV2: depositTokenWithZapNativeParams, isToken: true}); + bridge({params: tokenParams, paramsV2: depositTokenWithZapNativeParams, isToken: true}); + checkBalances({isToken: true}); + } + + function test_bridge_depositTokenRevertParams() public { + expectEventBridgeRequested({params: tokenParams, paramsV2: depositTokenRevertParams, isToken: true}); + bridge({params: tokenParams, paramsV2: depositTokenRevertParams, isToken: true}); + checkBalances({isToken: true}); + } + + function test_bridge_depositNativeParams() public { + expectEventBridgeRequested({params: nativeParams, paramsV2: depositNativeParams, isToken: false}); + bridge({params: nativeParams, paramsV2: depositNativeParams, isToken: false}); + checkBalances({isToken: false}); + } + + function test_bridge_depositNativeNoAmountParams() public { + expectEventBridgeRequested({params: nativeParams, paramsV2: depositNativeNoAmountParams, isToken: false}); + bridge({params: nativeParams, paramsV2: depositNativeNoAmountParams, isToken: false}); + checkBalances({isToken: false}); + } + + function test_bridge_depositNativeRevertParams() public { + expectEventBridgeRequested({params: nativeParams, paramsV2: depositNativeRevertParams, isToken: false}); + bridge({params: nativeParams, paramsV2: depositNativeRevertParams, isToken: false}); + checkBalances({isToken: false}); + } +} diff --git a/packages/contracts-rfq/test/integration/TokenZapV1.t.sol b/packages/contracts-rfq/test/integration/TokenZapV1.t.sol new file mode 100644 index 0000000000..ddf34693d0 --- /dev/null +++ b/packages/contracts-rfq/test/integration/TokenZapV1.t.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {FastBridgeV2, IFastBridge, IFastBridgeV2} from "../../contracts/FastBridgeV2.sol"; +import {BridgeTransactionV2Lib} from "../../contracts/libs/BridgeTransactionV2.sol"; +import {ZapDataV1} from "../../contracts/libs/ZapDataV1.sol"; +import {TokenZapV1} from "../../contracts/zaps/TokenZapV1.sol"; + +import {VaultManyArguments} from "../mocks/VaultManyArguments.sol"; +import {MockERC20} from "../MockERC20.sol"; + +import {Test} from "forge-std/Test.sol"; + +// solhint-disable ordering +abstract contract TokenZapV1IntegrationTest is Test { + address internal constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + uint32 internal constant SRC_CHAIN_ID = 1337; + uint32 internal constant DST_CHAIN_ID = 7331; + + uint256 internal constant SRC_AMOUNT = 1 ether; + uint256 internal constant DST_AMOUNT = 0.9999 ether; + uint256 internal constant ZAP_NATIVE = 123_456; + + FastBridgeV2 internal fastBridge; + TokenZapV1 internal dstZap; + + address internal user = makeAddr("User"); + address internal relayer = makeAddr("Relayer"); + + MockERC20 internal srcToken; + MockERC20 internal dstToken; + + VaultManyArguments internal dstVault; + + IFastBridge.BridgeParams internal tokenParams; + IFastBridge.BridgeParams internal nativeParams; + + IFastBridgeV2.BridgeParamsV2 internal depositTokenParams; + IFastBridgeV2.BridgeParamsV2 internal depositTokenWithZapNativeParams; + IFastBridgeV2.BridgeParamsV2 internal depositTokenRevertParams; + IFastBridgeV2.BridgeParamsV2 internal depositNativeParams; + IFastBridgeV2.BridgeParamsV2 internal depositNativeNoAmountParams; + IFastBridgeV2.BridgeParamsV2 internal depositNativeRevertParams; + + function setUp() public virtual { + fastBridge = new FastBridgeV2(address(this)); + fastBridge.grantRole(fastBridge.PROVER_ROLE(), relayer); + + srcToken = new MockERC20("SRC", 18); + dstToken = new MockERC20("DST", 18); + + dstZap = new TokenZapV1(); + dstVault = new VaultManyArguments(); + + createFixtures(); + mintTokens(); + } + + function createFixtures() public virtual { + tokenParams = IFastBridge.BridgeParams({ + dstChainId: DST_CHAIN_ID, + sender: user, + to: address(dstZap), + originToken: address(srcToken), + destToken: address(dstToken), + originAmount: SRC_AMOUNT, + destAmount: DST_AMOUNT, + sendChainGas: false, + deadline: block.timestamp + 1 days + }); + nativeParams = IFastBridge.BridgeParams({ + dstChainId: DST_CHAIN_ID, + sender: user, + to: address(dstZap), + originToken: NATIVE_GAS_TOKEN, + destToken: NATIVE_GAS_TOKEN, + originAmount: SRC_AMOUNT, + destAmount: DST_AMOUNT, + sendChainGas: false, + deadline: block.timestamp + 1 days + }); + // Deposit token + bytes memory zapData = dstZap.encodeZapData({ + target: address(dstVault), + payload: getDepositPayload(address(dstToken)), + amountPosition: 4 + 32 * 2 + }); + depositTokenParams.zapData = zapData; + depositTokenWithZapNativeParams.zapData = zapData; + depositTokenWithZapNativeParams.zapNative = ZAP_NATIVE; + // Deposit native + depositNativeParams.zapData = dstZap.encodeZapData({ + target: address(dstVault), + payload: getDepositPayload(NATIVE_GAS_TOKEN), + amountPosition: 4 + 32 * 2 + }); + // Deposit no amount + depositNativeNoAmountParams.zapData = dstZap.encodeZapData({ + target: address(dstVault), + payload: getDepositNoAmountPayload(), + amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT + }); + // Deposit revert + depositTokenRevertParams.zapData = dstZap.encodeZapData({ + target: address(dstVault), + payload: getDepositRevertPayload(), + amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT + }); + depositNativeRevertParams.zapData = dstZap.encodeZapData({ + target: address(dstVault), + payload: getDepositRevertPayload(), + amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT + }); + } + + function mintTokens() public virtual; + + function encodeBridgeTx( + IFastBridge.BridgeParams memory params, + IFastBridgeV2.BridgeParamsV2 memory paramsV2 + ) + public + pure + returns (bytes memory) + { + IFastBridgeV2.BridgeTransactionV2 memory bridgeTx = IFastBridgeV2.BridgeTransactionV2({ + originChainId: SRC_CHAIN_ID, + destChainId: params.dstChainId, + originSender: params.sender, + destRecipient: params.to, + originToken: params.originToken, + destToken: params.destToken, + originAmount: params.originAmount, + destAmount: params.destAmount, + // No protocol fees for the test + originFeeAmount: 0, + deadline: params.deadline, + // Single tx is sent, so nonce is 0 + nonce: 0, + exclusivityRelayer: address(0), + exclusivityEndTime: 0, + zapNative: paramsV2.zapNative, + zapData: paramsV2.zapData + }); + return BridgeTransactionV2Lib.encodeV2(bridgeTx); + } + + function getDepositPayload(address token) public view returns (bytes memory) { + return abi.encodeCall(dstVault.deposit, (token, abi.encode(token), DST_AMOUNT, user, abi.encode(user))); + } + + function getDepositNoAmountPayload() public view returns (bytes memory) { + return abi.encodeCall(dstVault.depositNoAmount, (user)); + } + + function getDepositRevertPayload() public view returns (bytes memory) { + return abi.encodeCall(dstVault.depositWithRevert, ()); + } +} diff --git a/packages/contracts-rfq/test/libs/ZapDataV1.t.sol b/packages/contracts-rfq/test/libs/ZapDataV1.t.sol new file mode 100644 index 0000000000..d93d9775bd --- /dev/null +++ b/packages/contracts-rfq/test/libs/ZapDataV1.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {ZapDataV1Harness, ZapDataV1} from "../harnesses/ZapDataV1Harness.sol"; + +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract ZapDataV1Test is Test { + uint16 internal constant EXPECTED_VERSION = 1; + + ZapDataV1Harness internal harness; + + function setUp() public { + harness = new ZapDataV1Harness(); + } + + function encodeZapData( + uint16 version, + uint16 amountPosition, + address target, + bytes memory payload + ) + public + pure + returns (bytes memory) + { + return abi.encodePacked(version, amountPosition, target, payload); + } + + function test_roundtrip_withAmount( + address target, + uint256 amount, + bytes memory prefix, + bytes memory postfix + ) + public + view + { + vm.assume(prefix.length + 32 + postfix.length < type(uint16).max); + vm.assume(target != address(0)); + + // We don't know the amount at the time of encoding, so we provide a placeholder. + uint16 amountPosition = uint16(prefix.length); + bytes memory encodedPayload = abi.encodePacked(prefix, uint256(0), postfix); + // We expect the correct amount to be substituted in the payload at the time of Zap. + bytes memory finalPayload = abi.encodePacked(prefix, amount, postfix); + + bytes memory zapData = harness.encodeV1(amountPosition, target, encodedPayload); + + harness.validateV1(zapData); + assertEq(harness.version(zapData), 1); + assertEq(harness.target(zapData), target); + assertEq(harness.payload(zapData, amount), finalPayload); + // Check against manually encoded ZapData. + assertEq(zapData, encodeZapData(EXPECTED_VERSION, amountPosition, target, encodedPayload)); + } + + function test_roundtrip_noAmount(address target, uint256 amount, bytes memory payload) public view { + vm.assume(payload.length < type(uint16).max); + vm.assume(target != address(0)); + + uint16 amountPosition = type(uint16).max; + bytes memory zapData = harness.encodeV1(amountPosition, target, payload); + + harness.validateV1(zapData); + assertEq(harness.version(zapData), 1); + assertEq(harness.target(zapData), target); + assertEq(harness.payload(zapData, amount), payload); + // Check against manually encoded ZapData. + assertEq(zapData, encodeZapData(EXPECTED_VERSION, amountPosition, target, payload)); + } + + function test_encodeV1_revert_targetZeroAddress() public { + vm.expectRevert(ZapDataV1.ZapDataV1__TargetZeroAddress.selector); + harness.encodeV1(type(uint16).max, address(0), ""); + } + + function test_encodeDecodeV1_revert_invalidAmountPosition( + address target, + uint16 amountPosition, + uint256 amount, + bytes memory payload + ) + public + { + vm.assume(payload.length < type(uint16).max); + vm.assume(target != address(0)); + // Make sure that (amountPosition + 32) is outside the bounds of the payload. + uint16 incorrectMin = payload.length > 31 ? uint16(payload.length) - 31 : 0; + uint16 incorrectMax = type(uint16).max - 1; + amountPosition = uint16(bound(uint256(amountPosition), incorrectMin, incorrectMax)); + bytes memory invalidEncodedZapData = abi.encodePacked(uint16(1), amountPosition, target, payload); + + vm.expectRevert(ZapDataV1.ZapDataV1__InvalidEncoding.selector); + harness.encodeV1(amountPosition, target, payload); + + // Validation should pass + harness.validateV1(invalidEncodedZapData); + harness.target(invalidEncodedZapData); + // But payload extraction should revert + vm.expectRevert(ZapDataV1.ZapDataV1__InvalidEncoding.selector); + harness.payload(invalidEncodedZapData, amount); + } + + function test_validateV1_revert_unsupportedVersion_withAmount( + uint16 version, + address target, + bytes memory prefix, + bytes memory postfix + ) + public + { + vm.assume(version != 1); + vm.assume(prefix.length + 32 + postfix.length < type(uint16).max); + // We don't know the amount at the time of encoding, so we provide a placeholder. + uint16 amountPosition = uint16(prefix.length); + bytes memory encodedPayload = abi.encodePacked(prefix, uint256(0), postfix); + + bytes memory invalidEncodedZapData = encodeZapData(version, amountPosition, target, encodedPayload); + + vm.expectRevert(abi.encodeWithSelector(ZapDataV1.ZapDataV1__UnsupportedVersion.selector, version)); + harness.validateV1(invalidEncodedZapData); + } + + function test_validateV1_revert_unsupportedVersion_noAmount( + uint16 version, + address target, + bytes memory payload + ) + public + { + vm.assume(version != 1); + vm.assume(payload.length < type(uint16).max); + + uint16 amountPosition = type(uint16).max; + bytes memory invalidEncodedZapData = encodeZapData(version, amountPosition, target, payload); + + vm.expectRevert(abi.encodeWithSelector(ZapDataV1.ZapDataV1__UnsupportedVersion.selector, version)); + harness.validateV1(invalidEncodedZapData); + } + + function test_validateV1_revert_invalidLength(bytes calldata fuzzData) public { + bytes memory minimumValidZapData = encodeZapData(EXPECTED_VERSION, type(uint16).max, address(0), ""); + uint256 invalidLength = fuzzData.length % minimumValidZapData.length; + bytes calldata invalidEncodedZapData = fuzzData[:invalidLength]; + + vm.expectRevert(ZapDataV1.ZapDataV1__InvalidEncoding.selector); + harness.validateV1(invalidEncodedZapData); + } +} diff --git a/packages/contracts-rfq/test/mocks/VaultManyArguments.sol b/packages/contracts-rfq/test/mocks/VaultManyArguments.sol new file mode 100644 index 0000000000..7e43817bd1 --- /dev/null +++ b/packages/contracts-rfq/test/mocks/VaultManyArguments.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {VaultMock} from "./VaultMock.sol"; + +/// @notice Vault mock for testing purposes. DO NOT USE IN PRODUCTION. +contract VaultManyArguments is VaultMock { + error VaultManyArguments__SomeError(); + + function deposit( + address token, + bytes memory encodedToken, + uint256 amount, + address user, + bytes memory encodedUser + ) + external + payable + { + // Make sure the data is not malformed + _validateBytes(token, encodedToken); + _validateBytes(user, encodedUser); + _deposit(user, token, amount); + } + + function depositNoAmount(address user) external payable { + _deposit(user, NATIVE_GAS_TOKEN, msg.value); + } + + function depositWithRevert() external payable { + revert VaultManyArguments__SomeError(); + } + + function _validateBytes(address addr, bytes memory encoded) internal pure { + if (keccak256(abi.encode(addr)) != keccak256(encoded)) revert VaultManyArguments__SomeError(); + } +} diff --git a/packages/contracts-rfq/test/mocks/VaultMock.sol b/packages/contracts-rfq/test/mocks/VaultMock.sol new file mode 100644 index 0000000000..b23fd55a41 --- /dev/null +++ b/packages/contracts-rfq/test/mocks/VaultMock.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice Vault mock for testing purposes. DO NOT USE IN PRODUCTION. +abstract contract VaultMock { + using SafeERC20 for IERC20; + + address internal constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + mapping(address user => mapping(address token => uint256 amount)) public balanceOf; + + error VaultMock__AmountIncorrect(); + + function _deposit(address user, address token, uint256 amount) internal { + if (token == NATIVE_GAS_TOKEN) { + if (msg.value != amount) revert VaultMock__AmountIncorrect(); + } else { + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + } + balanceOf[user][token] += amount; + } +} diff --git a/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol new file mode 100644 index 0000000000..7587131d3c --- /dev/null +++ b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {ZapDataV1} from "../../contracts/libs/ZapDataV1.sol"; +import {TokenZapV1} from "../../contracts/zaps/TokenZapV1.sol"; + +import {VaultManyArguments} from "../mocks/VaultManyArguments.sol"; +import {MockERC20} from "../MockERC20.sol"; + +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract TokenZapV1Test is Test { + uint256 internal constant AMOUNT = 0.987 ether; + + TokenZapV1 internal tokenZap; + VaultManyArguments internal vault; + MockERC20 internal erc20; + + address internal user; + address internal nativeGasToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + function setUp() public { + tokenZap = new TokenZapV1(); + vault = new VaultManyArguments(); + erc20 = new MockERC20("TKN", 18); + + user = makeAddr("user"); + + erc20.mint(address(this), 100 * AMOUNT); + deal(address(this), 100 * AMOUNT); + } + + function getVaultPayload(address token, uint256 amount) public view returns (bytes memory) { + return abi.encodeCall(vault.deposit, (token, abi.encode(token), amount, user, abi.encode(user))); + } + + function getVaultPayloadNoAmount() public view returns (bytes memory) { + return abi.encodeCall(vault.depositNoAmount, (user)); + } + + function getVaultPayloadWithRevert() public view returns (bytes memory) { + return abi.encodeCall(vault.depositWithRevert, ()); + } + + function getZapData(bytes memory originalPayload) public view returns (bytes memory) { + // Amount is the third argument of the deposit function + return tokenZap.encodeZapData(address(vault), originalPayload, 4 + 32 * 2); + } + + function getZapDataNoAmount(bytes memory originalPayload) public view returns (bytes memory) { + return tokenZap.encodeZapData(address(vault), originalPayload, originalPayload.length); + } + + function checkERC20HappyPath(bytes memory zapData, uint256 msgValue) public { + // Transfer tokens to the zap contract first + erc20.transfer(address(tokenZap), AMOUNT); + bytes4 returnValue = tokenZap.zap{value: msgValue}(address(erc20), AMOUNT, zapData); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(erc20)), AMOUNT); + } + + function test_zap_erc20_placeholderZero() public { + bytes memory zapData = getZapData(getVaultPayload(address(erc20), 0)); + checkERC20HappyPath(zapData, 0); + } + + function test_zap_erc20_placeholderNonZero() public { + // Use the approximate amount of tokens as placeholder + bytes memory zapData = getZapData(getVaultPayload(address(erc20), 1 ether)); + checkERC20HappyPath(zapData, 0); + } + + function test_zap_erc20_placeholderZero_withMsgValue() public { + bytes memory zapData = getZapData(getVaultPayload(address(erc20), 0)); + checkERC20HappyPath(zapData, 123_456); + // Should forward the msg.value to the vault + assertEq(address(vault).balance, 123_456); + } + + function test_zap_erc20_placeholderNonZero_withMsgValue() public { + bytes memory zapData = getZapData(getVaultPayload(address(erc20), 1 ether)); + checkERC20HappyPath(zapData, 123_456); + // Should forward the msg.value to the vault + assertEq(address(vault).balance, 123_456); + } + + function test_zap_erc20_placeholderZero_extraTokens() public { + // Mint some extra tokens to the zap contract + erc20.mint(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_erc20_placeholderZero(); + } + + function test_zap_erc20_placeholderNonZero_extraTokens() public { + // Mint some extra tokens to the zap contract + erc20.mint(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_erc20_placeholderNonZero(); + } + + function checkNativeHappyPath(bytes memory zapData) public { + bytes4 returnValue = tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, nativeGasToken), AMOUNT); + } + + function test_zap_native_placeholderZero() public { + bytes memory zapData = getZapData(getVaultPayload(nativeGasToken, 0)); + checkNativeHappyPath(zapData); + } + + function test_zap_native_placeholderNonZero() public { + // Use the approximate amount of tokens as placeholder + bytes memory zapData = getZapData(getVaultPayload(nativeGasToken, 1 ether)); + checkNativeHappyPath(zapData); + } + + function test_zap_native_noAmount() public { + bytes memory zapData = getZapDataNoAmount(getVaultPayloadNoAmount()); + checkNativeHappyPath(zapData); + } + + function test_zap_native_placeholderZero_extraNative() public { + // Mint some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_native_placeholderZero(); + } + + function test_zap_native_placeholderNonZero_extraNative() public { + // Mint some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_native_placeholderNonZero(); + } + + function test_zap_native_noAmount_extraNative() public { + // Mint some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_native_noAmount(); + } + + function test_encodeZapData_roundtrip(address token, uint256 placeholderAmount, uint256 amount) public view { + bytes memory originalPayload = getVaultPayload(token, placeholderAmount); + bytes memory expectedPayload = getVaultPayload(token, amount); + + bytes memory zapData = getZapData(originalPayload); + (address target, bytes memory payload) = tokenZap.decodeZapData(zapData, amount); + + assertEq(target, address(vault)); + assertEq(payload, expectedPayload); + } + + function test_encodeZapData_roundtripNoAmount(uint256 amountPosition) public view { + bytes memory payload = getVaultPayloadNoAmount(); + // Any value >= payload.length could be used to signal that the amount is not an argument of the target function + amountPosition = bound(amountPosition, payload.length, type(uint256).max); + + bytes memory zapData = tokenZap.encodeZapData(address(vault), payload, amountPosition); + (address target, bytes memory decodedPayload) = tokenZap.decodeZapData(zapData, 0); + assertEq(target, address(vault)); + assertEq(decodedPayload, payload); + } + + // ══════════════════════════════════════════════════ REVERTS ══════════════════════════════════════════════════════ + + function test_zap_erc20_revert_notEnoughTokens() public { + bytes memory zapData = getZapData(getVaultPayload(address(erc20), 0)); + // Transfer tokens to the zap contract first, but not enough + erc20.transfer(address(tokenZap), AMOUNT - 1); + vm.expectRevert(); + tokenZap.zap(address(erc20), AMOUNT, zapData); + } + + function test_zap_erc20_revert_targetReverted() public { + bytes memory zapData = getZapData(getVaultPayloadWithRevert()); + // Transfer tokens to the zap contract first + erc20.transfer(address(tokenZap), AMOUNT); + vm.expectRevert(VaultManyArguments.VaultManyArguments__SomeError.selector); + tokenZap.zap(address(erc20), AMOUNT, zapData); + } + + function test_zap_native_revert_targetReverted() public { + bytes memory zapData = getZapData(getVaultPayloadWithRevert()); + vm.expectRevert(VaultManyArguments.VaultManyArguments__SomeError.selector); + tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); + } + + function test_zap_native_revert_msgValueLowerThanExpected() public { + bytes memory originalPayload = getVaultPayload(nativeGasToken, 0); + bytes memory zapData = getZapData(originalPayload); + + vm.expectRevert(TokenZapV1.TokenZapV1__AmountIncorrect.selector); + tokenZap.zap{value: 1 ether - 1 wei}(nativeGasToken, 1 ether, zapData); + } + + function test_zap_native_revert_msgValueHigherThanExpected() public { + bytes memory originalPayload = getVaultPayload(nativeGasToken, 0); + bytes memory zapData = getZapData(originalPayload); + + vm.expectRevert(TokenZapV1.TokenZapV1__AmountIncorrect.selector); + tokenZap.zap{value: 1 ether + 1 wei}(nativeGasToken, 1 ether, zapData); + } + + function test_encodeZapData_revert_payloadLengthAboveMax() public { + bytes memory tooLongPayload = new bytes(2 ** 16); + vm.expectRevert(TokenZapV1.TokenZapV1__PayloadLengthAboveMax.selector); + tokenZap.encodeZapData(address(vault), tooLongPayload, 0); + } + + function test_encodeZapData_revert_targetZeroAddress() public { + bytes memory payload = getVaultPayloadNoAmount(); + + vm.expectRevert(ZapDataV1.ZapDataV1__TargetZeroAddress.selector); + tokenZap.encodeZapData(address(0), payload, payload.length); + } +} From 4cdb6ab5ec4a1a6bfcf16fa9e516909dc07fb5ae Mon Sep 17 00:00:00 2001 From: ChiTimesChi Date: Fri, 22 Nov 2024 11:55:22 +0000 Subject: [PATCH 02/12] Publish - @synapsecns/contracts-rfq@0.14.0 --- packages/contracts-rfq/CHANGELOG.md | 11 +++++++++++ packages/contracts-rfq/package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/CHANGELOG.md b/packages/contracts-rfq/CHANGELOG.md index 84f3d98166..db03e97763 100644 --- a/packages/contracts-rfq/CHANGELOG.md +++ b/packages/contracts-rfq/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.14.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.13.0...@synapsecns/contracts-rfq@0.14.0) (2024-11-22) + + +### Features + +* **contracts-rfq:** Token Zap [SLT-389] ([#3352](https://github.com/synapsecns/sanguine/issues/3352)) ([743e859](https://github.com/synapsecns/sanguine/commit/743e859e3274ed449c6410441bd664ff2aaf9740)), closes [#3382](https://github.com/synapsecns/sanguine/issues/3382) + + + + + # [0.13.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.12.1...@synapsecns/contracts-rfq@0.13.0) (2024-11-18) diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json index 9e6300ca22..86643b74e0 100644 --- a/packages/contracts-rfq/package.json +++ b/packages/contracts-rfq/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/contracts-rfq", "license": "MIT", - "version": "0.13.0", + "version": "0.14.0", "description": "FastBridge contracts.", "private": true, "files": [ From 601922bcad0e8ef5561cc85fa028ffb209267af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CF=87=C2=B2?= <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:53:54 +0000 Subject: [PATCH 03/12] refactor(contracts-rfq): docs cleanup [SLT-493] (#3405) * refactor: AdminV2 cleanup * refactor: FastBridgeV2 comments styling * refactor: struct assignment * refactor: regroup functions * docs: move to interfaces, fixing errors * docs: final round of FastBridgeV2 comment changes * docs: natspec * nit --------- Co-authored-by: parodime --- packages/contracts-rfq/contracts/AdminV2.sol | 55 ++-- .../contracts-rfq/contracts/FastBridgeV2.sol | 290 ++++++++++-------- .../contracts/interfaces/IAdminV2.sol | 7 + .../contracts/interfaces/IFastBridgeV2.sol | 5 +- .../contracts/interfaces/IMulticallTarget.sol | 18 +- .../contracts/interfaces/IZapRecipient.sol | 9 + .../contracts/libs/BridgeTransactionV2.sol | 2 +- .../contracts/utils/MulticallTarget.sol | 21 +- 8 files changed, 235 insertions(+), 172 deletions(-) diff --git a/packages/contracts-rfq/contracts/AdminV2.sol b/packages/contracts-rfq/contracts/AdminV2.sol index 5a8cf447ad..b1ab948ef0 100644 --- a/packages/contracts-rfq/contracts/AdminV2.sol +++ b/packages/contracts-rfq/contracts/AdminV2.sol @@ -8,70 +8,69 @@ import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +/// @title AdminV2 +/// @notice Provides administrative functions and controls for managing the FastBridgeV2 contract, +/// including access control and configuration settings. contract AdminV2 is AccessControlEnumerable, IAdminV2, IAdminV2Errors { using SafeERC20 for IERC20; - /// @notice Address reserved for native gas token (ETH on Ethereum and most L2s, AVAX on Avalanche, etc) + /// @notice The address reserved for the native gas token (ETH on Ethereum and most L2s, AVAX on Avalanche, etc.). address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - /// @notice Role identifier for Quoter API's off-chain authentication. + /// @notice The role identifier for the Quoter API's off-chain authentication. /// @dev Only addresses with this role can post FastBridge quotes to the API. bytes32 public constant QUOTER_ROLE = keccak256("QUOTER_ROLE"); - /// @notice Role identifier for Prover's on-chain authentication in FastBridge. + /// @notice The role identifier for the Prover's on-chain authentication in FastBridge. /// @dev Only addresses with this role can provide proofs that a FastBridge request has been relayed. bytes32 public constant PROVER_ROLE = keccak256("PROVER_ROLE"); - /// @notice Role identifier for Guard's on-chain authentication in FastBridge. + /// @notice The role identifier for the Guard's on-chain authentication in FastBridge. /// @dev Only addresses with this role can dispute submitted relay proofs during the dispute period. bytes32 public constant GUARD_ROLE = keccak256("GUARD_ROLE"); - /// @notice Role identifier for Canceler's on-chain authentication in FastBridge. + /// @notice The role identifier for the Canceler's on-chain authentication in FastBridge. /// @dev Only addresses with this role can cancel a FastBridge transaction without the cancel delay. bytes32 public constant CANCELER_ROLE = keccak256("CANCELER_ROLE"); - /// @notice Role identifier for Governor's on-chain administrative authority. + /// @notice The role identifier for the Governor's on-chain administrative authority. /// @dev Only addresses with this role can perform administrative tasks within the contract. bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE"); - /// @notice Denominator for fee rates, represents 100%. + /// @notice The denominator for fee rates, representing 100%. uint256 public constant FEE_BPS = 1e6; - /// @notice Maximum protocol fee rate: 1% on origin amount. + /// @notice The maximum protocol fee rate: 1% of the origin amount. uint256 public constant FEE_RATE_MAX = 0.01e6; - /// @notice Minimum cancel delay that can be set by the governor. + /// @notice The minimum cancel delay that can be set by the governor. uint256 public constant MIN_CANCEL_DELAY = 1 hours; - /// @notice Default cancel delay set during the contract deployment. + /// @notice The default cancel delay set during contract deployment. uint256 public constant DEFAULT_CANCEL_DELAY = 1 days; - /// @notice Protocol fee rate taken on origin amount deposited in origin chain + /// @notice The protocol fee rate taken on the origin amount deposited in the origin chain. uint256 public protocolFeeRate; - /// @notice Protocol fee amounts accumulated + /// @notice The accumulated protocol fee amounts. mapping(address => uint256) public protocolFees; - /// @notice Delay for a transaction after which it could be permisionlessly cancelled + /// @notice The delay period after which a transaction can be permissionlessly cancelled. uint256 public cancelDelay; - /// @notice This is deprecated and should not be used. + /// @notice This variable is deprecated and should not be used. /// @dev Use ZapNative V2 requests instead. uint256 public immutable chainGasAmount = 0; - constructor(address _owner) { - _grantRole(DEFAULT_ADMIN_ROLE, _owner); + constructor(address defaultAdmin) { + _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); _setCancelDelay(DEFAULT_CANCEL_DELAY); } - /// @notice Allows the contract governor to set the cancel delay. The cancel delay is the time after the transaction - /// deadline after which it can be permissionlessly cancelled, if it hasn't been proven by any of the Relayers. + /// @inheritdoc IAdminV2 function setCancelDelay(uint256 newCancelDelay) external onlyRole(GOVERNOR_ROLE) { _setCancelDelay(newCancelDelay); } - /// @notice Allows the contract governor to set the protocol fee rate. The protocol fee is taken from the origin - /// amount only for completed and claimed transactions. - /// @dev The protocol fee is abstracted away from the relayers, they always operate using the amounts after fees: - /// what they see as the origin amount emitted in the log is what they get credited with. + /// @inheritdoc IAdminV2 function setProtocolFeeRate(uint256 newFeeRate) external onlyRole(GOVERNOR_ROLE) { if (newFeeRate > FEE_RATE_MAX) revert FeeRateAboveMax(); uint256 oldFeeRate = protocolFeeRate; @@ -79,14 +78,15 @@ contract AdminV2 is AccessControlEnumerable, IAdminV2, IAdminV2Errors { emit FeeRateUpdated(oldFeeRate, newFeeRate); } - /// @notice Allows the contract governor to sweep the accumulated protocol fees in the contract. + /// @inheritdoc IAdminV2 function sweepProtocolFees(address token, address recipient) external onlyRole(GOVERNOR_ROLE) { + // Early exit if no accumulated fees. uint256 feeAmount = protocolFees[token]; - if (feeAmount == 0) return; // skip if no accumulated fees - + if (feeAmount == 0) return; + // Reset the accumulated fees first. protocolFees[token] = 0; emit FeesSwept(token, recipient, feeAmount); - /// Sweep the fees as the last transaction action + // Sweep the fees as the last transaction action. if (token == NATIVE_GAS_TOKEN) { Address.sendValue(payable(recipient), feeAmount); } else { @@ -94,7 +94,8 @@ contract AdminV2 is AccessControlEnumerable, IAdminV2, IAdminV2Errors { } } - /// @notice Internal function to set the cancel delay. Security checks are performed outside of this function. + /// @notice Internal logic to set the cancel delay. Security checks are performed outside of this function. + /// @dev This function is marked as private to prevent child contracts from calling it directly. function _setCancelDelay(uint256 newCancelDelay) private { if (newCancelDelay < MIN_CANCEL_DELAY) revert CancelDelayBelowMin(); uint256 oldCancelDelay = cancelDelay; diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol index a5aa64851a..08fe907507 100644 --- a/packages/contracts-rfq/contracts/FastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -14,37 +14,50 @@ import {MulticallTarget} from "./utils/MulticallTarget.sol"; import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -/// @notice FastBridgeV2 is a contract for bridging tokens across chains. +/// @title FastBridgeV2 +/// @notice Core component of the SynapseRFQ protocol, enabling Relayers (Solvers) to fulfill bridge requests. +/// Supports ERC20 and native gas tokens, along with the Zap feature for executing actions on the destination chain. +/// Users interact with the off-chain Quoter API to obtain a current quote for a bridge transaction. +/// They then submit the bridge request with the quote to this contract, depositing their assets in escrow. +/// Relayers can fulfill requests by relaying them to the destination chain and must prove fulfillment to claim funds. +/// Guards monitor proofs and can dispute discrepancies. +/// Users can reclaim funds by cancelling their requests if it has not been fulfilled within the specified deadline. contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2Errors { using BridgeTransactionV2Lib for bytes; using SafeERC20 for IERC20; - /// @notice Dispute period for relayed transactions + /// @notice The duration of the dispute period for relayed transactions. uint256 public constant DISPUTE_PERIOD = 30 minutes; - /// @notice Minimum deadline period to relay a requested bridge transaction + /// @notice The minimum required time between transaction request and deadline. uint256 public constant MIN_DEADLINE_PERIOD = 30 minutes; - /// @notice Maximum length of accepted zapData + /// @notice The maximum allowed length for zapData. uint256 public constant MAX_ZAP_DATA_LENGTH = 2 ** 16 - 1; - /// @notice Status of the bridge tx on origin chain + /// @notice Maps transaction IDs to bridge details (status, destination chain ID, proof timestamp, and relayer). + /// Note: this is only stored for transactions having local chain as the origin chain. mapping(bytes32 => BridgeTxDetails) public bridgeTxDetails; - /// @notice Relay details on destination chain + /// @notice Maps transaction IDs to relay details (block number, block timestamp, and relayer). + /// Note: this is only stored for transactions having local chain as the destination chain. mapping(bytes32 => BridgeRelay) public bridgeRelayDetails; - /// @notice Unique bridge nonces tracked per originSender + /// @notice Maps sender addresses to their unique bridge nonce. mapping(address => uint256) public senderNonces; - /// @notice This is deprecated and should not be used. - /// @dev Replaced by senderNonces + /// @notice This variable is deprecated and should not be used. + /// @dev Replaced by senderNonces. uint256 public immutable nonce = 0; - /// @notice the block the contract was deployed at + /// @notice The block number at which this contract was deployed. uint256 public immutable deployBlock; - constructor(address _owner) AdminV2(_owner) { + /// @notice Initializes the FastBridgeV2 contract with the provided default admin, + /// sets the default cancel delay, and records the deploy block number. + constructor(address defaultAdmin) AdminV2(defaultAdmin) { deployBlock = block.number; } + // ══════════════════════════════════════ EXTERNAL MUTABLE (USER FACING) ═══════════════════════════════════════════ + /// @inheritdoc IFastBridge function bridge(BridgeParams memory params) external payable { bridge({ @@ -59,9 +72,18 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E }); } + /// Note: this function is deprecated and will be removed in a future version. + /// @dev Replaced by `cancel`. + /// @inheritdoc IFastBridge + function refund(bytes calldata request) external { + cancel(request); + } + + // ══════════════════════════════════════ EXTERNAL MUTABLE (AGENT FACING) ══════════════════════════════════════════ + /// @inheritdoc IFastBridge function relay(bytes calldata request) external payable { - // relay override will validate the request + // `relay` override will validate the request. relay({request: request, relayer: msg.sender}); } @@ -73,24 +95,26 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E /// @inheritdoc IFastBridgeV2 function claim(bytes calldata request) external { - // claim override will validate the request + // `claim` override will validate the request. claim({request: request, to: address(0)}); } /// @inheritdoc IFastBridge function dispute(bytes32 transactionId) external onlyRole(GUARD_ROLE) { + // Aggregate the read operations from the same storage slot. BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - // Aggregate the read operations from the same storage slot address disputedRelayer = $.proofRelayer; BridgeStatus status = $.status; uint56 proofBlockTimestamp = $.proofBlockTimestamp; - // Can only dispute a RELAYER_PROVED transaction within the dispute period + + // Can only dispute a RELAYER_PROVED transaction within the dispute period. if (status != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); if (_timeSince(proofBlockTimestamp) > DISPUTE_PERIOD) { revert DisputePeriodPassed(); } - // Update status to REQUESTED and delete the disputed proof details - // Note: these are storage writes + + // Update status to REQUESTED and delete the disputed proof details. + // Note: these are storage writes. $.status = BridgeStatus.REQUESTED; $.proofRelayer = address(0); $.proofBlockTimestamp = 0; @@ -98,18 +122,15 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E emit BridgeProofDisputed(transactionId, disputedRelayer); } - /// Note: this function is deprecated and will be removed in a future version. - /// @inheritdoc IFastBridge - function refund(bytes calldata request) external { - cancel(request); - } + // ══════════════════════════════════════════════ EXTERNAL VIEWS ═══════════════════════════════════════════════════ /// @inheritdoc IFastBridge function canClaim(bytes32 transactionId, address relayer) external view returns (bool) { + // The correct relayer can only claim a RELAYER_PROVED transaction after the dispute period. BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - // The correct relayer can only claim a RELAYER_PROVED transaction after the dispute period if ($.status != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); if ($.proofRelayer != relayer) revert SenderIncorrect(); + return _timeSince($.proofBlockTimestamp) > DISPUTE_PERIOD; } @@ -119,9 +140,9 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E /// - `zapData` is ignored /// In order to process all kinds of requests use getBridgeTransactionV2 instead. function getBridgeTransaction(bytes calldata request) external view returns (BridgeTransaction memory) { - // Try decoding into V2 struct first. This will revert if V1 struct is passed + // Try decoding into V2 struct first. This will revert if V1 struct is passed. try this.getBridgeTransactionV2(request) returns (BridgeTransactionV2 memory txV2) { - // Note: we entirely ignore the zapData field, as it was not present in V1 + // Note: we entirely ignore the zapData field, as it was not present in V1. return BridgeTransaction({ originChainId: txV2.originChainId, destChainId: txV2.destChainId, @@ -137,7 +158,7 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E nonce: txV2.nonce }); } catch { - // Fallback to V1 struct + // Fallback to V1 struct. return abi.decode(request, (BridgeTransaction)); } } @@ -148,28 +169,31 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E return BridgeTransactionV2Lib.decodeV2(request); } + // ═══════════════════════════════════════ PUBLIC MUTABLE (USER FACING) ════════════════════════════════════════════ + /// @inheritdoc IFastBridgeV2 function bridge(BridgeParams memory params, BridgeParamsV2 memory paramsV2) public payable { + // If relayer exclusivity is not intended for this bridge, set exclusivityEndTime to static zero. + // Otherwise, set exclusivity to expire at the current block ts offset by quoteExclusivitySeconds. int256 exclusivityEndTime = 0; - // if relayer exclusivity is not intended for this bridge, set exclusivityEndTime to static zero - // otherwise, set exclusivity to expire at the current block ts offset by quoteExclusivitySeconds if (paramsV2.quoteRelayer != address(0)) { exclusivityEndTime = int256(block.timestamp) + paramsV2.quoteExclusivitySeconds; } _validateBridgeParams(params, paramsV2, exclusivityEndTime); - // transfer tokens to bridge contract - /// @dev use returned originAmount in request in case of transfer fees + // Transfer tokens to bridge contract. We use the actual transferred amount in case of transfer fees. uint256 originAmount = _takeBridgedUserAsset(params.originToken, params.originAmount); - // track amount of origin token owed to protocol - uint256 originFeeAmount; + // Track the amount of origin token owed to protocol. + uint256 originFeeAmount = 0; if (protocolFeeRate > 0) { originFeeAmount = (originAmount * protocolFeeRate) / FEE_BPS; - originAmount -= originFeeAmount; // remove from amount used in request as not relevant for relayers + // The Relayer filling this request will be paid the originAmount after fees. + // Note: the protocol fees will be accumulated only when the Relayer claims the origin collateral. + originAmount -= originFeeAmount; } - // set status to requested + // Hash the bridge request and set the initial status to REQUESTED. bytes memory request = BridgeTransactionV2Lib.encodeV2( BridgeTransactionV2({ originChainId: uint32(block.chainid), @@ -182,16 +206,17 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E destAmount: params.destAmount, originFeeAmount: originFeeAmount, deadline: params.deadline, - nonce: senderNonces[params.sender]++, // increment nonce on every bridge + // Increment the sender's nonce on every bridge. + nonce: senderNonces[params.sender]++, exclusivityRelayer: paramsV2.quoteRelayer, - // We checked exclusivityEndTime to be in range [0 .. params.deadline] above, so can safely cast + // We checked exclusivityEndTime to be in range [0 .. params.deadline] above, so can safely cast. exclusivityEndTime: uint256(exclusivityEndTime), zapNative: paramsV2.zapNative, zapData: paramsV2.zapData }) ); bytes32 transactionId = keccak256(request); - // Note: the tx status will be updated throughout the tx lifecycle, while destChainId is set once here + // Note: the tx status will be updated throughout the tx lifecycle, while destChainId is set once here. bridgeTxDetails[transactionId].status = BridgeStatus.REQUESTED; bridgeTxDetails[transactionId].destChainId = params.dstChainId; @@ -209,22 +234,63 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E emit BridgeQuoteDetails(transactionId, paramsV2.quoteId); } + /// @inheritdoc IFastBridgeV2 + function cancel(bytes calldata request) public { + // Decode the request and check that it could be cancelled. + request.validateV2(); + bytes32 transactionId = keccak256(request); + + // Can only cancel a REQUESTED transaction after its deadline expires. + BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; + if ($.status != BridgeStatus.REQUESTED) revert StatusIncorrect(); + + // Permissionless cancel is only allowed after `cancelDelay` on top of the deadline. + uint256 deadline = request.deadline(); + if (!hasRole(CANCELER_ROLE, msg.sender)) deadline += cancelDelay; + if (block.timestamp <= deadline) revert DeadlineNotExceeded(); + + // Update status to REFUNDED. + // Note: this is a storage write. + $.status = BridgeStatus.REFUNDED; + + // Return the full amount (collateral + protocol fees) to the original sender. + // The protocol fees are only accumulated when the transaction is claimed, so we don't need to update them here. + address to = request.originSender(); + address token = request.originToken(); + uint256 amount = request.originAmount() + request.originFeeAmount(); + + // Emit the event before any external calls. + emit BridgeDepositRefunded(transactionId, to, token, amount); + + // Return the funds to the original sender as last transaction action. + if (token == NATIVE_GAS_TOKEN) { + Address.sendValue(payable(to), amount); + } else { + IERC20(token).safeTransfer(to, amount); + } + } + + // ═══════════════════════════════════════ PUBLIC MUTABLE (AGENT FACING) ═══════════════════════════════════════════ + /// @inheritdoc IFastBridgeV2 function relay(bytes calldata request, address relayer) public payable { + // Decode the request and check that it could be relayed. request.validateV2(); bytes32 transactionId = keccak256(request); _validateRelayParams(request, transactionId, relayer); - // mark bridge transaction as relayed - bridgeRelayDetails[transactionId] = - BridgeRelay({blockNumber: uint48(block.number), blockTimestamp: uint48(block.timestamp), relayer: relayer}); - // transfer tokens to recipient on destination chain and trigger Zap if requested + // Mark the bridge request as relayed by saving the relayer and the block details. + bridgeRelayDetails[transactionId].blockNumber = uint48(block.number); + bridgeRelayDetails[transactionId].blockTimestamp = uint48(block.timestamp); + bridgeRelayDetails[transactionId].relayer = relayer; + + // Transfer tokens to recipient on destination chain and trigger Zap if requested. address to = request.destRecipient(); address token = request.destToken(); uint256 amount = request.destAmount(); uint256 zapNative = request.zapNative(); - // Emit the event before any external calls + // Emit the event before any external calls. emit BridgeRelayed({ transactionId: transactionId, relayer: relayer, @@ -240,13 +306,13 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E // All state changes have been done at this point, can proceed to the external calls. // This follows the checks-effects-interactions pattern to mitigate potential reentrancy attacks. if (token == NATIVE_GAS_TOKEN) { - // For the native gas token, additional zapNative is not allowed + // For the native gas token, additional zapNative is not allowed. if (zapNative != 0) revert ZapNativeNotSupported(); - // Check that the correct msg.value was sent + // Check that the correct msg.value was sent. if (msg.value != amount) revert MsgValueIncorrect(); - // Don't do a native transfer yet: we will handle it alongside the Zap below + // Don't do a native transfer yet: we will handle it alongside the Zap below. } else { - // For ERC20s, we check that the correct msg.value was sent + // For ERC20s, we check that the correct msg.value was sent. if (msg.value != zapNative) revert MsgValueIncorrect(); // We need to transfer the tokens from the Relayer to the recipient first before performing an // optional post-transfer Zap. @@ -263,10 +329,9 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E bytes calldata zapData = request.zapData(); if (zapData.length != 0) { // Zap Data is present: Zap has been requested by the recipient. Trigger it forwarding the full msg.value. - + _triggerZapWithChecks({recipient: to, token: token, amount: amount, zapData: zapData}); // Note: if token has a fee on transfers, the recipient will have received less than `amount`. // This is a very niche edge case and should be handled by the recipient contract. - _triggerZapWithChecks({recipient: to, token: token, amount: amount, zapData: zapData}); } else if (msg.value != 0) { // Zap Data is missing, but msg.value was sent. This could happen in two different cases: // - Relay with the native gas token is happening. @@ -278,12 +343,12 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E /// @inheritdoc IFastBridgeV2 function prove(bytes32 transactionId, bytes32 destTxHash, address relayer) public onlyRole(PROVER_ROLE) { + // Can only prove a REQUESTED transaction. BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - - // Can only prove a REQUESTED transaction if ($.status != BridgeStatus.REQUESTED) revert StatusIncorrect(); - // Update status to RELAYER_PROVED and store the proof details - // Note: these are storage writes + + // Update status to RELAYER_PROVED and store the proof details. + // Note: these are storage writes. $.status = BridgeStatus.RELAYER_PROVED; $.proofBlockTimestamp = uint56(block.timestamp); $.proofRelayer = relayer; @@ -293,69 +358,41 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E /// @inheritdoc IFastBridge function claim(bytes calldata request, address to) public { + // Decode the request and check that it could be claimed. request.validateV2(); bytes32 transactionId = keccak256(request); + + // Aggregate the read operations from the same storage slot. BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - // Aggregate the read operations from the same storage slot address proofRelayer = $.proofRelayer; BridgeStatus status = $.status; uint56 proofBlockTimestamp = $.proofBlockTimestamp; - // Can only claim a RELAYER_PROVED transaction after the dispute period + // Can only claim a RELAYER_PROVED transaction after the dispute period. if (status != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); - if (_timeSince(proofBlockTimestamp) <= DISPUTE_PERIOD) { - revert DisputePeriodNotPassed(); - } - + if (_timeSince(proofBlockTimestamp) <= DISPUTE_PERIOD) revert DisputePeriodNotPassed(); if (to == address(0)) { - // Anyone could claim the funds to the proven relayer on their behalf + // Anyone could claim the funds to the proven relayer on their behalf. to = proofRelayer; } else if (proofRelayer != msg.sender) { - // Only the proven relayer could specify an address to claim the funds to + // Only the proven relayer could specify an address to claim the funds to. revert SenderIncorrect(); } - // Update status to RELAYER_CLAIMED and transfer the origin collateral to the specified claim address - // Note: this is a storage write + // Update status to RELAYER_CLAIMED and transfer the origin collateral to the specified claim address. + // Note: this is a storage write. $.status = BridgeStatus.RELAYER_CLAIMED; + // Accumulate protocol fees if origin fee amount exists. address token = request.originToken(); uint256 amount = request.originAmount(); - // Update protocol fees if origin fee amount exists uint256 originFeeAmount = request.originFeeAmount(); if (originFeeAmount > 0) protocolFees[token] += originFeeAmount; - // Emit the event before any external calls - emit BridgeDepositClaimed(transactionId, proofRelayer, to, token, amount); - // Complete the relayer claim as the last transaction action - if (token == NATIVE_GAS_TOKEN) { - Address.sendValue(payable(to), amount); - } else { - IERC20(token).safeTransfer(to, amount); - } - } - /// @inheritdoc IFastBridgeV2 - function cancel(bytes calldata request) public { - request.validateV2(); - bytes32 transactionId = keccak256(request); - BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - // Can only cancel a REQUESTED transaction after its deadline expires - if ($.status != BridgeStatus.REQUESTED) revert StatusIncorrect(); - uint256 deadline = request.deadline(); - // Permissionless cancel is only allowed after `cancelDelay` on top of the deadline - if (!hasRole(CANCELER_ROLE, msg.sender)) deadline += cancelDelay; - if (block.timestamp <= deadline) revert DeadlineNotExceeded(); - // Update status to REFUNDED and return the full amount (collateral + protocol fees) to the original sender. - // The protocol fees are only updated when the transaction is claimed, so we don't need to update them here. - // Note: this is a storage write - $.status = BridgeStatus.REFUNDED; + // Emit the event before any external calls. + emit BridgeDepositClaimed(transactionId, proofRelayer, to, token, amount); - address to = request.originSender(); - address token = request.originToken(); - uint256 amount = request.originAmount() + request.originFeeAmount(); - // Emit the event before any external calls - emit BridgeDepositRefunded(transactionId, to, token, amount); - // Complete the user cancel as the last transaction action + // Complete the relayer claim as the last transaction action. if (token == NATIVE_GAS_TOKEN) { Address.sendValue(payable(to), amount); } else { @@ -363,6 +400,8 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E } } + // ═══════════════════════════════════════════════ PUBLIC VIEWS ════════════════════════════════════════════════════ + /// @inheritdoc IFastBridgeV2 function bridgeStatuses(bytes32 transactionId) public view returns (BridgeStatus status) { return bridgeTxDetails[transactionId].status; @@ -371,20 +410,21 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E /// @inheritdoc IFastBridgeV2 function bridgeProofs(bytes32 transactionId) public view returns (uint96 timestamp, address relayer) { BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - timestamp = $.proofBlockTimestamp; relayer = $.proofRelayer; } /// @inheritdoc IFastBridgeV2 function bridgeRelays(bytes32 transactionId) public view returns (bool) { - // has this transactionId been relayed? + // This transaction has been relayed if the relayer address is recorded. return bridgeRelayDetails[transactionId].relayer != address(0); } - /// @notice Takes the bridged asset from the user into FastBridgeV2 custody. It will be later - /// claimed by the relayer who completed the relay on destination chain, or transferred back to the user - /// via the cancel function should no one complete the relay. + // ═════════════════════════════════════════════ INTERNAL METHODS ══════════════════════════════════════════════════ + + /// @notice Takes the bridged asset from the user into FastBridgeV2 custody. The asset will later be + /// claimed by the relayer who completed the relay on the destination chain, or returned to the user + /// via the cancel function if no relay is completed. function _takeBridgedUserAsset(address token, uint256 amount) internal returns (uint256 amountTaken) { if (token == NATIVE_GAS_TOKEN) { // For the native gas token, we just need to check that the supplied msg.value is correct. @@ -395,50 +435,52 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E // For ERC20s, token is explicitly transferred from the user to FastBridgeV2. // We don't allow non-zero `msg.value` to avoid extra funds from being stuck in FastBridgeV2. if (msg.value != 0) revert MsgValueIncorrect(); - // Throw an explicit error if the provided token address is not a contract + // Throw an explicit error if the provided token address is not a contract. if (token.code.length == 0) revert TokenNotContract(); + + // Use the balance difference as the amount taken in case of fee on transfer tokens. amountTaken = IERC20(token).balanceOf(address(this)); IERC20(token).safeTransferFrom(msg.sender, address(this), amount); - // Use the balance difference as the amount taken in case of fee on transfer tokens. amountTaken = IERC20(token).balanceOf(address(this)) - amountTaken; } } - /// @notice Calls the Recipient's hook function with the specified zapData and performs - /// all the necessary checks for the returned value. + /// @notice Calls the recipient's hook function with the specified zapData and validates + /// the returned value. function _triggerZapWithChecks(address recipient, address token, uint256 amount, bytes calldata zapData) internal { - // This will bubble any revert messages from the hook function + // Call the recipient's hook function with the specified zapData, bubbling any revert messages. bytes memory returnData = Address.functionCallWithValue({ target: recipient, data: abi.encodeCall(IZapRecipient.zap, (token, amount, zapData)), - // Note: see `relay()` for reasoning behind passing msg.value + // Note: see `relay()` for reasoning behind passing msg.value. value: msg.value }); - // Explicit revert if no return data at all + + // Explicit revert if no return data at all. if (returnData.length == 0) revert RecipientNoReturnValue(); - // Check that exactly a single return value was returned + // Check that exactly a single return value was returned. if (returnData.length != 32) revert RecipientIncorrectReturnValue(); - // Return value should be abi-encoded hook function selector + // Return value should be abi-encoded hook function selector. if (bytes32(returnData) != bytes32(IZapRecipient.zap.selector)) { revert RecipientIncorrectReturnValue(); } } - /// @notice Calculates time since proof submitted - /// @dev proof.timestamp stores casted uint56(block.timestamp) block timestamps for gas optimization - /// _timeSince(proof) can accomodate rollover case when block.timestamp > type(uint56).max but - /// proof.timestamp < type(uint56).max via unchecked statement - /// @param proofBlockTimestamp The bridge proof block timestamp - /// @return delta Time delta since proof submitted + /// @notice Calculates the time elapsed since a proof was submitted. + /// @dev The proof.timestamp stores block timestamps as uint56 for gas optimization. + /// _timeSince(proof) handles timestamp rollover when block.timestamp > type(uint56).max but + /// proof.timestamp < type(uint56).max via an unchecked statement. + /// @param proofBlockTimestamp The block timestamp when the proof was submitted. + /// @return delta The time elapsed since proof submission. function _timeSince(uint56 proofBlockTimestamp) internal view returns (uint256 delta) { unchecked { delta = uint56(block.timestamp) - proofBlockTimestamp; } } - /// @notice Performs all the necessary checks for a bridge to happen. - /// @dev There's no good way to refactor this function to reduce cyclomatic complexity due to - /// the number of checks that need to be performed, so we skip the code-complexity rule here. + /// @notice Validates all parameters required for a bridge transaction. + /// @dev This function's complexity cannot be reduced due to the number of required checks, + /// so we disable the code-complexity rule. // solhint-disable-next-line code-complexity function _validateBridgeParams( BridgeParams memory params, @@ -448,32 +490,34 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E internal view { - // Check V1 (legacy) params + // Check V1 (legacy) params. if (params.dstChainId == block.chainid) revert ChainIncorrect(); if (params.originAmount == 0 || params.destAmount == 0) revert AmountIncorrect(); if (params.sender == address(0) || params.to == address(0)) revert ZeroAddress(); if (params.originToken == address(0) || params.destToken == address(0)) revert ZeroAddress(); if (params.deadline < block.timestamp + MIN_DEADLINE_PERIOD) revert DeadlineTooShort(); - // Check V2 params + + // Check V2 params. if (paramsV2.zapData.length > MAX_ZAP_DATA_LENGTH) revert ZapDataLengthAboveMax(); if (paramsV2.zapNative != 0 && params.destToken == NATIVE_GAS_TOKEN) { revert ZapNativeNotSupported(); } - // exclusivityEndTime must be in range [0 .. params.deadline] + + // exclusivityEndTime must be in range [0 .. params.deadline]. if (exclusivityEndTime < 0 || exclusivityEndTime > int256(params.deadline)) { revert ExclusivityParamsIncorrect(); } } - /// @notice Performs all the necessary checks for a relay to happen. + /// @notice Validates all parameters required for a relay transaction. function _validateRelayParams(bytes calldata request, bytes32 transactionId, address relayer) internal view { if (relayer == address(0)) revert ZeroAddress(); - // Check if the transaction has already been relayed + // Check that the transaction has not been relayed yet and is for the current chain. if (bridgeRelays(transactionId)) revert TransactionRelayed(); if (request.destChainId() != block.chainid) revert ChainIncorrect(); - // Check the deadline for relay to happen + // Check that the deadline for relay to happen has not passed yet. if (block.timestamp > request.deadline()) revert DeadlineExceeded(); - // Check the exclusivity period, if it is still ongoing + // Check the exclusivity period, if it was specified and is still ongoing. address exclRelayer = request.exclusivityRelayer(); if (exclRelayer != address(0) && exclRelayer != relayer && block.timestamp <= request.exclusivityEndTime()) { revert ExclusivityPeriodNotPassed(); diff --git a/packages/contracts-rfq/contracts/interfaces/IAdminV2.sol b/packages/contracts-rfq/contracts/interfaces/IAdminV2.sol index 1a30434161..90115d2f49 100644 --- a/packages/contracts-rfq/contracts/interfaces/IAdminV2.sol +++ b/packages/contracts-rfq/contracts/interfaces/IAdminV2.sol @@ -6,9 +6,16 @@ interface IAdminV2 { event FeeRateUpdated(uint256 oldFeeRate, uint256 newFeeRate); event FeesSwept(address token, address recipient, uint256 amount); + /// @notice Allows the governor to set the cancel delay. The cancel delay is the time period after the transaction + /// deadline during which a transaction can be permissionlessly cancelled if it hasn't been proven by any Relayer. function setCancelDelay(uint256 newCancelDelay) external; + /// @notice Allows the governor to set the protocol fee rate. The protocol fee is taken from the origin + /// amount and is only applied to completed and claimed transactions. + /// @dev The protocol fee is abstracted away from the relayers; they always operate using the amounts after fees. + /// The origin amount they see in the emitted log is what they get credited with. function setProtocolFeeRate(uint256 newFeeRate) external; + /// @notice Allows the governor to withdraw the accumulated protocol fees from the contract. function sweepProtocolFees(address token, address recipient) external; } diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol index 258369b191..bf60539d10 100644 --- a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol @@ -5,7 +5,7 @@ import {IFastBridge} from "./IFastBridge.sol"; interface IFastBridgeV2 is IFastBridge { enum BridgeStatus { - NULL, // doesn't exist yet + NULL, // Doesn't exist yet. REQUESTED, RELAYER_PROVED, RELAYER_CLAIMED, @@ -26,8 +26,7 @@ interface IFastBridgeV2 is IFastBridge { } /// @notice New params introduced in the FastBridgeV2. - /// We are passing fields from the older BridgeParams struct outside of this struct - /// for backwards compatibility. + /// We are passing fields from the older BridgeParams struct outside of this struct for backwards compatibility. /// Note: quoteRelayer and quoteExclusivitySeconds are either both zero (indicating no exclusivity) /// or both non-zero (indicating exclusivity for the given period). /// Note: zapNative > 0 can NOT be used with destToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE (native token) diff --git a/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol b/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol index 8902983c72..f162e0d160 100644 --- a/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol +++ b/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -/// @notice Interface for a contract that can be called multiple times by the same caller. Inspired by MulticallV3: +/// @notice Interface for a contract that supports multiple calls from the same caller. Inspired by MulticallV3: /// https://github.com/mds1/multicall/blob/master/src/Multicall3.sol interface IMulticallTarget { struct Result { @@ -9,7 +9,23 @@ interface IMulticallTarget { bytes returnData; } + /// @notice Executes multiple calls to this contract in a single transaction while preserving msg.sender. + /// Return data from the calls is discarded. + /// @dev This method is non-payable, so only calls with msg.value of 0 can be batched. + /// If ignoreReverts is set to true, reverted calls will be skipped. + /// Otherwise, the entire batch will revert with the original revert reason. + /// @param data List of ABI-encoded calldata for the calls to execute + /// @param ignoreReverts Whether to skip calls that revert function multicallNoResults(bytes[] calldata data, bool ignoreReverts) external; + + /// @notice Executes multiple calls to this contract in a single transaction while preserving msg.sender. + /// Return data from each call is preserved. + /// @dev This method is non-payable, so only calls with msg.value of 0 can be batched. + /// If ignoreReverts is set to true, reverted calls will be skipped. + /// Otherwise, the entire batch will revert with the original revert reason. + /// @param data List of ABI-encoded calldata for the calls to execute + /// @param ignoreReverts Whether to skip calls that revert + /// @return results List of results from the calls, each containing (success, returnData) function multicallWithResults( bytes[] calldata data, bool ignoreReverts diff --git a/packages/contracts-rfq/contracts/interfaces/IZapRecipient.sol b/packages/contracts-rfq/contracts/interfaces/IZapRecipient.sol index 8746a9c16e..2300854ce4 100644 --- a/packages/contracts-rfq/contracts/interfaces/IZapRecipient.sol +++ b/packages/contracts-rfq/contracts/interfaces/IZapRecipient.sol @@ -1,6 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; +/// @notice Interface for contracts that can perform Zap operations. Such contracts could be used as Recipients +/// in a FastBridge transaction that includes a Zap operation. The Zap Data should include instructions on how +/// exactly the Zap operation should be executed, which would typically include the target address and calldata +/// to use. The exact implementation of the Zap Data encoding is up to the Recipient contract. interface IZapRecipient { + /// @notice Performs a Zap operation with the given token and amount according to the provided Zap data. + /// @param token The address of the token being used for the Zap operation. + /// @param amount The amount of tokens to be used. + /// @param zapData The encoded data specifying how the Zap operation should be executed. + /// @return The function selector to indicate successful execution. function zap(address token, uint256 amount, bytes memory zapData) external payable returns (bytes4); } diff --git a/packages/contracts-rfq/contracts/libs/BridgeTransactionV2.sol b/packages/contracts-rfq/contracts/libs/BridgeTransactionV2.sol index 2bdca5ef8b..dc16cb8b57 100644 --- a/packages/contracts-rfq/contracts/libs/BridgeTransactionV2.sol +++ b/packages/contracts-rfq/contracts/libs/BridgeTransactionV2.sol @@ -46,7 +46,7 @@ library BridgeTransactionV2Lib { error BridgeTransactionV2__InvalidEncodedTx(); error BridgeTransactionV2__UnsupportedVersion(uint16 version); - /// @notice Validates the encoded transaction to be a tightly packed encoded payload for BridgeTransactionV2. + /// @notice Validates that the encoded transaction is a tightly packed encoded payload for BridgeTransactionV2. /// @dev Checks the minimum length and the version, use this function before decoding any of the fields. function validateV2(bytes calldata encodedTx) internal pure { // Check the minimum length: must at least include all static fields. diff --git a/packages/contracts-rfq/contracts/utils/MulticallTarget.sol b/packages/contracts-rfq/contracts/utils/MulticallTarget.sol index 51493c10e2..6d7bbbd1c2 100644 --- a/packages/contracts-rfq/contracts/utils/MulticallTarget.sol +++ b/packages/contracts-rfq/contracts/utils/MulticallTarget.sol @@ -4,18 +4,12 @@ pragma solidity ^0.8.4; import {IMulticallTarget} from "../interfaces/IMulticallTarget.sol"; // solhint-disable avoid-low-level-calls -/// @notice Template for a contract that supports batched calls (preserving the msg.sender). -/// Only calls with zero msg.value could be batched. +/// @notice Abstract contract template that supports batched calls while preserving msg.sender. +/// Only calls with msg.value of 0 can be batched. abstract contract MulticallTarget is IMulticallTarget { error MulticallTarget__UndeterminedRevert(); - /// @notice Perform a batched call to this contract, preserving the msg.sender. - /// The return data from each call is discarded. - /// @dev The method is non-payable, so only calls with `msg.value == 0` could be batched. - /// It's possible to ignore the reverts from the calls by setting the `ignoreReverts` flag. - /// Otherwise, the whole batch call will be reverted with the original revert reason. - /// @param data List of abi-encoded calldata for the calls to perform. - /// @param ignoreReverts Whether to ignore the revert errors from the calls. + /// @inheritdoc IMulticallTarget function multicallNoResults(bytes[] calldata data, bool ignoreReverts) external { for (uint256 i = 0; i < data.length; ++i) { // We perform a delegate call to ourself to preserve the msg.sender. This is identical to `msg.sender` @@ -29,14 +23,7 @@ abstract contract MulticallTarget is IMulticallTarget { } } - /// @notice Perform a batched call to this contract, preserving the msg.sender. - /// The return data from each call is preserved. - /// @dev The method is non-payable, so only calls with `msg.value == 0` could be batched. - /// It's possible to ignore the reverts from the calls by setting the `ignoreReverts` flag. - /// Otherwise, the whole batch call will be reverted with the original revert reason. - /// @param data List of abi-encoded calldata for the calls to perform. - /// @param ignoreReverts Whether to ignore the revert errors from the calls. - /// @return results List of results from the calls: `(success, returnData)`. + /// @inheritdoc IMulticallTarget function multicallWithResults( bytes[] calldata data, bool ignoreReverts From 24a88dcd65984e900d04ae577f8a16d16c598d5a Mon Sep 17 00:00:00 2001 From: ChiTimesChi Date: Fri, 22 Nov 2024 14:58:59 +0000 Subject: [PATCH 04/12] Publish - @synapsecns/contracts-rfq@0.14.1 --- packages/contracts-rfq/CHANGELOG.md | 8 ++++++++ packages/contracts-rfq/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/CHANGELOG.md b/packages/contracts-rfq/CHANGELOG.md index db03e97763..df38802346 100644 --- a/packages/contracts-rfq/CHANGELOG.md +++ b/packages/contracts-rfq/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.14.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.0...@synapsecns/contracts-rfq@0.14.1) (2024-11-22) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + # [0.14.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.13.0...@synapsecns/contracts-rfq@0.14.0) (2024-11-22) diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json index 86643b74e0..f1e8fbbf5c 100644 --- a/packages/contracts-rfq/package.json +++ b/packages/contracts-rfq/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/contracts-rfq", "license": "MIT", - "version": "0.14.0", + "version": "0.14.1", "description": "FastBridge contracts.", "private": true, "files": [ From 31997a82672f41e339abe8da76617f9ffdae7852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CF=87=C2=B2?= <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:16:32 +0000 Subject: [PATCH 05/12] test(contracts-rfq): gas bench for `TokenZapV1` (#3406) * test: add minimal mock for a vault contract * test: gas bench for TokenZap --- .../test/mocks/SimpleVaultMock.sol | 15 +++++ .../test/mocks/VaultManyArguments.sol | 4 ++ .../test/zaps/TokenZapV1.GasBench.t.sol | 65 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 packages/contracts-rfq/test/mocks/SimpleVaultMock.sol create mode 100644 packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol diff --git a/packages/contracts-rfq/test/mocks/SimpleVaultMock.sol b/packages/contracts-rfq/test/mocks/SimpleVaultMock.sol new file mode 100644 index 0000000000..219976979e --- /dev/null +++ b/packages/contracts-rfq/test/mocks/SimpleVaultMock.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {VaultMock} from "./VaultMock.sol"; + +// solhint-disable no-empty-blocks +/// @notice Vault mock for testing purposes. DO NOT USE IN PRODUCTION. +contract SimpleVaultMock is VaultMock { + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testSimpleVaultMock() external {} + + function deposit(address token, uint256 amount, address user) external payable { + _deposit(user, token, amount); + } +} diff --git a/packages/contracts-rfq/test/mocks/VaultManyArguments.sol b/packages/contracts-rfq/test/mocks/VaultManyArguments.sol index 7e43817bd1..20408bf6b9 100644 --- a/packages/contracts-rfq/test/mocks/VaultManyArguments.sol +++ b/packages/contracts-rfq/test/mocks/VaultManyArguments.sol @@ -3,10 +3,14 @@ pragma solidity ^0.8.20; import {VaultMock} from "./VaultMock.sol"; +// solhint-disable no-empty-blocks /// @notice Vault mock for testing purposes. DO NOT USE IN PRODUCTION. contract VaultManyArguments is VaultMock { error VaultManyArguments__SomeError(); + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testSimpleVaultMock() external {} + function deposit( address token, bytes memory encodedToken, diff --git a/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol b/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol new file mode 100644 index 0000000000..bae47da888 --- /dev/null +++ b/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {TokenZapV1} from "../../contracts/zaps/TokenZapV1.sol"; + +import {SimpleVaultMock} from "../mocks/SimpleVaultMock.sol"; +import {MockERC20} from "../MockERC20.sol"; + +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract TokenZapV1GasBenchmarkTest is Test { + uint256 internal constant AMOUNT = 0.1337 ether; + + SimpleVaultMock internal vault; + TokenZapV1 internal tokenZap; + MockERC20 internal erc20; + + address internal user; + address internal nativeGasToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + function setUp() public { + tokenZap = new TokenZapV1(); + vault = new SimpleVaultMock(); + erc20 = new MockERC20("TKN", 18); + + user = makeAddr("user"); + + erc20.mint(address(this), AMOUNT); + deal(address(this), AMOUNT); + // To simulate an average case we assume that the Vault contract has already other deposited funds. + erc20.mint(address(vault), 1000 * AMOUNT); + deal(address(vault), 1000 * AMOUNT); + // We also assume that this is not the first tx through the Zap, so the infinite approval has already been set. + vm.prank(address(tokenZap)); + erc20.approve(address(vault), type(uint256).max); + } + + function getVaultPayload(address token, uint256 amount) public view returns (bytes memory) { + return abi.encodeCall(vault.deposit, (token, amount, user)); + } + + function getZapData(bytes memory originalPayload) public view returns (bytes memory) { + // Amount is the second argument of the deposit function. + return tokenZap.encodeZapData(address(vault), originalPayload, 4 + 32); + } + + function test_deposit_erc20() public { + bytes memory depositPayload = getVaultPayload(address(erc20), AMOUNT); + bytes memory zapData = getZapData(depositPayload); + // Transfer tokens to the zap contract first. + erc20.transfer(address(tokenZap), AMOUNT); + tokenZap.zap(address(erc20), AMOUNT, zapData); + // Check that the vault registered the deposit. + assertEq(vault.balanceOf(user, address(erc20)), AMOUNT); + } + + function test_deposit_native() public { + bytes memory depositPayload = getVaultPayload(nativeGasToken, AMOUNT); + bytes memory zapData = getZapData(depositPayload); + tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); + // Check that the vault registered the deposit. + assertEq(vault.balanceOf(user, nativeGasToken), AMOUNT); + } +} From d8ef816882765fd0970acee76fd9e7b1e4b7b2ef Mon Sep 17 00:00:00 2001 From: ChiTimesChi Date: Fri, 22 Nov 2024 19:21:14 +0000 Subject: [PATCH 06/12] Publish - @synapsecns/contracts-rfq@0.14.2 --- packages/contracts-rfq/CHANGELOG.md | 8 ++++++++ packages/contracts-rfq/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/CHANGELOG.md b/packages/contracts-rfq/CHANGELOG.md index df38802346..bbf16c10d6 100644 --- a/packages/contracts-rfq/CHANGELOG.md +++ b/packages/contracts-rfq/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.14.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.1...@synapsecns/contracts-rfq@0.14.2) (2024-11-22) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + ## [0.14.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.0...@synapsecns/contracts-rfq@0.14.1) (2024-11-22) **Note:** Version bump only for package @synapsecns/contracts-rfq diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json index f1e8fbbf5c..f329a66a83 100644 --- a/packages/contracts-rfq/package.json +++ b/packages/contracts-rfq/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/contracts-rfq", "license": "MIT", - "version": "0.14.1", + "version": "0.14.2", "description": "FastBridge contracts.", "private": true, "files": [ From 748a3a27b0181dfe949f2964b49d67cb825238f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CF=87=C2=B2?= <88190723+ChiTimesChi@users.noreply.github.com> Date: Sat, 23 Nov 2024 21:06:41 +0000 Subject: [PATCH 07/12] build(contracts-rfq): increase optimizer runs to 1M [SLT-294] (#3407) --- packages/contracts-rfq/foundry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/contracts-rfq/foundry.toml b/packages/contracts-rfq/foundry.toml index 5bca86282a..62a64ec29e 100644 --- a/packages/contracts-rfq/foundry.toml +++ b/packages/contracts-rfq/foundry.toml @@ -2,6 +2,7 @@ # 2024-01-01 block_timestamp = 1_704_067_200 evm_version = "paris" +optimizer_runs = 1_000_000 src = 'contracts' out = 'out' libs = ["lib", "node_modules"] From 2e16814c9ec38477e722b838229a18505ddacb63 Mon Sep 17 00:00:00 2001 From: ChiTimesChi Date: Sat, 23 Nov 2024 21:11:43 +0000 Subject: [PATCH 08/12] Publish - @synapsecns/contracts-rfq@0.14.3 --- packages/contracts-rfq/CHANGELOG.md | 8 ++++++++ packages/contracts-rfq/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/CHANGELOG.md b/packages/contracts-rfq/CHANGELOG.md index bbf16c10d6..04ff871412 100644 --- a/packages/contracts-rfq/CHANGELOG.md +++ b/packages/contracts-rfq/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.14.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.2...@synapsecns/contracts-rfq@0.14.3) (2024-11-23) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + ## [0.14.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.1...@synapsecns/contracts-rfq@0.14.2) (2024-11-22) **Note:** Version bump only for package @synapsecns/contracts-rfq diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json index f329a66a83..959310415f 100644 --- a/packages/contracts-rfq/package.json +++ b/packages/contracts-rfq/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/contracts-rfq", "license": "MIT", - "version": "0.14.2", + "version": "0.14.3", "description": "FastBridge contracts.", "private": true, "files": [ From b59c9d0a98cbe1982a28ff4d43c6cb97c7adddce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CF=87=C2=B2?= <88190723+ChiTimesChi@users.noreply.github.com> Date: Sun, 24 Nov 2024 18:17:14 +0000 Subject: [PATCH 09/12] refactor(contracts-rfq): reorganise imports (#3409) * refactor: separate imports * refactor: sort imports * refactor: imports in test files --- packages/contracts-rfq/contracts/Admin.sol | 2 +- packages/contracts-rfq/contracts/AdminV2.sol | 6 +++++- packages/contracts-rfq/contracts/FastBridge.sol | 2 +- packages/contracts-rfq/contracts/FastBridgeV2.sol | 11 ++++++++--- .../contracts-rfq/contracts/libs/UniversalToken.sol | 2 +- packages/contracts-rfq/contracts/zaps/TokenZapV1.sol | 9 ++++++++- packages/contracts-rfq/foundry.toml | 1 + packages/contracts-rfq/test/FastBridgeMock.sol | 3 ++- .../test/FastBridgeV2.Src.RefundV1.t.sol | 2 +- packages/contracts-rfq/test/FastBridgeV2.t.sol | 4 ++-- packages/contracts-rfq/test/MulticallTarget.t.sol | 2 +- packages/contracts-rfq/test/UniversalTokenLib.t.sol | 3 ++- .../test/integration/FastBridge.MulticallTarget.t.sol | 2 +- .../integration/FastBridgeV2.MulticallTarget.t.sol | 2 +- .../integration/FastBridgeV2.TokenZapV1.Dst.t.sol | 2 +- .../integration/FastBridgeV2.TokenZapV1.Src.t.sol | 2 +- .../contracts-rfq/test/integration/TokenZapV1.t.sol | 2 +- packages/contracts-rfq/test/libs/ZapDataV1.t.sol | 2 +- packages/contracts-rfq/test/mocks/VaultMock.sol | 2 +- .../contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol | 2 +- packages/contracts-rfq/test/zaps/TokenZapV1.t.sol | 2 +- 21 files changed, 42 insertions(+), 23 deletions(-) diff --git a/packages/contracts-rfq/contracts/Admin.sol b/packages/contracts-rfq/contracts/Admin.sol index ffb352b28a..b6617d262c 100644 --- a/packages/contracts-rfq/contracts/Admin.sol +++ b/packages/contracts-rfq/contracts/Admin.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.20; import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; -import {UniversalTokenLib} from "./libs/UniversalToken.sol"; import {IAdmin} from "./interfaces/IAdmin.sol"; +import {UniversalTokenLib} from "./libs/UniversalToken.sol"; contract Admin is IAdmin, AccessControlEnumerable { using UniversalTokenLib for address; diff --git a/packages/contracts-rfq/contracts/AdminV2.sol b/packages/contracts-rfq/contracts/AdminV2.sol index b1ab948ef0..64806426c3 100644 --- a/packages/contracts-rfq/contracts/AdminV2.sol +++ b/packages/contracts-rfq/contracts/AdminV2.sol @@ -1,11 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +// ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ + import {IAdminV2} from "./interfaces/IAdminV2.sol"; import {IAdminV2Errors} from "./interfaces/IAdminV2Errors.sol"; +// ═════════════════════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════════════════════════ + import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; /// @title AdminV2 diff --git a/packages/contracts-rfq/contracts/FastBridge.sol b/packages/contracts-rfq/contracts/FastBridge.sol index 68966aa0e0..2386f8500f 100644 --- a/packages/contracts-rfq/contracts/FastBridge.sol +++ b/packages/contracts-rfq/contracts/FastBridge.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.20; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./libs/Errors.sol"; import {UniversalTokenLib} from "./libs/UniversalToken.sol"; diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol index 08fe907507..3ef400f6e9 100644 --- a/packages/contracts-rfq/contracts/FastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -1,17 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import {BridgeTransactionV2Lib} from "./libs/BridgeTransactionV2.sol"; +// ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ -import {AdminV2} from "./AdminV2.sol"; import {IFastBridge} from "./interfaces/IFastBridge.sol"; import {IFastBridgeV2} from "./interfaces/IFastBridgeV2.sol"; import {IFastBridgeV2Errors} from "./interfaces/IFastBridgeV2Errors.sol"; import {IZapRecipient} from "./interfaces/IZapRecipient.sol"; +// ═════════════════════════════════════════════ INTERNAL IMPORTS ══════════════════════════════════════════════════ + +import {AdminV2} from "./AdminV2.sol"; +import {BridgeTransactionV2Lib} from "./libs/BridgeTransactionV2.sol"; import {MulticallTarget} from "./utils/MulticallTarget.sol"; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +// ═════════════════════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════════════════════════ + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; /// @title FastBridgeV2 diff --git a/packages/contracts-rfq/contracts/libs/UniversalToken.sol b/packages/contracts-rfq/contracts/libs/UniversalToken.sol index c57bf141e0..935cfb1cd2 100644 --- a/packages/contracts-rfq/contracts/libs/UniversalToken.sol +++ b/packages/contracts-rfq/contracts/libs/UniversalToken.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {TokenNotContract} from "./Errors.sol"; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; library UniversalTokenLib { using SafeERC20 for IERC20; diff --git a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol index a17d3d9bb1..a754c55f57 100644 --- a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol +++ b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol @@ -1,11 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; +// ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ + import {IZapRecipient} from "../interfaces/IZapRecipient.sol"; + +// ═════════════════════════════════════════════ INTERNAL IMPORTS ══════════════════════════════════════════════════ + import {ZapDataV1} from "../libs/ZapDataV1.sol"; +// ═════════════════════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════════════════════════ + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /// @title TokenZapV1 /// @notice Facilitates atomic token operations known as "Zaps," allowing to execute predefined actions diff --git a/packages/contracts-rfq/foundry.toml b/packages/contracts-rfq/foundry.toml index 62a64ec29e..48f7927e13 100644 --- a/packages/contracts-rfq/foundry.toml +++ b/packages/contracts-rfq/foundry.toml @@ -17,6 +17,7 @@ ignore = ["contracts/legacy/**/*.sol"] line_length = 120 multiline_func_header = 'all' number_underscore = 'thousands' +sort_imports = true [rpc_endpoints] arbitrum = "${ARBITRUM_RPC}" diff --git a/packages/contracts-rfq/test/FastBridgeMock.sol b/packages/contracts-rfq/test/FastBridgeMock.sol index 87c660312d..68026550f5 100644 --- a/packages/contracts-rfq/test/FastBridgeMock.sol +++ b/packages/contracts-rfq/test/FastBridgeMock.sol @@ -2,8 +2,9 @@ pragma solidity ^0.8.20; import {Admin} from "../contracts/Admin.sol"; -import {IFastBridge} from "../contracts/interfaces/IFastBridge.sol"; + import {FastBridge} from "../contracts/FastBridge.sol"; +import {IFastBridge} from "../contracts/interfaces/IFastBridge.sol"; contract FastBridgeMock is IFastBridge, Admin { // @dev the block the contract was deployed at diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.RefundV1.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.RefundV1.t.sol index 3b26f83c55..44443a3920 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.RefundV1.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.RefundV1.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {FastBridgeV2SrcTest, BridgeTransactionV2Lib, IFastBridgeV2} from "./FastBridgeV2.Src.t.sol"; +import {BridgeTransactionV2Lib, FastBridgeV2SrcTest, IFastBridgeV2} from "./FastBridgeV2.Src.t.sol"; // solhint-disable func-name-mixedcase, ordering contract FastBridgeV2SrcRefundV1Test is FastBridgeV2SrcTest { diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.t.sol index 706f05d681..5a99259ad2 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.t.sol @@ -8,14 +8,14 @@ import {IFastBridge} from "../contracts/interfaces/IFastBridge.sol"; // solhint-disable-next-line no-unused-import import {IFastBridgeV2} from "../contracts/interfaces/IFastBridgeV2.sol"; -import {IFastBridgeV2Errors} from "../contracts/interfaces/IFastBridgeV2Errors.sol"; import {FastBridgeV2} from "../contracts/FastBridgeV2.sol"; +import {IFastBridgeV2Errors} from "../contracts/interfaces/IFastBridgeV2Errors.sol"; import {MockERC20} from "./MockERC20.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; import {Test} from "forge-std/Test.sol"; -import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {StdStorage, stdStorage} from "forge-std/Test.sol"; // solhint-disable no-empty-blocks, max-states-count, ordering abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { diff --git a/packages/contracts-rfq/test/MulticallTarget.t.sol b/packages/contracts-rfq/test/MulticallTarget.t.sol index 76141272c8..909c63463d 100644 --- a/packages/contracts-rfq/test/MulticallTarget.t.sol +++ b/packages/contracts-rfq/test/MulticallTarget.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.4; import {IMulticallTarget} from "../contracts/interfaces/IMulticallTarget.sol"; -import {MulticallTargetHarness, MulticallTarget} from "./harnesses/MulticallTargetHarness.sol"; +import {MulticallTarget, MulticallTargetHarness} from "./harnesses/MulticallTargetHarness.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/packages/contracts-rfq/test/UniversalTokenLib.t.sol b/packages/contracts-rfq/test/UniversalTokenLib.t.sol index 58bbd34435..ce46c08b19 100644 --- a/packages/contracts-rfq/test/UniversalTokenLib.t.sol +++ b/packages/contracts-rfq/test/UniversalTokenLib.t.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.17; import {TokenNotContract} from "../contracts/libs/Errors.sol"; -import {UniversalTokenLibHarness} from "./UniversalTokenLibHarness.sol"; + import {MockERC20} from "./MockERC20.sol"; import {MockRevertingRecipient} from "./MockRevertingRecipient.sol"; +import {UniversalTokenLibHarness} from "./UniversalTokenLibHarness.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/packages/contracts-rfq/test/integration/FastBridge.MulticallTarget.t.sol b/packages/contracts-rfq/test/integration/FastBridge.MulticallTarget.t.sol index 53c597136e..182b25f050 100644 --- a/packages/contracts-rfq/test/integration/FastBridge.MulticallTarget.t.sol +++ b/packages/contracts-rfq/test/integration/FastBridge.MulticallTarget.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.20; import {FastBridge} from "../../contracts/FastBridge.sol"; -import {MulticallTargetIntegrationTest, IFastBridge} from "./MulticallTarget.t.sol"; +import {IFastBridge, MulticallTargetIntegrationTest} from "./MulticallTarget.t.sol"; contract FastBridgeMulticallTargetTest is MulticallTargetIntegrationTest { function deployAndConfigureFastBridge() public override returns (address) { diff --git a/packages/contracts-rfq/test/integration/FastBridgeV2.MulticallTarget.t.sol b/packages/contracts-rfq/test/integration/FastBridgeV2.MulticallTarget.t.sol index f58dceb310..4a23e98169 100644 --- a/packages/contracts-rfq/test/integration/FastBridgeV2.MulticallTarget.t.sol +++ b/packages/contracts-rfq/test/integration/FastBridgeV2.MulticallTarget.t.sol @@ -4,7 +4,7 @@ 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"; +import {IFastBridge, MulticallTargetIntegrationTest} from "./MulticallTarget.t.sol"; contract FastBridgeV2MulticallTargetTest is MulticallTargetIntegrationTest { function deployAndConfigureFastBridge() public override returns (address) { diff --git a/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol index 9a32c1a259..487f344b5e 100644 --- a/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol +++ b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import {TokenZapV1IntegrationTest, VaultManyArguments, IFastBridge, IFastBridgeV2} from "./TokenZapV1.t.sol"; +import {IFastBridge, IFastBridgeV2, TokenZapV1IntegrationTest, VaultManyArguments} from "./TokenZapV1.t.sol"; // solhint-disable func-name-mixedcase, ordering contract FastBridgeV2TokenZapV1DstTest is TokenZapV1IntegrationTest { diff --git a/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol index 40541c6056..6ade6c7030 100644 --- a/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol +++ b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import {TokenZapV1IntegrationTest, IFastBridge, IFastBridgeV2} from "./TokenZapV1.t.sol"; +import {IFastBridge, IFastBridgeV2, TokenZapV1IntegrationTest} from "./TokenZapV1.t.sol"; // solhint-disable func-name-mixedcase, ordering contract FastBridgeV2TokenZapV1SrcTest is TokenZapV1IntegrationTest { diff --git a/packages/contracts-rfq/test/integration/TokenZapV1.t.sol b/packages/contracts-rfq/test/integration/TokenZapV1.t.sol index ddf34693d0..fec2ee9042 100644 --- a/packages/contracts-rfq/test/integration/TokenZapV1.t.sol +++ b/packages/contracts-rfq/test/integration/TokenZapV1.t.sol @@ -6,8 +6,8 @@ import {BridgeTransactionV2Lib} from "../../contracts/libs/BridgeTransactionV2.s import {ZapDataV1} from "../../contracts/libs/ZapDataV1.sol"; import {TokenZapV1} from "../../contracts/zaps/TokenZapV1.sol"; -import {VaultManyArguments} from "../mocks/VaultManyArguments.sol"; import {MockERC20} from "../MockERC20.sol"; +import {VaultManyArguments} from "../mocks/VaultManyArguments.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/packages/contracts-rfq/test/libs/ZapDataV1.t.sol b/packages/contracts-rfq/test/libs/ZapDataV1.t.sol index d93d9775bd..f592782ca8 100644 --- a/packages/contracts-rfq/test/libs/ZapDataV1.t.sol +++ b/packages/contracts-rfq/test/libs/ZapDataV1.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import {ZapDataV1Harness, ZapDataV1} from "../harnesses/ZapDataV1Harness.sol"; +import {ZapDataV1, ZapDataV1Harness} from "../harnesses/ZapDataV1Harness.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/packages/contracts-rfq/test/mocks/VaultMock.sol b/packages/contracts-rfq/test/mocks/VaultMock.sol index b23fd55a41..b4d1f514ec 100644 --- a/packages/contracts-rfq/test/mocks/VaultMock.sol +++ b/packages/contracts-rfq/test/mocks/VaultMock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /// @notice Vault mock for testing purposes. DO NOT USE IN PRODUCTION. abstract contract VaultMock { diff --git a/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol b/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol index bae47da888..3726b7d7b0 100644 --- a/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol +++ b/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.24; import {TokenZapV1} from "../../contracts/zaps/TokenZapV1.sol"; -import {SimpleVaultMock} from "../mocks/SimpleVaultMock.sol"; import {MockERC20} from "../MockERC20.sol"; +import {SimpleVaultMock} from "../mocks/SimpleVaultMock.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol index 7587131d3c..37a07838e2 100644 --- a/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol +++ b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol @@ -4,8 +4,8 @@ pragma solidity 0.8.24; import {ZapDataV1} from "../../contracts/libs/ZapDataV1.sol"; import {TokenZapV1} from "../../contracts/zaps/TokenZapV1.sol"; -import {VaultManyArguments} from "../mocks/VaultManyArguments.sol"; import {MockERC20} from "../MockERC20.sol"; +import {VaultManyArguments} from "../mocks/VaultManyArguments.sol"; import {Test} from "forge-std/Test.sol"; From f81e1283d08df0081fb6ee0baf3955a2fc9988a6 Mon Sep 17 00:00:00 2001 From: ChiTimesChi Date: Sun, 24 Nov 2024 18:21:24 +0000 Subject: [PATCH 10/12] Publish - @synapsecns/contracts-rfq@0.14.4 --- packages/contracts-rfq/CHANGELOG.md | 8 ++++++++ packages/contracts-rfq/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/CHANGELOG.md b/packages/contracts-rfq/CHANGELOG.md index 04ff871412..9479b963f3 100644 --- a/packages/contracts-rfq/CHANGELOG.md +++ b/packages/contracts-rfq/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.14.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.3...@synapsecns/contracts-rfq@0.14.4) (2024-11-24) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + ## [0.14.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.2...@synapsecns/contracts-rfq@0.14.3) (2024-11-23) **Note:** Version bump only for package @synapsecns/contracts-rfq diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json index 959310415f..e4fc3955e2 100644 --- a/packages/contracts-rfq/package.json +++ b/packages/contracts-rfq/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/contracts-rfq", "license": "MIT", - "version": "0.14.3", + "version": "0.14.4", "description": "FastBridge contracts.", "private": true, "files": [ From f6b319be7141f1b8d978459934614019ca69e6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CF=87=C2=B2?= <88190723+ChiTimesChi@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:24:44 +0000 Subject: [PATCH 11/12] refactor(contracts-rfq): mark new methods as `V2` (#3410) * refactor: mark new methods as V2 * refactor: update tests --- .../contracts-rfq/contracts/FastBridgeV2.sol | 18 +++++++++--------- .../contracts/interfaces/IFastBridgeV2.sol | 10 +++++----- .../test/FastBridgeV2.Dst.Base.t.sol | 2 +- .../contracts-rfq/test/FastBridgeV2.Dst.t.sol | 6 +++--- .../test/FastBridgeV2.Src.Base.t.sol | 8 ++++---- .../contracts-rfq/test/FastBridgeV2.Src.t.sol | 12 ++++++------ .../FastBridgeV2.TokenZapV1.Src.t.sol | 2 +- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol index 3ef400f6e9..f5e9cbd363 100644 --- a/packages/contracts-rfq/contracts/FastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -65,7 +65,7 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E /// @inheritdoc IFastBridge function bridge(BridgeParams memory params) external payable { - bridge({ + bridgeV2({ params: params, paramsV2: BridgeParamsV2({ quoteRelayer: address(0), @@ -81,7 +81,7 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E /// @dev Replaced by `cancel`. /// @inheritdoc IFastBridge function refund(bytes calldata request) external { - cancel(request); + cancelV2(request); } // ══════════════════════════════════════ EXTERNAL MUTABLE (AGENT FACING) ══════════════════════════════════════════ @@ -89,17 +89,17 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E /// @inheritdoc IFastBridge function relay(bytes calldata request) external payable { // `relay` override will validate the request. - relay({request: request, relayer: msg.sender}); + relayV2({request: request, relayer: msg.sender}); } /// @inheritdoc IFastBridge function prove(bytes calldata request, bytes32 destTxHash) external { request.validateV2(); - prove({transactionId: keccak256(request), destTxHash: destTxHash, relayer: msg.sender}); + proveV2({transactionId: keccak256(request), destTxHash: destTxHash, relayer: msg.sender}); } /// @inheritdoc IFastBridgeV2 - function claim(bytes calldata request) external { + function claimV2(bytes calldata request) external { // `claim` override will validate the request. claim({request: request, to: address(0)}); } @@ -177,7 +177,7 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E // ═══════════════════════════════════════ PUBLIC MUTABLE (USER FACING) ════════════════════════════════════════════ /// @inheritdoc IFastBridgeV2 - function bridge(BridgeParams memory params, BridgeParamsV2 memory paramsV2) public payable { + function bridgeV2(BridgeParams memory params, BridgeParamsV2 memory paramsV2) public payable { // If relayer exclusivity is not intended for this bridge, set exclusivityEndTime to static zero. // Otherwise, set exclusivity to expire at the current block ts offset by quoteExclusivitySeconds. int256 exclusivityEndTime = 0; @@ -240,7 +240,7 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E } /// @inheritdoc IFastBridgeV2 - function cancel(bytes calldata request) public { + function cancelV2(bytes calldata request) public { // Decode the request and check that it could be cancelled. request.validateV2(); bytes32 transactionId = keccak256(request); @@ -278,7 +278,7 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E // ═══════════════════════════════════════ PUBLIC MUTABLE (AGENT FACING) ═══════════════════════════════════════════ /// @inheritdoc IFastBridgeV2 - function relay(bytes calldata request, address relayer) public payable { + function relayV2(bytes calldata request, address relayer) public payable { // Decode the request and check that it could be relayed. request.validateV2(); bytes32 transactionId = keccak256(request); @@ -347,7 +347,7 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E } /// @inheritdoc IFastBridgeV2 - function prove(bytes32 transactionId, bytes32 destTxHash, address relayer) public onlyRole(PROVER_ROLE) { + function proveV2(bytes32 transactionId, bytes32 destTxHash, address relayer) public onlyRole(PROVER_ROLE) { // Can only prove a REQUESTED transaction. BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; if ($.status != BridgeStatus.REQUESTED) revert StatusIncorrect(); diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol index bf60539d10..141b3808a7 100644 --- a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol @@ -70,28 +70,28 @@ interface IFastBridgeV2 is IFastBridge { /// to provide temporary exclusivity fill rights for the quote relayer. /// @param params The parameters required to bridge /// @param paramsV2 The parameters for exclusivity fill rights (optional, can be left empty) - function bridge(BridgeParams memory params, BridgeParamsV2 memory paramsV2) external payable; + function bridgeV2(BridgeParams memory params, BridgeParamsV2 memory paramsV2) external payable; /// @notice Relays destination side of bridge transaction by off-chain relayer /// @param request The encoded bridge transaction to relay on destination chain /// @param relayer The address of the relaying entity which should have control of the origin funds when claimed - function relay(bytes memory request, address relayer) external payable; + function relayV2(bytes memory request, address relayer) external payable; /// @notice Provides proof on origin side that relayer provided funds on destination side of bridge transaction /// @param transactionId The transaction id associated with the encoded bridge transaction to prove /// @param destTxHash The destination tx hash proving bridge transaction was relayed /// @param relayer The address of the relaying entity which should have control of the origin funds when claimed - function prove(bytes32 transactionId, bytes32 destTxHash, address relayer) external; + function proveV2(bytes32 transactionId, bytes32 destTxHash, address relayer) external; /// @notice Completes bridge transaction on origin chain by claiming originally deposited capital. /// @notice Can only send funds to the relayer address on the proof. /// @param request The encoded bridge transaction to claim on origin chain - function claim(bytes memory request) external; + function claimV2(bytes memory request) external; /// @notice Cancels an outstanding bridge transaction in case optimistic bridging failed and returns the full amount /// to the original sender. /// @param request The encoded bridge transaction to refund - function cancel(bytes memory request) external; + function cancelV2(bytes memory request) external; /// @notice Checks if a transaction has been relayed /// @param transactionId The ID of the transaction to check diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol index 42dac15820..93a7605c4a 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol @@ -50,6 +50,6 @@ contract FastBridgeV2DstBaseTest is FastBridgeV2Test { { bytes memory request = BridgeTransactionV2Lib.encodeV2(bridgeTx); vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.relay{value: msgValue}(request, relayer); + fastBridge.relayV2{value: msgValue}(request, relayer); } } diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol index 6028e751d2..7a4148ca8c 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol @@ -453,19 +453,19 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { // V1 doesn't have any version field expectRevertUnsupportedVersion(0); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.relay(mockRequestV1, relayerB); + fastBridge.relayV2(mockRequestV1, relayerB); } function test_relay_withRelayerAddress_revert_invalidRequestV2() public { expectRevertInvalidEncodedTx(); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.relay(invalidRequestV2, relayerB); + fastBridge.relayV2(invalidRequestV2, relayerB); } function test_relay_withRelayerAddress_revert_requestV3() public { expectRevertUnsupportedVersion(3); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.relay(mockRequestV3, relayerB); + fastBridge.relayV2(mockRequestV3, relayerB); } function test_relay_withRelayerAddress_revert_chainIncorrect() public { diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol index 2d655aa49d..54853731ef 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol @@ -68,12 +68,12 @@ abstract contract FastBridgeV2SrcBaseTest is FastBridgeV2Test { public { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.bridge{value: msgValue}(params, paramsV2); + fastBridge.bridgeV2{value: msgValue}(params, paramsV2); } function prove(address caller, bytes32 transactionId, bytes32 destTxHash, address relayer) public { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.prove(transactionId, destTxHash, relayer); + fastBridge.proveV2(transactionId, destTxHash, relayer); } function prove(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx, bytes32 destTxHash) public { @@ -83,7 +83,7 @@ abstract contract FastBridgeV2SrcBaseTest is FastBridgeV2Test { function claim(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.claim(BridgeTransactionV2Lib.encodeV2(bridgeTx)); + fastBridge.claimV2(BridgeTransactionV2Lib.encodeV2(bridgeTx)); } function claim(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx, address to) public { @@ -98,7 +98,7 @@ abstract contract FastBridgeV2SrcBaseTest is FastBridgeV2Test { function cancel(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public virtual { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.cancel(BridgeTransactionV2Lib.encodeV2(bridgeTx)); + fastBridge.cancelV2(BridgeTransactionV2Lib.encodeV2(bridgeTx)); } function test_nonce() public view { diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol index 539fe94c4d..5b3db7e3a9 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -998,19 +998,19 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { // V1 doesn't have any version field expectRevertUnsupportedVersion(0); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.claim(mockRequestV1); + fastBridge.claimV2(mockRequestV1); } function test_claim_revert_invalidRequestV2() public { expectRevertInvalidEncodedTx(); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.claim(invalidRequestV2); + fastBridge.claimV2(invalidRequestV2); } function test_claim_revert_requestV3() public { expectRevertUnsupportedVersion(3); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.claim(mockRequestV3); + fastBridge.claimV2(mockRequestV3); } function test_claim_toDiffAddress_revert_requestV1() public { @@ -1036,18 +1036,18 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { // V1 doesn't have any version field expectRevertUnsupportedVersion(0); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.cancel(mockRequestV1); + fastBridge.cancelV2(mockRequestV1); } function test_cancel_revert_invalidRequestV2() public { expectRevertInvalidEncodedTx(); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.cancel(invalidRequestV2); + fastBridge.cancelV2(invalidRequestV2); } function test_cancel_revert_requestV3() public { expectRevertUnsupportedVersion(3); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.cancel(mockRequestV3); + fastBridge.cancelV2(mockRequestV3); } } diff --git a/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol index 6ade6c7030..9b408f5669 100644 --- a/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol +++ b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol @@ -37,7 +37,7 @@ contract FastBridgeV2TokenZapV1SrcTest is TokenZapV1IntegrationTest { public { vm.prank({msgSender: user, txOrigin: user}); - fastBridge.bridge{value: isToken ? 0 : SRC_AMOUNT}(params, paramsV2); + fastBridge.bridgeV2{value: isToken ? 0 : SRC_AMOUNT}(params, paramsV2); } function expectEventBridgeRequested( From 91dee921b435ca6a457a00267141f7549baafc6b Mon Sep 17 00:00:00 2001 From: ChiTimesChi Date: Mon, 25 Nov 2024 13:29:05 +0000 Subject: [PATCH 12/12] Publish - @synapsecns/contracts-rfq@0.14.5 --- packages/contracts-rfq/CHANGELOG.md | 8 ++++++++ packages/contracts-rfq/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/CHANGELOG.md b/packages/contracts-rfq/CHANGELOG.md index 9479b963f3..dd82aa77c9 100644 --- a/packages/contracts-rfq/CHANGELOG.md +++ b/packages/contracts-rfq/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.14.5](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.4...@synapsecns/contracts-rfq@0.14.5) (2024-11-25) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + ## [0.14.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.3...@synapsecns/contracts-rfq@0.14.4) (2024-11-24) **Note:** Version bump only for package @synapsecns/contracts-rfq diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json index e4fc3955e2..16fa86d1b1 100644 --- a/packages/contracts-rfq/package.json +++ b/packages/contracts-rfq/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/contracts-rfq", "license": "MIT", - "version": "0.14.4", + "version": "0.14.5", "description": "FastBridge contracts.", "private": true, "files": [