Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions audit/auditLog.json
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,13 @@
"auditorGitHandle": "sujithsomraaj",
"auditReportPath": "./audit/reports/2025.10.15_FeeForwarder(v1.0.0).pdf",
"auditCommitHash": "5022c0aca332e2a066f1398e3456632679a836f9"
},
"audit20251020": {
"auditCompletedOn": "20.10.2025",
"auditedBy": "Burra Security",
"auditorGitHandle": "burrasec",
"auditReportPath": "./audit/reports/2025.10.20_EcoFacet(v1.1.0).pdf",
"auditCommitHash": "25a5b880bca3ea51f060787e30ccc2bdcd13d6f9"
}
},
"auditedContracts": {
Expand Down Expand Up @@ -485,7 +492,8 @@
"1.0.2": ["audit20250413", "audit20250508"]
},
"EcoFacet": {
"1.0.0": ["audit20251001"]
"1.0.0": ["audit20251001"],
"1.1.0": ["audit20251020"]
},
"DiamondCutFacet": {
"1.0.0": ["audit20250508"]
Expand Down Expand Up @@ -553,7 +561,8 @@
"1.0.6": ["audit20250508"]
},
"IEcoPortal": {
"1.0.0": ["audit20251001"]
"1.0.0": ["audit20251001"],
"1.1.0": ["audit20251020"]
},
"IGarden": {
"1.0.0": ["audit20250919"]
Expand Down
Binary file added audit/reports/2025.10.20_EcoFacet(v1.1.0).pdf
Binary file not shown.
64 changes: 33 additions & 31 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions deployments/_deployments_log_file.json
Original file line number Diff line number Diff line change
Expand Up @@ -43367,12 +43367,12 @@
"staging": {
"1.0.0": [
{
"ADDRESS": "0x9051a65F82C9Dc2Fb4400B9ED5A4A16938613739",
"ADDRESS": "0xe9cF0bad93090f26051CCCe9A15a5c7395635D35",
"OPTIMIZER_RUNS": "1000000",
"TIMESTAMP": "2025-10-10 16:08:01",
"TIMESTAMP": "2025-10-16 12:38:29",
"CONSTRUCTOR_ARGS": "0x000000000000000000000000399dbd5df04f83103f77a58cba2b7c4d3cdede97",
"SALT": "",
"VERIFIED": "false",
"VERIFIED": "true",
"ZK_SOLC_VERSION": ""
}
]
Expand Down
3 changes: 2 additions & 1 deletion deployments/optimism.diamond.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@
"Name": "AcrossFacetPackedV4",
"Version": "1.0.0"
},
"0x9051a65F82C9Dc2Fb4400B9ED5A4A16938613739": {
"0xe9cF0bad93090f26051CCCe9A15a5c7395635D35": {
"Name": "EcoFacet",
"Version": "1.0.0"
}
Expand All @@ -190,6 +190,7 @@
"ERC20Proxy": "0xF6d5cf7a12d89BC0fD34E27d2237875b564A6ADf",
"Executor": "0x23f882bA2fa54A358d8599465EB471f58Cc26751",
"FeeCollector": "0x7F8E9bEBd1Dea263A36a6916B99bd84405B9654a",
"FeeForwarder": "",
"GasZipPeriphery": "",
"LidoWrapper": "",
"LiFiDEXAggregator": "",
Expand Down
2 changes: 1 addition & 1 deletion deployments/optimism.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"Permit2Proxy": "0x808eb38763f3F51F9C47bc93Ef8d5aB7E6241F46",
"GasZipFacet": "0xfEeCe7B3e68B9cBeADB60598973704a776ac3ca1",
"MayanFacet": "0x59A1Bcaa32EdB1a233fEF945857529BBD6df247f",
"EcoFacet": "0x9051a65F82C9Dc2Fb4400B9ED5A4A16938613739",
"EcoFacet": "0xe9cF0bad93090f26051CCCe9A15a5c7395635D35",
"GlacisFacet": "0x36e1375B0755162d720276dFF6893DF02bd49225",
"AcrossFacetV4": "0x91559A75bd9045681265C77922b3cAeDB3D5120d",
"ReceiverAcrossV4": "0x1d5bD612Ce761060A4bEd77b606ab7e723D4E91E",
Expand Down
14 changes: 14 additions & 0 deletions docs/EcoFacet.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ The methods listed above take a variable labeled `_ecoData`. This data is specif
/// @param rewardDeadline Timestamp for reward claim eligibility
/// @param encodedRoute Encoded route data containing destination chain routing information
/// @param solanaATA Associated Token Account address for Solana bridging (bytes32)
/// @param refundRecipient Address that will receive refunds if the intent expires unfulfilled
struct EcoData {
bytes nonEVMReceiver;
address prover;
uint64 rewardDeadline;
bytes encodedRoute;
bytes32 solanaATA;
address refundRecipient;
}
```

Expand All @@ -46,12 +48,14 @@ The receiver address is specified differently depending on the destination chain
- Set `bridgeData.receiver` to the actual EVM receiver address
- Leave `nonEVMReceiver` empty (`""`)
- Leave `solanaATA` as `bytes32(0)`
- Set `refundRecipient` to the address that should receive refunds (typically the user's address)
- The contract validates that the receiver in the encoded route matches `bridgeData.receiver`

- **For Solana destination chain**:
- Set `bridgeData.receiver` to `NON_EVM_ADDRESS` constant (`0x11f111f111f111F111f111f111F111f111f111F1`)
- Provide the Solana address in `nonEVMReceiver` as bytes (base58 address encoded as bytes)
- Provide the Associated Token Account (ATA) address in `solanaATA` as bytes32
- Set `refundRecipient` to the address that should receive refunds (typically the user's address)
- The contract validates that `solanaATA` matches the ATA encoded in the route

Examples:
Expand All @@ -61,15 +65,23 @@ Examples:
bridgeData.receiver = 0x123...; // Actual EVM receiver address
ecoData.nonEVMReceiver = ""; // Empty bytes
ecoData.solanaATA = bytes32(0); // Zero for EVM chains
ecoData.refundRecipient = msg.sender; // User address for refunds

// EVM to Solana bridge
bridgeData.receiver = NON_EVM_ADDRESS; // Special constant
ecoData.nonEVMReceiver = solanaAddressBytes; // Solana address as bytes
ecoData.solanaATA = 0x8f37c499ccbb92...; // Solana ATA as bytes32
ecoData.refundRecipient = msg.sender; // User address for refunds
```

### Important Notes

- **Refund Recipient**: The `refundRecipient` parameter specifies where funds will be sent if the intent expires unfulfilled. This is particularly important when using proxy contracts (e.g., Permit2Proxy) as intermediaries. Always set this to the end user's address to ensure they can receive refunds, not the proxy contract address.

- **Duplicate Bridge Protection**: The contract prevents duplicate bridge calls with identical parameters. If you attempt to bridge with the same intent parameters twice, the second transaction will revert with an `IntentAlreadyFunded` error. This protects against accidental fund loss.

- **Positive Slippage**: When using `swapAndStartBridgeTokensViaEco`, any positive slippage from the swap (tokens received above `minAmount`) is automatically refunded to the user. Only the exact `minAmount` specified is bridged.

- **Native Token Bridging**: The EcoFacet contract does not support native token bridging. Only ERC20 token transfers are supported. Transactions will revert if native tokens are specified as the sending asset.

- **Fee Model**: Eco uses a fee-inclusive model where the fee is already deducted from the source amount:
Expand All @@ -96,6 +108,8 @@ ecoData.solanaATA = 0x8f37c499ccbb92...; // Solana ATA as bytes32

- **TRON Compatibility**: TRON is treated as EVM-compatible in the smart contract validation logic since it uses the same Route struct encoding as EVM chains. Only Solana requires special non-EVM handling with `nonEVMReceiver` and `solanaATA` parameters.

- **Solana ATA Validation**: For Solana bridges, the contract validates that the Associated Token Account (ATA) specified in `solanaATA` matches the ATA encoded in bytes 251-283 of the route. The ATA is derived from the user's wallet address and the SPL token mint address, not the user's wallet address directly.

## Swap Data

Some methods accept a `SwapData _swapData` parameter.
Expand Down
15 changes: 6 additions & 9 deletions script/demoScripts/demoEco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ interface IEcoQuoteRequest {
destinationToken: string
sourceAmount: string
funder: string
refundRecipient: string
recipient: string
}
contracts?: {
Expand All @@ -202,7 +201,6 @@ interface IEcoQuoteResponse {
sourceAmount: string
destinationAmount: string
funder: string
refundRecipient: string
recipient: string
fees: Array<{
name: string
Expand All @@ -216,7 +214,6 @@ interface IEcoQuoteResponse {
}>
deadline: number
estimatedFulfillTimeSec: number
// Optional route field that may be returned by the API
encodedRoute?: string
}
contracts: {
Expand Down Expand Up @@ -303,7 +300,6 @@ async function getEcoQuote(
destinationToken,
sourceAmount: amount.toString(),
funder: signerAddress,
refundRecipient: signerAddress,
recipient: recipientAddress,
},
}
Expand Down Expand Up @@ -552,11 +548,12 @@ async function main(args: {
}

const ecoData: EcoFacet.EcoDataStruct = {
nonEVMReceiver: nonEVMReceiverBytes, // Solana address as bytes or '0x' for EVM
prover: quote.data.contracts.prover, // Prover address from quote
rewardDeadline: BigInt(quote.data.quoteResponse.deadline), // Deadline from quote
encodedRoute: encodedRoute, // Encoded route information for the bridge
solanaATA: solanaATA, // ATA for Solana or zero for EVM chains
nonEVMReceiver: nonEVMReceiverBytes,
prover: quote.data.contracts.prover,
rewardDeadline: BigInt(quote.data.quoteResponse.deadline),
encodedRoute: encodedRoute,
solanaATA: solanaATA,
refundRecipient: signerAddress,
}

// === Ensure allowance ===
Expand Down
73 changes: 49 additions & 24 deletions src/Facets/EcoFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ import { InvalidConfig, InvalidReceiver } from "../Errors/GenericErrors.sol";
/// @title EcoFacet
/// @author LI.FI (https://li.fi)
/// @notice Provides functionality for bridging through Eco Protocol
/// @custom:version 1.0.0
/// @custom:version 1.1.0
contract EcoFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable, LiFiData {
/// Storage ///
/// Errors ///

error IntentAlreadyFunded();

/// Constants and Immutables ///

IEcoPortal public immutable PORTAL;
uint64 private immutable ECO_CHAIN_ID_TRON = 728126428;
uint64 private immutable ECO_CHAIN_ID_SOLANA = 1399811149;
uint64 private constant ECO_CHAIN_ID_TRON = 728126428;
uint64 private constant ECO_CHAIN_ID_SOLANA = 1399811149;

/// Constants ///

Expand Down Expand Up @@ -68,12 +72,14 @@ contract EcoFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable, LiFiData {
/// @param rewardDeadline Timestamp for reward claim eligibility
/// @param encodedRoute Encoded route data containing destination chain routing information
/// @param solanaATA Associated Token Account address for Solana bridging (bytes32)
/// @param refundRecipient Address that will receive refunds if the intent expires unfulfilled
struct EcoData {
bytes nonEVMReceiver;
address prover;
uint64 rewardDeadline;
bytes encodedRoute;
bytes32 solanaATA;
address refundRecipient;
}

/// Constructor ///
Expand Down Expand Up @@ -117,12 +123,6 @@ contract EcoFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable, LiFiData {
/// @param _bridgeData Bridge data containing core parameters
/// @param _swapData Array of swap data for source swaps
/// @param _ecoData Eco-specific parameters for the bridge
/// @dev IMPORTANT LIMITATION: For ERC20 tokens, positive slippage from pre-bridge swaps
/// may remain in the diamond contract. The intent amount is encoded in encodedRoute
/// (provided by Eco API), and the Portal only transfers the exact amount specified in minAmount.
/// If swaps produce more tokens than expected (positive slippage), only minAmount is transferred
/// to the Portal vault. Any excess remains in the diamond. This is a known limitation that can
/// be significant when bridging large amounts.
function swapAndStartBridgeTokensViaEco(
ILiFi.BridgeData memory _bridgeData,
LibSwap.SwapData[] calldata _swapData,
Expand All @@ -139,14 +139,23 @@ contract EcoFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable, LiFiData {
{
_validateEcoData(_bridgeData, _ecoData);

_bridgeData.minAmount = _depositAndSwap(
uint256 actualAmountAfterSwap = _depositAndSwap(
_bridgeData.transactionId,
_bridgeData.minAmount,
_swapData,
payable(msg.sender),
0
payable(msg.sender)
);

if (actualAmountAfterSwap > _bridgeData.minAmount) {
uint256 positiveSlippage = actualAmountAfterSwap -
_bridgeData.minAmount;
LibAsset.transferERC20(
_bridgeData.sendingAssetId,
payable(_ecoData.refundRecipient),
positiveSlippage
);
}

_startBridge(_bridgeData, _ecoData);
}

Expand All @@ -166,9 +175,7 @@ contract EcoFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable, LiFiData {

return
IEcoPortal.Reward({
// If and when native bridging is enabled ensure the creator
// address is able to receive ETH in the case of a refund
creator: msg.sender,
creator: _ecoData.refundRecipient,
prover: _ecoData.prover,
deadline: _ecoData.rewardDeadline,
nativeAmount: NATIVE_REWARD_AMOUNT,
Expand Down Expand Up @@ -200,6 +207,16 @@ contract EcoFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable, LiFiData {
destination = uint64(_bridgeData.destinationChainId);
}

bytes32 intentHash = _getIntentHash(
destination,
_ecoData.encodedRoute,
reward
);

if (PORTAL.getRewardStatus(intentHash) != IEcoPortal.Status.Initial) {
revert IntentAlreadyFunded();
}

LibAsset.maxApproveERC20(
IERC20(_bridgeData.sendingAssetId),
address(PORTAL),
Expand Down Expand Up @@ -229,10 +246,8 @@ contract EcoFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable, LiFiData {
EcoData calldata _ecoData
) private view {
if (_ecoData.prover == address(0)) revert InvalidConfig();
if (
_ecoData.rewardDeadline == 0 ||
_ecoData.rewardDeadline <= block.timestamp
) {
if (_ecoData.refundRecipient == address(0)) revert InvalidConfig();
if (_ecoData.rewardDeadline <= block.timestamp) {
revert InvalidConfig();
}

Expand Down Expand Up @@ -294,16 +309,16 @@ contract EcoFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable, LiFiData {
revert InvalidReceiver();
}

// Extract the Solana recipient address from a Borsh-encoded Route struct
// Extract the Associated Token Account (ATA) from the Borsh-encoded Route struct
// The Route struct contains TransferChecked instruction calldata where:
// - The entire Route struct is Borsh-serialized
// - Within the serialized Route, the TransferChecked instruction data is embedded
// - The recipient account (destination wallet) is located at bytes 251-283 (32 bytes)
// - The destination ATA address is located at bytes 251-283 (32 bytes)
// - This position is determined by the Route struct layout and the position of the
// recipient pubkey within the TransferChecked instruction calldata
// ATA pubkey within the TransferChecked instruction calldata
// - Borsh encoding preserves the exact byte positions for fixed-size fields like pubkeys
// - The total encoded route for Solana must be exactly 319 bytes
// Extract bytes 251-283 (32 bytes) which contain the recipient address
// Extract bytes 251-283 (32 bytes) which contain the destination ATA
bytes32 routeReceiver = bytes32(
_ecoData.encodedRoute[SOLANA_RECEIVER_OFFSET:SOLANA_RECEIVER_END]
);
Expand All @@ -313,4 +328,14 @@ contract EcoFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable, LiFiData {
revert InvalidReceiver();
}
}

function _getIntentHash(
uint64 destination,
bytes calldata route,
IEcoPortal.Reward memory reward
) private pure returns (bytes32) {
bytes32 routeHash = keccak256(route);
bytes32 rewardHash = keccak256(abi.encode(reward));
return keccak256(abi.encodePacked(destination, routeHash, rewardHash));
}
}
13 changes: 12 additions & 1 deletion src/Interfaces/IEcoPortal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ pragma solidity ^0.8.17;
/// @title IEcoPortal
/// @notice Interface for Eco Protocol Portal
/// @author LI.FI (https://li.fi)
/// @custom:version 1.0.0
/// @custom:version 1.1.0
interface IEcoPortal {
enum Status {
Initial,
Funded,
Withdrawn,
Refunded
}

struct TokenAmount {
address token;
uint256 amount;
Expand All @@ -26,4 +33,8 @@ interface IEcoPortal {
Reward calldata reward,
bool allowPartial
) external payable returns (bytes32 intentHash, address vault);

function getRewardStatus(
bytes32 intentHash
) external view returns (Status status);
}
Loading
Loading