We express our gratitude to the Unizen team for the collaborative engagement that enabled the execution of this Smart Contract Security Assessment.
Unizen is a next-generation DEX aggregator, offering developers, traders, and businesses the ability to unlock unparalleled token swap capabilities.
Document
Review Scope
The system users should acknowledge all the risks summed up in the risks section of the report
Functional requirements are detailed.
Project overview is detailed
All roles in the system are described.
Use cases are described and detailed.
For each contract, all futures are described.
All interactions are described.
Technical description is clear and concise.
Run instructions are provided.
Technical specification is provided.
The NatSpec documentation is sufficient.
The development environment is configured.
Code coverage of the project is approximately 60.00% (branch coverage).
Deployment and basic user interactions are covered with tests.
Negative cases coverage is partially missed.
Interactions involving multiple users are tested.
Fork tests are present.
Not all branches are covered by tests.
The Unizen DEX Aggregator leverages a set of specialized smart contracts, optimizing decentralized token swaps with a focus on cross-chain interoperability and cost efficiency. Unizen enables seamless, omni-chain swaps across various blockchain networks through a modular contract architecture, aggregating liquidity from multiple interoperability providers, including LayerZero, Debridge, Meson, Stargate, and Thorchain. This architecture supports single-chain and cross-chain trades, utilizing optimized routes to minimize costs and execution time. Key roles include the Router, which orchestrates trade execution, Executors for specific swap tasks, and specialized contracts for cross-chain compatibility.
The files in the scope:
SingleChainExecutor.sol: Manages single-chain token swaps using multiple methods on a single blockchain.
GasLessExecutor.sol: Facilitates gasless transactions by validating signed user orders for trade execution without requiring user gas.
UnizenRouter.sol: Central router that initiates all trade executions, coordinates executors, and manages protocol fees.
UnizenBridgeExecutor.sol: Handles asset bridging across chains within Unizen’s ecosystem using LayerZero interoperability.
DebridgeMesonWormholeExecutor.sol: Executes cross-chain swaps via Debridge, Meson, and Wormhole protocols.
BaseExecutor.sol: Provides foundational swap, fee handling, and asset recovery functions for executors.
DexHelpers.sol: Maintains a whitelist of approved DEXes and functions that executors can access.
StargateThorchainExecutor.sol: Manages cross-chain swaps using Stargate and Thorchain protocols.
EthReceiver.sol: Simple contract to receive and hold ETH.
Controller.sol: Governs the protocol, managing contract pausing, whitelisting DEXes, and handling dev permissions.
CCTPBase.sol: Base contract for cross-chain messaging and asset transfers via Wormhole and Circle's CCTP.
IGaslessExecutor.sol: Defines the structure for gasless order execution, including UnizenGasLessOrder
struct to handle gasless swap details and an event for gasless swap completion.
IStargateThorchainExecutor.sol: Provides structures and functions for cross-chain swaps via Stargate and Thorchain, detailing swap parameters and events for tracking cross-chain swap status and UTXO operations.
IRouter.sol: Interface for UnizenRouter, managing token transfers and program execution, with specific structures for handling executors, token transfer permits, and executable programs.
IDebridgeMesonWormholeExecutor.sol: Specifies structures and functions for executing cross-chain swaps using Debridge, Meson, and Wormhole protocols, detailing swap events for each protocol’s operations.
ISingleChainExecutor.sol: Interface for single-chain swaps, defining structures for exact-in and exact-out swap types and functions for initiating these swaps with related events.
IWETH.sol: Interface for Wrapped ETH (WETH) functions, including deposit
, transfer
, and withdraw
methods to interact with the WETH token contract.
IDexHelpers.sol: Interface for DexHelpers, providing a function to verify if a given DEX and function selector is whitelisted for trading.
Types.sol: Defines key data structures (Integrator
, SwapCall
, ContractBalance
, Permit
) used across contracts to manage trade parameters, contract balances, and integrator fees.
IUniswapV2Pair.sol: Interface for Uniswap V2 pair interactions, specifically for executing token swaps within a Uniswap V2 liquidity pool.
ILayerZeroUserApplicationConfig.sol: Interface for configuring LayerZero messaging, allowing applications to set messaging versions, manage configurations, and resume message flow.
ILayerZeroReceiver.sol: Interface for receiving LayerZero cross-chain messages, enabling the contract to handle payloads from other chains through the lzReceive
function.
IStargateReceiver.sol: Interface for receiving tokens and executing swaps via Stargate protocol, handling messages with specific chain IDs, tokens, and payload data for cross-chain swaps.
UnizenRouter.sol
Owner
:
Can set executor and gasless executor status with setExecutor
.
Can set the Permit2
contract address with setPermit2
.
Can withdraw earned fees from executors using unizenWithdrawFee
.
Executor
:
Authorized to execute transactions through the execute
function.
Can call routerTransferTokens
to transfer tokens on behalf of users.
Can use routerTransferTokensPermit2
for token transfers under gasless conditions if it has been permitted by Permit2
.
Controller.sol
Owner
:
Assigns a developer management address via setDevMultiSign
.
Pauses and unpauses contract execution using adminPause
and adminUnPause
.
Dev
:
Can whitelist or remove DEX addresses via whiteListDex
and whiteListDexes
.
This role is given permissions to manage DEX verification lists and ensure only approved addresses can execute trades.
DexHelper.sol
Owner
:
Can set a developer for managing DEX whitelisting with setDev
.
Developer (Dev)
:
Can whitelist DEX addresses and their associated function selectors using whitelistDexes
.
Can remove DEX addresses from the whitelist through removeDexes
.
Can add or remove specific function selectors for any DEX with addOrRemoveFuncSign
.
BaseExecutor (Inherits in all Executors)
Owner
:
Updates the router and DexHelper contract addresses.
Manages token allowances by revoking approvals.
Withdraws earned fees from integrators with unizenWithdrawEarnedFee
.
Recovers assets mistakenly left in the contract via recoverAsset
.
UnizenRouter
:
Only Unizen Router can call unizenWithdrawEarnedFee
to withdraw earned fees across multiple tokens.
DebridgeMesonWormholeExecutor
Owner
:
Configures cross-chain parameters (DLN, Meson, Wormhole), including setting stable token statuses, relayer contracts, and integrators.
Updates contract configurations for Wormhole, including address mappings for supported tokens and integrators.
GasLessExecutor
Owner
:
Configures senders for gasless transactions with setSender
.
Can initialize contract settings including WETH and DexHelper.
UnizenRouter
:
Authorized to execute the swapGasLess
function for executing gasless swaps on behalf of users.
SingleChainExecutor
Owner
:
Configures contract parameters such as WETH and router address.
UnizenRouter
:
Authorized to execute swap
and swapExactOut
, allowing swaps and managing liquidity on a single chain.
StargateThorchainExecutor
Owner
:
Configures Stargate and Thorchain routers and manages valid destination addresses.
Sets destination addresses for supported chain IDs.
Manages stable token mappings and approvals across Stargate pools.
UnizenRouter
:
Authorized to execute swapSTG
and swapTC
for cross-chain swaps, allowing source and destination swaps across supported chains.
UnizenBridgeExecutor
Owner
:
Manages LayerZero endpoint configurations and cross-chain destination mappings.
Configures bridge tokens, their supported destinations, and corresponding mappings.
Sets fee rate and fee recipient addresses.
UnizenRouter
:
Authorizes deposits and token transfers via deposit
, enabling cross-chain bridging functionalities.
Scope Definition and Security Guarantees: The audit does not cover all code in the repository. Contracts outside the audit scope may introduce vulnerabilities, potentially impacting the overall security due to the interconnected nature of smart contracts.
System Reliance on External Contracts: The functioning of the system significantly relies on specific external contracts. Any flaws or vulnerabilities in these contracts adversely affect the audited project, potentially leading to security breaches or loss of funds.
Interactions with External DeFi Protocols: Dependence on external DeFi protocols inherits their risks and vulnerabilities. This might lead to direct financial losses if these protocols are exploited, indirectly affecting the audited project.
Owner's Unrestricted State Modification: The absence of restrictions on state variable modifications by the owner leads to arbitrary changes, affecting contract integrity and user trust, especially during critical operations.
Absence of Time-lock Mechanisms for Critical Operations: Without time-locks on critical operations, there is no buffer to review or revert potentially harmful actions, increasing the risk of rapid exploitation and irreversible changes.
Single Points of Failure and Control: The project is fully or partially centralized, introducing single points of failure and control. This centralization can lead to vulnerabilities in decision-making and operational processes, making the system more susceptible to targeted attacks or manipulation.
Administrative Key Control Risks: The digital contract architecture relies on administrative keys for critical operations. Centralized control over these keys presents a significant security risk, as compromise or misuse can lead to unauthorized actions or loss of funds.
Flexibility and Risk in Contract Upgrades: The project's contracts are upgradable, allowing the administrator to update the contract logic at any time. While this provides flexibility in addressing issues and evolving the project, it also introduces risks if upgrade processes are not properly managed or secured, potentially allowing for unauthorized changes that could compromise the project's integrity and security.
Absence of Upgrade Window Constraints: The contract suite allows for immediate upgrades without a mandatory review or waiting period, increasing the risk of rapid deployment of malicious or flawed code, potentially compromising the system's integrity and user assets.
Cross-Chain Execution Failures: Dependencies on external cross-chain protocols (e.g., Stargate, Thorchain) pose risks of execution failures, delays, or inconsistencies across chains.
Dex Whitelisting Risks: Dex whitelisting relies on trusted addresses and function selectors; any error in whitelisting or verification can lead to interactions with malicious DEX contracts.
User-Bypassed Fee Enforcement: Users are intended to interact with the Executor Router contract through the provided frontend, which ensures accurate fee inputs for both integrators and Unizen. However, since fees are part of the user-provided transaction parameters, there is a potential risk that users could bypass the frontend, craft custom transactions, and execute swaps without including the required fees. While Unizen's backend fetches the best swap opportunities to incentivize frontend usage, this bypass risk exists when users interact directly with the protocol using custom inputs.
Malicious or Misconfigured Executors: If the isExecutor
mappings are compromised or misconfigured, unauthorized executors could gain the ability to perform transactions using user permits or direct approvals. This risk enables malicious entities to execute unauthorized transfers of user funds directly to themselves without performing intended swaps, leading to potential financial loss and compromising contract integrity. Users should be cautious about the amounts they permit to the router, and strict controls must be maintained over executor authorization to mitigate this risk.
Code ― | Title | Status | Severity | |
---|---|---|---|---|
F-2024-7080 | Missing Validation of Permit User in _routerTransferTokens Function | Fixed | Critical | |
F-2024-6897 | Missing Initialization for Critical Variables Results in Contract Non-Functionality | Fixed | High | |
F-2024-6896 | Lack of Access Control in LayerZero Configuration Functions Allows Unauthorized Alteration of Protocol Settings | Fixed | High | |
F-2024-7099 | Exposed API Keys in Hardhat Configuration File | Mitigated | Medium | |
F-2024-7050 | Missing isETHTrade Logic in swapSimple Function | Fixed | Medium | |
F-2024-7004 | Lack of Nonce or Order Tracking in GasLessExecutor Allows Replay Attacks by Authorized Relayers | Fixed | Medium | |
F-2024-6910 | Lack of Call Success Check in Native Asset Withdrawal | Fixed | Medium | |
F-2024-6901 | Incorrect Parameter Usage and Ordering in setConfig and getConfig Functions | Fixed | Medium | |
F-2024-7103 | Hardcoded Gas Subtraction Leading to Potential Reverts and Unreliable Execution | Mitigated | Low | |
F-2024-7101 | Potential Fee Collection Failure Due to Discrepancies in Fee Tracking | Accepted | Low |
When auditing smart contracts, Hacken is using a risk-based approach that considers Likelihood, Impact, Exploitability and Complexity metrics to evaluate findings and score severities.
Reference on how risk scoring is done is available through the repository in our Github organization:
Severity
Description
Severity
Description
Severity
Description
Severity
Description
The "Potential Risks" section identifies issues that are not direct security vulnerabilities but could still affect the project’s performance, reliability, or user trust. These risks arise from design choices, architectural decisions, or operational practices that, while not immediately exploitable, may lead to problems under certain conditions. Additionally, potential risks can impact the quality of the audit itself, as they may involve external factors or components beyond the scope of the audit, leading to incomplete assessments or oversight of key areas. This section aims to provide a broader perspective on factors that could affect the project's long-term security, functionality, and the comprehensiveness of the audit findings.
The scope of the project includes the following smart contracts from the provided repository:
Scope Details
dependencies/Controller.sol
helpers/EthReceiver.sol
interfaces/IController.sol
interfaces/IDlnSource.sol
interfaces/ILayerZeroEndpoint.sol
interfaces/ILayerZeroReceiver.sol
interfaces/ILayerZeroUserApplicationConfig.sol
interfaces/IMeson.sol
interfaces/IStargateReceiver.sol
interfaces/IStargateRouter.sol
interfaces/ITcRouter.sol
libraries/DlnOrderLib.sol
libraries/wormhole/CCTPBase.sol
libraries/wormhole/interfaces/CCTPInterfaces/IMessageTransmitter.sol
libraries/wormhole/interfaces/CCTPInterfaces/IReceiver.sol
During the audit of Unizen, Hacken followed its methodology by performing fuzz-testing on the project's main functions. Due to the complex and dynamic interactions within the protocol, unexpected edge cases might arise. Therefore, it was important to use fuzz-testing to ensure that several system invariants hold true in all situations.
Fuzz-testing allows the input of many random data points into the system, helping to identify issues that regular testing might miss. A specific Echidna fuzzing suite was prepared for this task, and throughout the assessment, 45 invariants were tested over 30000 runs. This thorough testing ensured that the system works correctly even with unexpected or unusual inputs.
Invariant
testFuzzSetDLNConfigution():
Only the contract owner can set the DLN adapter address; when the owner sets a valid (non-zero) _dlnAdapter
, the contract's dlnAdapter
state variable is updated accordingly.Test Result
Run Count
Invariant
testFuzzSetDlnStableRevertsOnLengthMismatch():
The setDlnStable
function must revert when the lengths of stableTokens
and isActive
arrays do not match, ensuring array length consistency.Test Result
Run Count
Invariant
estFuzzSetDlnStableRevertsOnZeroToken():
The setDlnStable
function must revert if any address in the stableTokens
array is the zero address, ensuring only valid token addresses are registered.Test Result
Run Count
Invariant
testFuzzSetMesonConfigution():
Only the contract owner can set the Meson contract address; when the owner sets a valid (non-zero) _meson
, the contract's meson
state variable is updated accordingly.Test Result
Run Count
Invariant
testFuzzSetMesonConfigutionAccessControl():
Only the contract owner can set the Meson contract address; attempts by non-owner accounts to set the Meson address should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testFuzzSetMesonStableRevertsOnLengthMismatch():
The setMesonStable
function must revert when the lengths of stableTokens
and isActive
arrays do not match, ensuring array length consistency.Test Result
Run Count
Invariant
testFuzzSetMesonStableRevertsOnZeroToken():
The setMesonStable
function must revert if any address in the stableTokens
array is the zero address, ensuring only valid token addresses are registered.Test Result
Run Count
Invariant
testFuzzSetDLNConfigutionAccessControl():
Only the contract owner can set DLN configurations; attempts by non-owner accounts to set _dlnSource
and _dlnAdapter
should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testFuzzSetWormholeConfig():
Only the contract owner can set Wormhole configurations; when the owner sets valid (non-zero) _wormholeRelayer
, _wormhole
, _circleMessageTransmitter
, _circleTokenMessenger
, and _WormholeUSDC
, the contract's corresponding state variables are updated accordingly.Test Result
Run Count
Invariant
testFuzzSetWormholeConfigRevertsOnZeroAddress():
The setWormholeConfig
function must revert if any of the provided addresses are the zero address, ensuring only valid Wormhole configuration addresses are set.Test Result
Run Count
Invariant
testFuzzSetWormholeConfigAccessControl():
Only the contract owner can set Wormhole configurations; attempts by non-owner accounts to set Wormhole configurations should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testFuzzSetDexHelper():
Only the contract owner can set the DexHelper address; when the owner sets a valid _dexHelper
, the contract's dexHelper
state variable is updated accordingly.Test Result
Run Count
Invariant
testFuzzSetDexHelperAccessControl():
Only the contract owner can set the DexHelper address; attempts by non-owner accounts to set the DexHelper should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testFuzzSetDexHelperRevertsOnZeroAddress():
The setDexHelper
function must revert if any of the provided addresses are the zero address, ensuring only valid dexHelper addresses are set.Test Result
Run Count
Invariant
testFuzzSetRouter():
Only the contract owner can set the Unizen Router address; when the owner sets a valid _unizenRouter
, the contract's unizenRouter
state variable is updated accordingly.Test Result
Run Count
Invariant
testFuzzSetRouterAccessControl():
Only the contract owner can set the Unizen Router address; attempts by non-owner accounts to set the Unizen Router should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testFuzzSetRouterRevertsOnZeroAddress():
The setRouter
function must revert if any of the provided addresses are the zero address, ensuring only valid dexHelper addresses are set.Test Result
Run Count
Invariant
testFuzzSetDev():
Only the contract owner can set the developer address; when the owner sets a valid (non-zero) newDev
address, the dexHelper
's dev
state variable is updated accordingly.Test Result
Run Count
Invariant
testFuzzSetDevRevertsOnZeroAddress():
Setting the developer address to the zero address should revert with "DexHelper: newDev addresses cannot be zero", ensuring that dev
cannot be set to the zero address.Test Result
Run Count
Invariant
testFuzzSetDevAccessControl():
Only the contract owner can set the developer address; attempts by non-owner accounts to set the developer should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testFuzzRecoverAssetETH():
Only the contract owner can recover ETH by calling recoverAsset
with address(0)
; ensures that the owner's ETH balance increases by the recovered amount and the executor's ETH balance is zero after recovery.Test Result
Run Count
Invariant
testFuzzRecoverAssetAccessControl():
Only the contract owner can call recoverAsset
; attempts by non-owner accounts to recover assets should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testFuzzSetSender():
Only the contract owner can set a sender's enabled status; when the owner sets a valid (non-zero) _sender
with _isEnabled
, the contract's isValidSender
mapping is updated accordingly and unizenRouter
remains unchanged.Test Result
Run Count
Invariant
testFuzzSetSenderRevertsOnZeroAddress():
Setting the sender address to the zero address should revert with "GasLessExecutor: Sender address cannot be zero", ensuring that zero address cannot be set as a sender.Test Result
Run Count
Invariant
testFuzzSetSenderAccessControl():
Only the contract owner can set a sender's enabled status; attempts by non-owner accounts to set a sender should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testSetLZEndPointRevertsOnZeroAddress():
Setting the LayerZero Endpoint to the zero address should revert with "Unizen: Invalid LayerZero endpoint", ensuring that only valid LayerZero endpoints are set.Test Result
Run Count
Invariant
testSetRouterRevertsOnZeroAddress():
Setting the Router to the zero address should revert with "Unizen: Invalid Router address", ensuring that only valid Router addresses are set.Test Result
Run Count
Invariant
testSetDestAddrRevertsOnZeroBridgeExecutor():
Setting the Destination Address with a bridgeExecutor as the zero address should revert with "Unizen: Invalid bridge executor address", ensuring that only valid bridge executors are set.Test Result
Run Count
Invariant
testAddTokenRevertsOnZeroAddress()
: Adding a zero address as a token should revert with "Unizen: Invalid token address", ensuring that only valid token addresses are registered.Test Result
Run Count
Invariant
testSetFeeRevertsOnInvalidFee():
Setting the fee beyond the maximum allowed (e.g., >10000 basis points) should revert with "Unizen: Fee exceeds maximum", ensuring that fees are within acceptable limits.Test Result
Run Count
Invariant
testOnlyOwnerCanSetLZEndPoint():
Only the contract owner can set the LayerZero Endpoint; attempts by non-owner accounts to set the LayerZero Endpoint should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testOnlyOwnerCanAddToken():
Only the contract owner can add tokens; attempts by non-owner accounts to add tokens should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testOnlyOwnerCanRemoveToken():
Only the contract owner can remove tokens; attempts by non-owner accounts to remove tokens should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testOnlyOwnerCanSetFee():
Only the contract owner can set the fee; attempts by non-owner accounts to set the fee should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testOnlyOwnerCanSetDestAddr():
Only the contract owner can set the destination address; attempts by non-owner accounts to set the destination address should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testFuzzSetExecutor():
Only the contract owner can set an executor's validity and gasless status; when the owner sets a valid _executor
address with isValid
and _isGaslessExecutor
flags, the contract's isExecutor
and isGaslessExecutor
mappings are updated accordingly.Test Result
Run Count
Invariant
testFuzzSetExecutorAccessControl():
Only the contract owner can set an executor's validity and gasless status; attempts by non-owner accounts to set an executor should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testFuzzSetPermit2():
Only the contract owner can set the Permit2 contract address; when the owner sets a valid _permit2
address, the contract's PERMIT2
variable is updated accordingly.Test Result
Run Count
Invariant
testFuzzSetPermit2AccessControl():
Only the contract owner can set the Permit2 contract address; attempts by non-owner accounts to set Permit2 should revert with "Ownable: caller is not the owner".Test Result
Run Count
Invariant
testFuzzSetExecutorWithZeroAddress():
The contract allows setting an executor with the zero address; when the owner sets a zero address executor with isValid
and _isGaslessExecutor
flags, the contract's isExecutor
and isGaslessExecutor
mappings are updated accordingly.Test Result
Run Count
Invariant
testFuzzSetPermit2WithZeroAddress():
Only the contract owner can set the Permit2 contract address; when the owner sets Permit2 to the zero address, the contract's PERMIT2
variable is updated accordingly.Test Result
Run Count
Invariant
User swapExactOut Trade USDC to USDT
- This test verifies that when a user attempts to swap a maximum amount of USDC for an exact amount of USDT, the contract correctly increases the user's USDT balance by at least the specified expectedAmountOut
while ensuring the USDC spent does not exceed amountInMax
. It also checks that the transaction either succeeds under valid conditions or reverts gracefully when constraints like insufficient funds or invalid deadlines are met.Test Result
Run Count
Invariant
User swapExactIn Trade Native to USDT
- This test ensures that when a user swaps a specific amount of native tokens (e.g., ETH) for USDT, the user's native balance decreases appropriately and their USDT balance increases by at least the desired minimum amount. The test also verifies that transactions either execute successfully when all conditions are favorable or revert as expected due to issues like insufficient native balance or expired deadlines.Test Result
Run Count
Invariant
Permit2 User swapSimple Trade USDT to Native
- This test assesses the swapSimple function's ability to handle USDT to native token swaps using Permit2 for approvals. It ensures that the contract correctly processes Permit2 signatures to authorize token spending, updates user balances accurately upon successful swaps, and appropriately reverts transactions when encountering scenarios like excessive minimum output amounts, insufficient USDT balances, or invalid deadlinesTest Result
Run Count
The smart contracts in the scope of this audit could benefit from the introduction of automatic emergency actions for critical activities, such as unauthorized operations like ownership changes or proxy upgrades, as well as unexpected fund manipulations, including large withdrawals or minting events. Adding such mechanisms would enable the protocol to react automatically to unusual activity, ensuring that the contract remains secure and functions as intended.
To improve functionality, these emergency actions could be designed to trigger under specific conditions, such as:
Detecting changes to ownership or critical permissions.
Monitoring large or unexpected transactions and minting events.
Pausing operations when irregularities are identified.
These enhancements would provide an added layer of security, making the contract more robust and better equipped to handle unexpected situations while maintaining smooth operations.