Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

introduces a gas-optimized periphery contract for same-chain swaps (GenericSwapper v1.0.0) #710

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
29 changes: 29 additions & 0 deletions config/global.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,34 @@
"scroll": "https://transaction.safe.scroll.xyz/api",
"sei": "https://transaction.sei-safe.protofire.io/api",
"zksync": "https://safe-transaction-zksync.safe.global/api"
},

"nativeAddress": {
"mainnet": "0x0000000000000000000000000000000000000000",
"arbitrum": "0x0000000000000000000000000000000000000000",
"aurora": "0x0000000000000000000000000000000000000000",
"avalanche": "0x0000000000000000000000000000000000000000",
"base": "0x0000000000000000000000000000000000000000",
"blast": "0x0000000000000000000000000000000000000000",
"boba": "0x0000000000000000000000000000000000000000",
"bsc": "0x0000000000000000000000000000000000000000",
"celo": "0x471EcE3750Da237f93B8E339c536989b8978a438and",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix typo in Celo native address

The Celo native address has an erroneous "and" suffix which would make it an invalid address.

-    "celo": "0x471EcE3750Da237f93B8E339c536989b8978a438and",
+    "celo": "0x471EcE3750Da237f93B8E339c536989b8978a438"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"celo": "0x471EcE3750Da237f93B8E339c536989b8978a438and",
"celo": "0x471EcE3750Da237f93B8E339c536989b8978a438",

"fantom": "0x0000000000000000000000000000000000000000",
"fraxtal": "0x0000000000000000000000000000000000000000",
"fuse": "0x0000000000000000000000000000000000000000",
"gnosis": "0x0000000000000000000000000000000000000000",
"linea": "0x0000000000000000000000000000000000000000",
"mantle": "0x0000000000000000000000000000000000000000",
"metis": "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000",
"mode": "0x0000000000000000000000000000000000000000",
"moonbeam": "0x0000000000000000000000000000000000000000",
"moonriver": "0x0000000000000000000000000000000000000000",
"optimism": "0x0000000000000000000000000000000000000000",
"polygon": "0x0000000000000000000000000000000000000000",
"polygonzkevm": "0x0000000000000000000000000000000000000000",
"rootstock": "0x0000000000000000000000000000000000000000",
"scroll": "0x0000000000000000000000000000000000000000",
"sei": "0x0000000000000000000000000000000000000000",
"zksync": "0x0000000000000000000000000000000000000000"
}
}
12 changes: 12 additions & 0 deletions deployments/_deployments_log_file.json
Original file line number Diff line number Diff line change
Expand Up @@ -19858,6 +19858,18 @@
"VERIFIED": "true"
}
]
},
"staging": {
"1.0.1": [
{
"ADDRESS": "0xE871874D8AC30E8aCD0eC67529b4a5dDD73Bf0d6",
"OPTIMIZER_RUNS": "1000000",
"TIMESTAMP": "2024-07-10 10:13:56",
"CONSTRUCTOR_ARGS": "0x0000000000000000000000000000000000000000000000000000000000000000",
"SALT": "",
"VERIFIED": "true"
}
]
}
},
"celo": {
Expand Down
7 changes: 6 additions & 1 deletion deployments/bsc.diamond.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,13 @@
"Name": "GenericSwapFacet",
"Version": "1.0.0"
},
"0xf92D157a857d55B5DA987E35488e746EA85f1ca3": {
"0xA269cb81E6bBB86683558e449cb1bAFFdb155Bfc": {
"Name": "",
"Version": ""
},
"0xE871874D8AC30E8aCD0eC67529b4a5dDD73Bf0d6": {
"Name": "GenericSwapFacetV3",
"Version": "1.0.1"
}
},
"Periphery": {
Expand All @@ -81,6 +85,7 @@
"GasRebateDistributor": "",
"LiFuelFeeCollector": "0xc4f7A34b8d283f66925eF0f5CCdFC2AF3030DeaE",
"Receiver": "0x76EE0F8fb09047284B6ea89881595Fc6F5B09E12",
"ReceiverStargateV2": "",
"RelayerCelerIM": "",
"ServiceFeeCollector": "0xdF4257A2f61E5D432Dedbc9c65917C268323421d",
"TokenWrapper": "0x5215E9fd223BC909083fbdB2860213873046e45d"
Expand Down
3 changes: 2 additions & 1 deletion deployments/bsc.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"ThorSwapFacet": "0xa6aAe470E7B8E8916e692882A5db25bB40C398A7",
"AmarokFacetPacked": "0x7ac3EB2D191EBAb9E925CAbFD4F8155be066b3aa",
"MayanBridgeFacet": "0x5Ba4FeD1DAd2fD057A9f687B399B8e4cF2368214",
"MayanFacet": "0xd596C903d78870786c5DB0E448ce7F87A65A0daD"
"MayanFacet": "0xd596C903d78870786c5DB0E448ce7F87A65A0daD",
"GenericSwapFacetV3": "0xE871874D8AC30E8aCD0eC67529b4a5dDD73Bf0d6"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Version mismatch detected in deployed contract

The deployed GenericSwapFacetV3 contract has version 1.0.1 (as shown in the source code comment @custom:version 1.0.1), which does not match the expected version v1.0.0 mentioned in the PR title.

🔗 Analysis chain

Verify contract deployment and version

Please verify that the deployed GenericSwapFacetV3 contract at 0xE871874D8AC30E8aCD0eC67529b4a5dDD73Bf0d6 matches the expected version (v1.0.0) mentioned in the PR title.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify contract deployment and version

# Check if contract is deployed and verify its bytecode
echo "Checking contract deployment..."
cast code 0xE871874D8AC30E8aCD0eC67529b4a5dDD73Bf0d6 --rpc-url https://bsc-testnet.public.blastapi.io

# Search for version string in contract source
echo "Checking contract version..."
rg -l "1\.0\.0" src/

Length of output: 1485


Script:

#!/bin/bash
# Let's check the GenericSwapFacetV3 contract source and its version
echo "Searching for GenericSwapFacetV3 contract..."
fd "GenericSwapFacetV3" 

echo "Checking version in GenericSwapFacetV3..."
rg -A 5 "contract GenericSwapFacetV3" 

# Check if there are any version-related constants or variables
echo "Checking for version constants..."
rg "VERSION|version" --type solidity

Length of output: 2015


Script:

#!/bin/bash
# Let's check the contract source and deployment scripts for version information
echo "Checking GenericSwapFacetV3 source and related files..."
rg -p "version|VERSION" src/Facets/GenericSwapFacetV3.sol script/deploy/facets/DeployGenericSwapFacetV3.s.sol script/deploy/facets/UpdateGenericSwapFacetV3.s.sol

# Let's also check the deployment script content to understand deployment configuration
echo "Checking deployment script content..."
cat script/deploy/facets/DeployGenericSwapFacetV3.s.sol

Length of output: 1732

}
97 changes: 97 additions & 0 deletions docs/GenericSwapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# GenericSwapper

## Description

Gas-optimized periphery contract used for executing same-chain swaps (incl. an optional fee collection step)

## How To Use

This contract was designed to provide a heavily gas-optimized way to execute same-chain swaps. It will not emit any events and it can only use the LI.FI DEX Aggregator to execute these swaps. Other DEXs are not supported.
It will also not check if token approvals from the GenericSwapper to the LI.FI DEX Aggregator and to the FeeCollector exist. If such approvals are missing, they would have to be set first (see function below).

In order to still be able to trace transactions a `transactionId` will be appended to the calldata, separated from the actual calldata with a delimiter ('`0xdeadbeef`').

The contract has a number of specialized methods for various use cases:

This method is used to execute a single swap from ERC20 to ERC20

```solidity
/// @notice Performs a single swap from an ERC20 token to another ERC20 token
/// @param _receiver the address to receive the swapped tokens into (also excess tokens)
/// @param _minAmountOut the minimum amount of the final asset to receive
/// @param _swapData an object containing swap related data to perform swaps before bridging
function swapTokensSingleV3ERC20ToERC20(
address _receiver,
uint256 _minAmountOut,
LibSwap.SwapData calldata _swapData
)
```

This method is used to execute a single swap from ERC20 to native

```solidity
/// @notice Performs a single swap from an ERC20 token to the network's native token
/// @param _receiver the address to receive the swapped tokens into (also excess tokens)
/// @param _minAmountOut the minimum amount of the final asset to receive
/// @param _swapData an object containing swap related data to perform swaps before bridging
function swapTokensSingleV3ERC20ToNative(
address payable _receiver,
uint256 _minAmountOut,
LibSwap.SwapData calldata _swapData
)
```

This method is used to execute a single swap from native to ERC20

```solidity
/// @notice Performs a single swap from the network's native token to ERC20 token
/// @param _receiver the address to receive the swapped tokens into (also excess tokens)
/// @param _minAmountOut the minimum amount of the final asset to receive
/// @param _swapData an object containing swap related data to perform swaps before bridging
function swapTokensSingleV3NativeToERC20(
address payable _receiver,
uint256 _minAmountOut,
LibSwap.SwapData calldata _swapData
)
```

This method is used to execute a swap from ERC20 any other token (ERC20 or native) incl. a previous fee collection step

```solidity
/// @notice Performs multiple swaps in one transaction, starting with ERC20 and ending with native
/// @param _receiver the address to receive the swapped tokens into (also excess tokens)
/// @param _minAmountOut the minimum amount of the final asset to receive
/// @param _swapData an object containing swap related data to perform swaps before bridging
function swapTokensMultipleV3ERC20ToAny(
address payable _receiver,
uint256 _minAmountOut,
LibSwap.SwapData[] calldata _swapData
)

Comment on lines +61 to +70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect documentation for swapTokensMultipleV3ERC20ToAny

The NatSpec comment states "ending with native" but the function name and description suggest it can end with any token type (ERC20 or native).

-    /// @notice Performs multiple swaps in one transaction, starting with ERC20 and ending with native
+    /// @notice Performs multiple swaps in one transaction, starting with ERC20 and ending with any token (ERC20 or native)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// @notice Performs multiple swaps in one transaction, starting with ERC20 and ending with native
/// @param _receiver the address to receive the swapped tokens into (also excess tokens)
/// @param _minAmountOut the minimum amount of the final asset to receive
/// @param _swapData an object containing swap related data to perform swaps before bridging
function swapTokensMultipleV3ERC20ToAny(
address payable _receiver,
uint256 _minAmountOut,
LibSwap.SwapData[] calldata _swapData
)
/// @notice Performs multiple swaps in one transaction, starting with ERC20 and ending with any token (ERC20 or native)
/// @param _receiver the address to receive the swapped tokens into (also excess tokens)
/// @param _minAmountOut the minimum amount of the final asset to receive
/// @param _swapData an object containing swap related data to perform swaps before bridging
function swapTokensMultipleV3ERC20ToAny(
address payable _receiver,
uint256 _minAmountOut,
LibSwap.SwapData[] calldata _swapData
)

```

This method is used to execute a swap from native to ERC20 incl. a previous fee collection step

```solidity
/// @notice Performs multiple swaps in one transaction, starting with native and ending with ERC20
/// @param _receiver the address to receive the swapped tokens into (also excess tokens)
/// @param _minAmountOut the minimum amount of the final asset to receive
/// @param _swapData an object containing swap related data to perform swaps before bridging
function swapTokensMultipleV3NativeToERC20(
address payable _receiver,
uint256 _minAmountOut,
LibSwap.SwapData[] calldata _swapData
)
```

This method is used to set max approvals between the GenericSwapper and the DEXAggregator as well as the FeeCollector
(it can only be called by the registered admin address)

```solidity
/// @notice (Re-)Sets max approvals from this contract to DEX Aggregator and FeeCollector
/// @param _approvals The information which approvals to set for which token
function setApprovalForTokens(
TokenApproval[] calldata _approvals
)

```
28 changes: 27 additions & 1 deletion script/deploy/facets/DeployGenericSwapFacetV3.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,39 @@ pragma solidity ^0.8.17;

import { DeployScriptBase } from "./utils/DeployScriptBase.sol";
import { GenericSwapFacetV3 } from "lifi/Facets/GenericSwapFacetV3.sol";
import { stdJson } from "forge-std/Script.sol";

contract DeployScript is DeployScriptBase {
using stdJson for string;

constructor() DeployScriptBase("GenericSwapFacetV3") {}

function run() public returns (GenericSwapFacetV3 deployed) {
function run()
public
returns (GenericSwapFacetV3 deployed, bytes memory constructorArgs)
{
constructorArgs = getConstructorArgs();

deployed = GenericSwapFacetV3(
deploy(type(GenericSwapFacetV3).creationCode)
);
}

function getConstructorArgs() internal override returns (bytes memory) {
// get path of global config file
string memory globalConfigPath = string.concat(
root,
"/config/global.json"
);

// read file into json variable
string memory globalConfigJson = vm.readFile(globalConfigPath);

// extract network's native address
address nativeAddress = globalConfigJson.readAddress(
string.concat(".nativeAddress.", network)
);
Comment on lines +34 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add validation for native address

The native address is critical for gas-optimized swaps and should be validated before deployment.

Apply this diff:

        address nativeAddress = globalConfigJson.readAddress(
            string.concat(".nativeAddress.", network)
        );
+       require(nativeAddress != address(0), "DeployGenericSwapFacetV3: invalid native address");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// extract network's native address
address nativeAddress = globalConfigJson.readAddress(
string.concat(".nativeAddress.", network)
);
// extract network's native address
address nativeAddress = globalConfigJson.readAddress(
string.concat(".nativeAddress.", network)
);
require(nativeAddress != address(0), "DeployGenericSwapFacetV3: invalid native address");


return abi.encode(nativeAddress);
}
}
37 changes: 37 additions & 0 deletions script/deploy/facets/DeployGenericSwapper.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import { DeployScriptBase } from "./utils/DeployScriptBase.sol";
import { stdJson } from "forge-std/Script.sol";
import { GenericSwapper } from "lifi/Periphery/GenericSwapper.sol";

contract DeployScript is DeployScriptBase {
using stdJson for string;

constructor() DeployScriptBase("GenericSwapper") {}

function run()
public
returns (GenericSwapper deployed, bytes memory constructorArgs)
{
constructorArgs = getConstructorArgs();

deployed = GenericSwapper(deploy(type(GenericSwapper).creationCode));
}

function getConstructorArgs() internal override returns (bytes memory) {
string memory path = string.concat(
root,
"/deployments/",
network,
".",
fileSuffix,
"json"
);
string memory json = vm.readFile(path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for JSON file reading

The vm.readFile call should be wrapped in a try-catch block to handle potential file reading errors gracefully.

Apply this diff:

-       string memory json = vm.readFile(path);
+       string memory json;
+       try vm.readFile(path) returns (string memory result) {
+           json = result;
+       } catch {
+           revert("DeployGenericSwapper: failed to read deployment config");
+       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
string memory json = vm.readFile(path);
string memory json;
try vm.readFile(path) returns (string memory result) {
json = result;
} catch {
revert("DeployGenericSwapper: failed to read deployment config");
}

address dexAggregatorAddress = json.readAddress(".LiFiDEXAggregator");
address feeCollectorAddress = json.readAddress(".FeeCollector");

Comment on lines +32 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add address validation checks

The constructor arguments should be validated to ensure they are non-zero addresses before deployment.

Apply this diff to add validation:

        string memory json = vm.readFile(path);
        address dexAggregatorAddress = json.readAddress(".LiFiDEXAggregator");
        address feeCollectorAddress = json.readAddress(".FeeCollector");
+       require(dexAggregatorAddress != address(0), "DeployGenericSwapper: zero dexAggregator address");
+       require(feeCollectorAddress != address(0), "DeployGenericSwapper: zero feeCollector address");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
address dexAggregatorAddress = json.readAddress(".LiFiDEXAggregator");
address feeCollectorAddress = json.readAddress(".FeeCollector");
address dexAggregatorAddress = json.readAddress(".LiFiDEXAggregator");
address feeCollectorAddress = json.readAddress(".FeeCollector");
require(dexAggregatorAddress != address(0), "DeployGenericSwapper: zero dexAggregator address");
require(feeCollectorAddress != address(0), "DeployGenericSwapper: zero feeCollector address");

return abi.encode(dexAggregatorAddress, feeCollectorAddress);
}
}
11 changes: 11 additions & 0 deletions script/deploy/resources/deployRequirements.json
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,17 @@
}
}
},
"GenericSwapper": {
"configData": {
"contractAddresses": {
"FeeCollector": {
"allowToDeployWithZeroAddress": "false"
},
"LiFiDEXAggregator": {
"allowToDeployWithZeroAddress": "false"
}
}
},
"FeeCollector": {
"configData": {
"_owner": {
Expand Down
26 changes: 16 additions & 10 deletions src/Facets/GenericSwapFacetV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,24 @@ import { LibAllowList } from "../Libraries/LibAllowList.sol";
import { LibAsset } from "../Libraries/LibAsset.sol";
import { ContractCallNotAllowed, CumulativeSlippageTooHigh, NativeAssetTransferFailed } from "../Errors/GenericErrors.sol";
import { ERC20, SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";
import { console2 } from "forge-std/console2.sol";

/// @title GenericSwapFacetV3
/// @author LI.FI (https://li.fi)
/// @notice Provides gas-optimized functionality for fee collection and for swapping through any APPROVED DEX
/// @dev Can only execute calldata for APPROVED function selectors
/// @custom:version 1.0.0
/// @custom:version 1.0.1
contract GenericSwapFacetV3 is ILiFi {
using SafeTransferLib for ERC20;

/// Storage
address public immutable NATIVE_ADDRESS;

/// Constructor
/// @param _nativeAddress the address of the native token for this network
constructor(address _nativeAddress) {
NATIVE_ADDRESS = _nativeAddress;
}

/// External Methods ///

// SINGLE SWAPS
Expand Down Expand Up @@ -114,7 +122,7 @@ contract GenericSwapFacetV3 is ILiFi {
_transactionId,
_swapData.callTo,
sendingAssetId,
address(0),
NATIVE_ADDRESS,
fromAmount,
amountReceived,
block.timestamp
Expand All @@ -126,7 +134,7 @@ contract GenericSwapFacetV3 is ILiFi {
_referrer,
_receiver,
sendingAssetId,
address(0),
NATIVE_ADDRESS,
fromAmount,
amountReceived
);
Expand Down Expand Up @@ -183,7 +191,7 @@ contract GenericSwapFacetV3 is ILiFi {
emit LibSwap.AssetSwapped(
_transactionId,
callTo,
address(0),
NATIVE_ADDRESS,
receivingAssetId,
fromAmount,
amountReceived,
Expand All @@ -195,7 +203,7 @@ contract GenericSwapFacetV3 is ILiFi {
_integrator,
_referrer,
_receiver,
address(0),
NATIVE_ADDRESS,
receivingAssetId,
fromAmount,
amountReceived
Expand Down Expand Up @@ -506,7 +514,6 @@ contract GenericSwapFacetV3 is ILiFi {
uint256 _minAmountOut,
LibSwap.SwapData[] calldata _swapData
) private {
console2.log("in _transferNativeTokensAndEmitEvent");
uint256 amountReceived = address(this).balance;

// make sure minAmountOut was received
Expand All @@ -517,7 +524,6 @@ contract GenericSwapFacetV3 is ILiFi {
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = _receiver.call{ value: amountReceived }("");
if (!success) {
console2.log("HEYA");
revert NativeAssetTransferFailed();
}

Expand All @@ -528,7 +534,7 @@ contract GenericSwapFacetV3 is ILiFi {
_referrer,
_receiver,
_swapData[0].sendingAssetId,
address(0),
NATIVE_ADDRESS,
_swapData[0].fromAmount,
amountReceived
);
Expand All @@ -540,7 +546,7 @@ contract GenericSwapFacetV3 is ILiFi {
address receiver
) private {
// if a balance exists in sendingAsset, it must be positive slippage
if (address(sendingAsset) != address(0)) {
if (address(sendingAsset) != NATIVE_ADDRESS) {
uint256 sendingAssetBalance = sendingAsset.balanceOf(
address(this)
);
Expand Down
Loading
Loading