Halborn Logo

icon

Entangle Trillion - Entangle Labs


Prepared by:

Halborn Logo

HALBORN

Last Updated 07/25/2024

Date of Engagement by: January 15th, 2024 - February 15th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

51

Critical

10

High

6

Medium

5

Low

7

Informational

23


Table of Contents

Introduction

Entangle's Trillion enable a solution to the liquidity problem, transforming yield-generating assets into composable LSD equivalents.

The Entangle Team engaged Halborn to conduct a security assessment on their smart contracts beginning on 01/15/2024 and ending on 02/13/2024. The security assessment was scoped to the smart contracts provided in the GitHub repository. Commit hashes and further details can be found in the Scope section of this report.

Assessment Summary

Halborn was provided 4 weeks for the engagement and assigned 1 full-time security engineer to review the security of the smart contracts in scope. The engineer is a blockchain and smart contract security experts with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols.

The purpose of the assessment is to:

  • Identify potential security issues within the smart contracts.

  • Ensure that smart contract functionality operates as intended.

In summary, Halborn identified some security that should be addressed by the Entangle Team:

  • Withdrawals Blocked Due to Incorrect Function Names: Functions within the Curve Convex contracts were misnamed, leading to blocked withdrawal operations.

  • Broken Stargate Deposit Flow: Missing allowances in the Stargate deposit flow failed to execute deposits correctly.

  • Broken Integration through Cosmos Addresses: Incorrect handling of Cosmos addresses led to failures in cross-chain functionalities.

  • Withdrawal Blockages in the sellForLP Function: The price being set to zero within the SynthFactory contract inadvertently blocked withdrawals.

  • Unfair Synth Minting Due to Broken Protocol Invariant: The lack of proper handling of LP token decimals led to an imbalance in synth minting processes.

  • Fund Loss Risk in BurnAndBridge Function: The absence of whitelisting and address validation exposed the contract to potential unauthorized actions and fund loss.

  • Lack of Access Control: Functions such as proposeBurnAndBridge lacked sufficient access controls, posing risks of unauthorized actions.

  • Unrestricted Access in Cross-Chain Liquidity Pool Functions: Certain functions exposed the contract to risks due to lack of stringent access controls.

  • Re-entrancy Vulnerability: The withdraw & SwapTo the function of the EntangleTestBridge Contract was susceptible to re-entrancy attacks, posing a significant risk of fund drainage.

  • Risk of Overflow/Underflow: Certain contracts were at risk due to unsafe type conversions and calculations.

  • DexWrapper Contract Allows Setting Slippage to Zero: This could lead to unfavorable trading conditions and potential losses.

  • Inadequate Slippage Control in Liquidity Withdrawal: The liquidity withdrawal process lacked adequate slippage controls, risking unfavorable asset liquidation.

  • Lack of Access Controls in Token Minting and Burning: The absence of stringent access controls in critical functions exposed the platform to unauthorized minting and burning of tokens.

Test Approach and Methodology

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the code and can quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the assessment:

  • Research into architecture and purpose.

  • Smart contract manual code review and walkthrough.

  • Graphing out functionality and contract logic/connectivity/functions (solgraph).

  • Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes.

  • Manual testing by custom scripts.

  • Static Analysis of security for scoped contract, and imported functions (slither).

  • Testnet deployment (Foundry).

1. RISK METHODOLOGY

Every vulnerability and issue observed by Halborn is ranked based on two sets of Metrics and a Severity Coefficient. This system is inspired by the industry standard Common Vulnerability Scoring System.
The two Metric sets are: Exploitability and Impact. Exploitability captures the ease and technical means by which vulnerabilities can be exploited and Impact describes the consequences of a successful exploit.
The Severity Coefficients is designed to further refine the accuracy of the ranking with two factors: Reversibility and Scope. These capture the impact of the vulnerability on the environment as well as the number of users and smart contracts affected.
The final score is a value between 0-10 rounded up to 1 decimal place and 10 corresponding to the highest security risk. This provides an objective and accurate rating of the severity of security vulnerabilities in smart contracts.
The system is designed to assist in identifying and prioritizing vulnerabilities based on their level of risk to address the most critical issues in a timely manner.

1.1 EXPLOITABILITY

Attack Origin (AO):
Captures whether the attack requires compromising a specific account.
Attack Cost (AC):
Captures the cost of exploiting the vulnerability incurred by the attacker relative to sending a single transaction on the relevant blockchain. Includes but is not limited to financial and computational cost.
Attack Complexity (AX):
Describes the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability. Includes but is not limited to macro situation, available third-party liquidity and regulatory challenges.
Metrics:
EXPLOITABILIY METRIC (mem_e)METRIC VALUENUMERICAL VALUE
Attack Origin (AO)Arbitrary (AO:A)
Specific (AO:S)
1
0.2
Attack Cost (AC)Low (AC:L)
Medium (AC:M)
High (AC:H)
1
0.67
0.33
Attack Complexity (AX)Low (AX:L)
Medium (AX:M)
High (AX:H)
1
0.67
0.33
Exploitability EE is calculated using the following formula:

E=meE = \prod m_e

1.2 IMPACT

Confidentiality (C):
Measures the impact to the confidentiality of the information resources managed by the contract due to a successfully exploited vulnerability. Confidentiality refers to limiting access to authorized users only.
Integrity (I):
Measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of data stored and/or processed on-chain. Integrity impact directly affecting Deposit or Yield records is excluded.
Availability (A):
Measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability. This metric refers to smart contract features and functionality, not state. Availability impact directly affecting Deposit or Yield is excluded.
Deposit (D):
Measures the impact to the deposits made to the contract by either users or owners.
Yield (Y):
Measures the impact to the yield generated by the contract for either users or owners.
Metrics:
IMPACT METRIC (mIm_I)METRIC VALUENUMERICAL VALUE
Confidentiality (C)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Integrity (I)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Availability (A)None (A:N)
Low (A:L)
Medium (A:M)
High (A:H)
Critical (A:C)
0
0.25
0.5
0.75
1
Deposit (D)None (D:N)
Low (D:L)
Medium (D:M)
High (D:H)
Critical (D:C)
0
0.25
0.5
0.75
1
Yield (Y)None (Y:N)
Low (Y:L)
Medium (Y:M)
High (Y:H)
Critical (Y:C)
0
0.25
0.5
0.75
1
Impact II is calculated using the following formula:

I=max(mI)+mImax(mI)4I = max(m_I) + \frac{\sum{m_I} - max(m_I)}{4}

1.3 SEVERITY COEFFICIENT

Reversibility (R):
Describes the share of the exploited vulnerability effects that can be reversed. For upgradeable contracts, assume the contract private key is available.
Scope (S):
Captures whether a vulnerability in one vulnerable contract impacts resources in other contracts.
Metrics:
SEVERITY COEFFICIENT (CC)COEFFICIENT VALUENUMERICAL VALUE
Reversibility (rr)None (R:N)
Partial (R:P)
Full (R:F)
1
0.5
0.25
Scope (ss)Changed (S:C)
Unchanged (S:U)
1.25
1
Severity Coefficient CC is obtained by the following product:

C=rsC = rs

The Vulnerability Severity Score SS is obtained by:

S=min(10,EIC10)S = min(10, EIC * 10)

The score is rounded up to 1 decimal places.
SeverityScore Value Range
Critical9 - 10
High7 - 8.9
Medium4.5 - 6.9
Low2 - 4.4
Informational0 - 1.9

2. SCOPE

Files and Repository
(a) Repository: entangle-lsd-protocol
(b) Assessed Commit ID: 48e025e
(c) Items in scope:
  • /SynthChefs/FusionxSynthChef.sol
  • /SynthChefs/FusionxSynthChefMainnet.sol
  • /SynthChefs/CurveCompoundConvexSynthChef.sol
↓ Expand ↓
Out-of-Scope: Third-party libraries and dependencies, Economic attacks, Test and mock contacts
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

3. Assessment Summary & Findings Overview

Critical

10

High

6

Medium

5

Low

7

Informational

23

Security analysisRisk levelRemediation Date
Withdrawals Are Blocked Due to Wrong Function Name On the Curve ConvexCriticalSolved - 02/01/2024
Broken Stargate Deposit Flow Due to Missing AllowanceCriticalSolved - 02/15/2024
Broken integration through cosmos addressesCriticalSolved - 02/01/2024
Withdrawal blockage sellForLP Function Due to Price Being Set to Zero in SynthFactory ContractCriticalSolved - 02/01/2024
Broken Protocol Invariant On LP Token Decimals Leads to Unfair Synth MintingCriticalSolved - 02/01/2024
Fund Loss Due to Lack of Whitelisting and Address Validation in BurnAndBridge FunctionCriticalSolved - 02/01/2024
Lack of Access Control in proposeBurnAndBridge Function Exposes Contract to Unauthorized ActionsCriticalSolved - 02/01/2024
Unrestricted Access in Cross Chain Liquidity Pool FunctionsCriticalSolved - 02/01/2024
Lack of Access Controls in Token Minting and BurningCriticalSolved - 02/01/2024
Unsafe Casting Leads To Overflow/UnderflowCriticalSolved - 02/16/2024
Vulnerability in Compounder Contract Allowing Compromised EOA to Trigger Self-DestructionHighRisk Accepted - 02/01/2024
Function reinvest from SynthChef contracts are prone to MEV bot attackHighSolved - 02/15/2024
Exploiting Zero amountOutMin in DexWrappers for MEV AttacksHighNot Applicable - 02/15/2024
Incomplete Implementation of HandleTokenMintAndSwap in MasterSpotter ContractHighNot Applicable - 02/15/2024
Re-entrancy causes draining of funds through withdraw & SwapTo Function of EntangleTestBridge ContractHighNot Applicable - 02/15/2024
Inadequate Slippage Control in Liquidity WithdrawalHighNot Applicable - 02/15/2024
Implement TWAP for More Accurate Price Retrieval in Price OracleMediumSolved - 02/01/2024
Unsafe ERC20 operation(s)MediumSolved - 02/15/2024
Lack of Cross-Chain Validation in sellForLP Function of EntangleDexV2 ContractMediumSolved - 02/01/2024
Robust AllowanceMediumSolved - 02/15/2024
Absence of Price Staleness Check in proposeSetPrice FunctionMediumNot Applicable - 02/15/2024
Outdated OpenZeppelin Contracts Upgradeable Dependency in Entangle LSD ProtocolLowSolved - 02/15/2024
Incorrect Bit Masking and Shifting Operations for uint128 SID HandlingLowPartially Solved - 02/15/2024
Implementations Can Be InitializedLowPartially Solved - 02/15/2024
Missing init functions on the upgradeable contractsLowSolved - 02/15/2024
Incompatibility With Transfer-on-fee Or Deflationary TokensLowNot Applicable - 02/15/2024
Risk of Division by Zero in Price CalculationLowNot Applicable - 02/15/2024
DexWrapper Contract Allows Setting Slippage to ZeroLowNot Applicable - 02/15/2024
Absence of Event Emission in Setter Functions Compromises Transparency and AuditabilityInformationalAcknowledged - 02/15/2024
TODO Left in the codeInformationalAcknowledged - 02/15/2024
Missing/incomplete natspec can affect readability and uxInformationalAcknowledged - 02/15/2024
Use Ownable2Step instead of OwnableInformationalAcknowledged - 02/15/2024
Incompatibility of Admin Role Management with Multisig Wallets in SynthFactory Smart ContractInformationalAcknowledged - 02/01/2024
Unlocked PragmaInformationalSolved - 02/01/2024
Unoptimized LoopsInformationalAcknowledged - 02/15/2024
Using `private` rather than `public` for constants, saves gasInformationalAcknowledged - 02/15/2024
Use Custom Errors Instead Of Revert Strings To Save GasInformationalAcknowledged - 02/15/2024
No Need To Initialize Variables With Default ValuesInformationalAcknowledged - 02/15/2024
Removal of Redundant console.log Statements from CodebaseInformationalAcknowledged - 02/15/2024
Missing Access Control on withdraw Function in ATokenWrapper ContractInformationalAcknowledged - 02/15/2024
Removal of Hardhat Console Log Import in Mainnet ContractsInformationalAcknowledged - 02/15/2024
Redundant Import of SafeERC20 and IERC20 in ATokenWrapper ContractInformationalAcknowledged - 02/15/2024
Deprecated Increaseallowance And DecreaseallowanceInformationalAcknowledged - 02/15/2024
Obsolete SynapseBridge Contract FunctionalityInformationalAcknowledged - 02/15/2024
Non-Functional HandleBridgeExecuted FunctionInformationalAcknowledged - 02/15/2024
Redundant Functionality in HandleWithdrawLpAndExtract and HandleWithdrawLPInformationalAcknowledged - 02/15/2024
Ambiguity in Debug Functions debug_ClearSynthState and debug_ClearChefStateInformationalAcknowledged - 02/15/2024
Inconsistent Version Checking in StateParamsLibraryInformationalAcknowledged - 02/15/2024
Inconsistent Use of OwnableUpgradeable and AccessControlUpgradeable Across ContractsInformationalAcknowledged - 02/15/2024
Unnecessary Use of Assembly for Retrieving chainidInformationalAcknowledged - 02/15/2024
Missing Zero Address Validation in Contract SettersInformationalAcknowledged - 02/15/2024

4. Findings & Tech Details

4.1 Withdrawals Are Blocked Due to Wrong Function Name On the Curve Convex

// Critical

Description

The Entangle LSD Protocol's integration with the Curve Convex system in its CurveCompoundConvexSynthChef.sol contract is facing a critical issue due to a mismatch in function naming conventions. The Convex interface in the Entangle LSD Protocol contract defines a function witdraw(uint256 pid, uint256 amount). However, the actual Convex contract on the Ethereum mainnet defines this function as withdraw(uint256 pid, uint256 amount). The discrepancy lies in the spelling of "withdraw" (missing 'h' in the Entangle contract).

This mismatch leads to a failure in interfacing with the Convex contract, as calls to withdraw will not be recognized by the Convex system. This issue can halt critical functionalities related to withdrawing LP tokens, potentially impacting liquidity providers and the overall functioning of the protocol.


[CurveCompoundConvexSynthChef.sol#L184](https://github.com/Entangle-Protocol/entangle-lsd-protocol/blob/48e025e4ddf330e8ac710e82114db704a76d2857/contracts/SynthChefs/CurveCompoundConvexSynthChef.sol#L184)

    /// @notice Withdraw LP tokens from farm and transfer it to entangle MasterChef. Can only be called by the MASTER.
    /// @param poolId Entangle internal poolId.
    /// @param lpAmount Amount of LP tokens to withdraw.
    function withdrawLP(uint32 poolId, uint256 lpAmount) external onlyRole(MASTER) {
        console.log("CurveChef_withdrawLp", poolId);
        Pool memory pool = pools[poolId];
        //console.log(pool.convexID);
                console.log("CurveChef_withdrawLp");

        bool suc = convex.witdraw(pool.convexID, lpAmount);

        if (!suc) revert CurveCompoundConvexSynthChef__E4();

        IERC20Upgradeable(pool.lp).safeTransfer(masterChef, lpAmount);
    }

Proof of Concept
it("It should DepositLp", async () => {
    for (let index = 0; index < CurveConfig.Pools.length; index++) {
        LP = await ethers.getContractAt("ERC20", await MasterChef.getLpToken(SIDs[index]));
        const lpAmountToDeposit = await LP.connect(admin).balanceOf(admin.address);
        let lpAmountBefore = await MasterChef.connect(admin).getLpTokenTotalBalance(SIDs[index]);
        console.log(lpAmountToDeposit);

        expect(lpAmountBefore).to.eq(0);

        await LP.connect(admin).approve(MasterChef.address, lpAmountToDeposit);
        const tx1 = await MasterChef.connect(admin).depositLP(SIDs[index], lpAmountToDeposit);
        await tx1.wait();

        let lpAmountAfterDeposit = await MasterChef.getLpTokenTotalBalance(SIDs[index]);
        expect(lpAmountAfterDeposit).to.be.greaterThan(lpAmountBefore);
        console.log(lpAmountAfterDeposit);
    }
});

Ethereum Main-net Contract : https://etherscan.io/address/0xF403C135812408BFbE8713b5A23a04b3D48AAE31#code


BVSS
Recommendation

Correct the function name in the Convex interface within the CurveCompoundConvexSynthChef.sol contract to match the Convex mainnet contract. The correct function name is withdraw(uint256 pid, uint256 amount).

Remediation Plan: The Entangle team solved the issue by correcting the function name.

Remediation Hash
References

4.2 Broken Stargate Deposit Flow Due to Missing Allowance

// Critical

Description

The depositLP function in the StargateSynthChef.sol contract of the Entangle LSD Protocol is designed to facilitate the deposit of LP tokens for farming. However, a critical oversight in the contract's implementation can lead to failed transactions and impede the deposit process. The depositLP function directly calls the deposit function of the Stargate LP Staking contract without first ensuring that the Stargate contract is allowed to transfer the LP tokens on behalf of the user. In the absence of a prior approve transaction, the safeTransferFrom call in the Stargate contract will fail because the contract lacks the necessary allowance to transfer the user's LP tokens.


[StargateSynthChef.sol#L127C1-L138C1](https://github.com/Entangle-Protocol/entangle-lsd-protocol/blob/48e025e4ddf330e8ac710e82114db704a76d2857/contracts/SynthChefs/StargateSynthChef.sol#L127C1-L138C1)


    /// @notice Deposit LP tokens to farm. Can only be called by the MASTER.
    /// @param poolId Entangle internal poolId
    /// @param lpAmount Amount of LP tokens to deposit
    function depositLP(uint32 poolId, uint256 lpAmount) external onlyRole(MASTER) isPoolExist(address(pools[poolId].LPToken)) {
        console.log("StargateChef_depositLP", poolId);
        if(lpAmount == 0) revert StargateSynthChef__E3();
        Pool memory pool = pools[poolId];
        console.log(address(lpStaking), lpAmount, pool.stargateLPStakingPoolID);

        lpStaking.deposit(pool.stargateLPStakingPoolID, lpAmount);
    }

Proof of Concept
it("It should DepositLp", async () => {
    for (let index = 0; index < StargateConfig.Pools.length; index++) {
        LP = await ethers.getContractAt("ERC20", await MasterChef.getLpToken(SIDs[index]));
        const lpAmountToDeposit = await LP.connect(admin).balanceOf(admin.address);
        let lpAmountBefore = await MasterChef.connect(admin).getLpTokenTotalBalance(SIDs[index]);
        console.log(lpAmountToDeposit);

        expect(lpAmountBefore).to.eq(0);

        await LP.connect(admin).approve(MasterChef.address, lpAmountToDeposit);
        const tx1 = await MasterChef.connect(admin).depositLP(SIDs[index], lpAmountToDeposit);
        await tx1.wait();

        let lpAmountAfterDeposit = await MasterChef.getLpTokenTotalBalance(SIDs[index]);
        expect(lpAmountAfterDeposit).to.be.greaterThan(lpAmountBefore);
        console.log(lpAmountAfterDeposit);
    }
});
BVSS
Recommendation

Before the deposit call, the contract should check if the Stargate LP Staking contract has sufficient allowance to transfer the user's LP tokens. Also, It should approve the Stargate router.


Remediation Plan: The Entangle team solved the issue by calling approve function.

Remediation Hash

4.3 Broken integration through cosmos addresses

// Critical

Description

In the proposeBurnAndBridge/proposeBridgeExecuted function, we are directly getting the address parameter as an argument. However, It should be bytes instead of address. This will directly block integrations on the Cosmos (bech32) addresses.


[/contracts/BalanceManager.sol#L144](https://github.com/Entangle-Protocol/entangle-lsd-protocol/blob/48e025e4ddf330e8ac710e82114db704a76d2857/contracts/BalanceManager.sol#L144)


function proposeBurnAndBridge(uint64 destChainId, uint128 sid, uint256 synthAmount, address recipient) external {
    bytes4 selector = bytes4(keccak256("HandleBurnAndBridge(bytes)"));
    bytes memory functionParams = abi.encode(destChainId, sid, synthAmount, recipient);
    aggregationSpotter.propose(protocolId, eobChainId, abi.encode(eventProcessorAddress), selector, functionParams);
}

[BalanceManager.sol#L218](https://github.com/Entangle-Protocol/entangle-lsd-protocol/blob/48e025e4ddf330e8ac710e82114db704a76d2857/contracts/BalanceManager.sol#L218)

  function proposeBridgeExecuted(uint64 destChainId, uint128 sid, uint256 synthAmount, address recipient) external {
      bytes4 selector = bytes4(keccak256("HandleBridgeExecuted(bytes)"));
      bytes memory functionParams = abi.encode(destChainId, sid, synthAmount, recipient);
      aggregationSpotter.propose(protocolId, eobChainId, abi.encode(eventProcessorAddress), selector, functionParams);
  }

Proof of Concept
    function setUp() public {
        vm.createSelectFork(vm.envString("ETH_URL"));
        vm.rollFork(47442254);

        lsdSystem = new EntangleLSDBootstrapperETH();

        // boostrap the whole Liquid Vault system with Balance manager mock
        lsdSystem.bootstrap();

        protocolId = uint32(1) + (uint32(1) << 31);

        // sid deployed in LSD protocol
        sid = (uint128(lsdSystem.getChainID()) << 64) + (uint128(protocolId) << 32) + (uint128(1));

        aTokenWrapper = address(new ATokenWrapper());
    }

    function testBrokenCosmosAddress() external {
        lsdSystem.proposeBurnAndBridge(1,sid,100,0xF403C135812408BFbE8713b5A23a04b3D48AAE31);
    }

Only ETH addresses are allowed
BVSS
Recommendation

Consider changing address variable with a byte array.


Remediation Plan: The Entangle team solved the issue by deprecating the feature.

Remediation Hash

4.4 Withdrawal blockage sellForLP Function Due to Price Being Set to Zero in SynthFactory Contract

// Critical

Description

The SynthFactory smart contract contains a critical vulnerability related to the sellForLP function. This function is designed to allow users to sell their synthetic assets (synths) for LP (Liquidity Provider) tokens. The price of the synth is fetched from the contract using the getPrice function, which is set by the setPrice function.
The vulnerability arises from the fact that the setPrice function allows the PRICE_MANAGER role to set the price of a synth to zero. When the price is set to zero, the sellForLP function calculates the amount of LP tokens to be received (lpAmount) as zero, regardless of the amount of synth being sold. This scenario leads to a situation where users can burn their synths without receiving any LP tokens in return, effectively resulting in a loss of funds.


Users can lose their synths without receiving any LP tokens if the price is maliciously or accidentally set to zero.


[EntangleDexV2.sol#L159](https://github.com/Entangle-Protocol/entangle-lsd-protocol/blob/48e025e4ddf330e8ac710e82114db704a76d2857/contracts/EntangleDexV2.sol#L159)

   function sellForLP(uint128 sid, uint256 synthAmount, address recipient) public isPausedSID(sid) onlyLpStakingSid(sid) nonReentrant {
       if (synthAmount == 0) revert EntangleDex__E5();
       synthFactory.burn(sid, synthAmount, msg.sender, 0);
       uint price = synthFactory.getPrice(sid);
       uint lpAmount = (synthAmount * price) / 10 ** 18;
       if (block.chainid == SidLibrary.chainId(sid)) {
           masterSynthChef.withdrawLP(sid, lpAmount, recipient);
       } else {
           emit LPStackingSynthSell(sid, lpAmount, synthAmount, recipient);
           balanceManager.proposeSellForLp(sid, synthAmount, recipient);
       }
   }
Proof of Concept
image (8).png
BVSS
Recommendation

Implement a validation check in the setPrice function to ensure that the price cannot be set to zero. Consider setting a reasonable minimum price threshold.


Remediation Plan: The Entangle team solved the issue by preventing the price setter with "0".

Remediation Hash

4.5 Broken Protocol Invariant On LP Token Decimals Leads to Unfair Synth Minting

// Critical

Description

The function buyForLP has an issue concerning the handling of LP token decimals. This function is intended to facilitate the purchase of synthetic assets using LP tokens. The vulnerability arises from the calculation of the amount of synthetic assets to mint. The calculation does not account for the decimal places of the LP token, leading to an incorrect amount of synthetic assets being minted. The formula used is (lpAmount 10 * 18) / price. Here, lpAmount is assumed to be in wei (with 18 decimals), but LP tokens can have a different number of decimals.


https://github.com/Entangle-Protocol/entangle-lsd-protocol/blob/48e025e4ddf330e8ac710e82114db704a76d2857/contracts/EntangleDexV2.sol#L148


function buyForLP(uint128 sid, uint lpAmount) public onlyLpStakingSid(sid) onlyHomeChain(sid) isPausedSID(sid) nonReentrant {
    if (lpAmount == 0) revert EntangleDex__E5();
    IERC20 lpToken = IERC20(masterSynthChef.getLpToken(sid));
    lpToken.safeTransferFrom(msg.sender, address(this), lpAmount);
    lpToken.approve(address(masterSynthChef), lpAmount);
    masterSynthChef.depositLP(sid, lpAmount);
    uint price = synthFactory.getPrice(sid);
    uint amount = (lpAmount * 10 ** 18) / price;
    synthFactory.mint(sid, amount, msg.sender, 0);
}
Proof of Concept
Decimal Difference
Decimal Difference - Non 18
BVSS
Recommendation

Before performing the calculation, retrieve the actual number of decimals for the LP token using a function like lpToken.decimals().


Remediation Plan: The Entangle team solved the issue by minting small amount of tokens.

Remediation Hash

4.6 Fund Loss Due to Lack of Whitelisting and Address Validation in BurnAndBridge Function

// Critical

Description

In the burnAndBridge function of a smart contract, there is an issue due to the absence of validations for destChainId and recipient address. - The function does not check if the destChainId provided is part of a predetermined list of approved chains. Without this check, there is a risk of burning tokens and attempting to bridge to an unsupported or incorrect chain, leading to irrevocable loss of funds. - The function lacks a validation step for the recipient address. This absence could lead to scenarios where tokens are sent to an incorrect or invalid address, potentially resulting in the loss of those tokens.

[EntangleDexV2.sol#L178](https://github.com/Entangle-Protocol/entangle-lsd-protocol/blob/48e025e4ddf330e8ac710e82114db704a76d2857/contracts/EntangleDexV2.sol#L178)

   function burnAndBridge(uint64 destChainId, uint128 sid, uint256 synthAmount, address recipient) public onlyLpStakingSid(sid) isCrossChainAllowed(sid) isPausedSID(sid) nonReentrant {
       if (synthAmount == 0) revert EntangleDex__E5();
       synthFactory.burn(sid, synthAmount, msg.sender, 0);
       emit BurnAndBridge(destChainId, sid, synthAmount, recipient);
       balanceManager.proposeBurnAndBridge(destChainId, sid, synthAmount, recipient);
   }
Proof of Concept
    function testDepositWithdraw() external {
        address lpToken = lsdSystem.masterSynthChef().getLpToken(sid);
        address synth = address(lsdSystem.synthFactory().getSynth(sid));
        uint amount = 200e18;

        // deposit
        _mintSynth(lpToken, amount);

        assertEq(IERC20(lpToken).balanceOf(address(this)), 0, "LP not deposited");
        assertGt(IERC20(synth).balanceOf(address(this)), 0, "Synth not minted");

        lsdSystem.entangleDex().burnAndBridge(10000,sid,1,address(this));

    }

Lack of chain-id validation
BVSS
Recommendation

- Implement a mechanism to maintain a whitelist of supported destination chain IDs (destChainId). Before proceeding with the burn and bridge operation, verify that the provided destChainId is in the whitelist.

- Add a check to ensure the recipient address is a valid and non-zero address. This step is crucial to prevent transactions to incorrect or null addresses.

Remediation Plan: The Entangle team solved the issue by deprecating the feature.

Remediation Hash

4.7 Lack of Access Control in proposeBurnAndBridge Function Exposes Contract to Unauthorized Actions

// Critical

Description

The smart contract function proposeBurnAndBridge is designed to initiate significant operations such as burning synthetic assets and preparing them for cross-chain bridging. However, this function lacks any form of access control mechanism, making it publicly callable by any entity on the network. Without restrictions, such as those provided by role-based access control (RBAC) or ownership checks, the contract is vulnerable to unauthorized access and potential malicious manipulation.

This design flaw exposes the contract to various risks, including unauthorized burning of synthetic assets, unintended interactions with liquidity pools, and unauthorized initiation of cross-chain operations. The absence of access control mechanisms fundamentally compromises the contract's integrity and the security of its operations.


https://github.com/Entangle-Protocol/entangle-lsd-protocol/blob/48e025e4ddf330e8ac710e82114db704a76d2857/contracts/BalanceManager.sol#L144

   function proposeBurnAndBridge(uint64 destChainId, uint128 sid, uint256 synthAmount, address recipient) external {
       bytes4 selector = bytes4(keccak256("HandleBurnAndBridge(bytes)"));
       bytes memory functionParams = abi.encode(destChainId, sid, synthAmount, recipient);
       aggregationSpotter.propose(protocolId, eobChainId, abi.encode(eventProcessorAddress), selector, functionParams);
   }
Proof of Concept
    function testMaliciousCrossChainAction() external {
        balanceManager.proposeBurnAndBridge(100000000,sid,100,0xF403C135812408BFbE8713b5A23a04b3D48AAE31);
    }

BVSS
Recommendation

Introduce access control checks within the proposeBurnAndBridge function to ensure that only authorized addresses (e.g., MasterSynthChef or other designated administrators) can invoke it.


Remediation Plan: The Entangle team solved the issue by deprecating the feature.

Remediation Hash

4.8 Unrestricted Access in Cross Chain Liquidity Pool Functions

// Critical

Description

The smart contract functions proposeSellForLp, proposeSellForLPAndWithdraw, proposeDepositLp, proposeWithdrawLp, and proposeCompound are integral to managing synthetic assets and liquidity pool (LP) interactions within the contract ecosystem. These functions facilitate critical operations such as selling synthetic assets for LP tokens, depositing and withdrawing LP tokens, and compounding LP balances on farms. However, a significant security concern arises from the absence of access control mechanisms in these functions, making them publicly callable without any restrictions.

This unrestricted access poses several risks, notably allowing any external actor to potentially initiate synthetic asset sales, LP deposits, withdrawals, and other sensitive operations without authorization.


   function proposeSellForLp(uint128 sid, uint256 synthAmount, address recipient) external {
       bytes4 selector = bytes4(keccak256("HandleLPStackingSynthSell(bytes)"));
       bytes memory functionParams = abi.encode(sid, synthAmount, recipient);
       aggregationSpotter.propose(protocolId, eobChainId, abi.encode(eventProcessorAddress), selector, functionParams);
   }

   function proposeSellForLPAndWithdraw(uint128 sid, uint256 lpAmount, uint256 synthAmount, uint64 chainId, address recipient) external {
       bytes4 selector = bytes4(keccak256("HandleLPStackingSynthSellAndWithdraw(bytes)"));
       bytes memory functionParams = abi.encode(sid, lpAmount, synthAmount, chainId, recipient);
       aggregationSpotter.propose(protocolId, eobChainId, abi.encode(eventProcessorAddress), selector, functionParams);
   }

   function proposeDepositLp(uint128 sid, uint256 lpAmount, uint256 totalLpBalance) external {
       bytes4 selector = bytes4(keccak256("HandleDepositLP(bytes)"));
       bytes memory functionParams = abi.encode(sid, lpAmount, totalLpBalance);
       aggregationSpotter.propose(protocolId, eobChainId, abi.encode(eventProcessorAddress), selector, functionParams);
   }

   function proposeWithdrawLp(uint128 sid, uint256 lpAmount, address recipient, uint256 totalLpBalance) external {
       bytes4 selector = bytes4(keccak256("HandleWithdrawLP(bytes)"));
       bytes memory functionParams = abi.encode(sid, lpAmount, recipient, totalLpBalance);
       aggregationSpotter.propose(protocolId, eobChainId, abi.encode(eventProcessorAddress), selector, functionParams);
   }

   function proposeCompound(uint128 sid, uint256 newLpBalanceOnFarm) external {
       bytes4 selector = bytes4(keccak256("HandleCompound(bytes)"));
       bytes memory functionParams = abi.encode(sid, newLpBalanceOnFarm);
       aggregationSpotter.propose(protocolId, eobChainId, abi.encode(eventProcessorAddress), selector, functionParams);
   }
Proof of Concept
   function testMaliciousCrossChainAction() external {
       balanceManager.proposeBurnAndBridge(100000000, sid, 100, 0xF403C135812408BFbE8713b5A23a04b3D48AAE31);
       balanceManager.proposeSellForLp(sid, 100, 100, 0xF403C135812408BFbE8713b5A23a04b3D48AAE31);
       balanceManager.proposeSellForLPAndWithdraw(sid, 100, 100, 9999, 100, 100, 0xF403C135812408BFbE8713b5A23a04b3D48AAE31);
       balanceManager.proposeDepositLp(sid, 100, 100);
       balanceManager.proposeWithdrawLp(sid, 100, 100, 0xF403C135812408BFbE8713b5A23a04b3D48AAE31);
       balanceManager.proposeWithdrawLp(sid, 100, 0xF403C135812408BFbE8713b5A23a04b3D48AAE31, 100);
       balanceManager.proposeCompound(sid, 1000);
   }
BVSS
Recommendation

Integrate RBAC mechanisms to restrict function calls to authorized entities, such as contract administrators or designated accounts. Utilize OpenZeppelin's AccessControl library or similar to manage roles and permissions efficiently.


Remediation Plan: The Entangle team solved the issue by deprecating the feature.

Remediation Hash

4.9 Lack of Access Controls in Token Minting and Burning

// Critical

Description

The smart contract functions proposeMint and proposeBurn play crucial roles in managing the tokenomics of the ecosystem by handling the minting and burning of synthetic assets. Despite their significance, both functions currently lack essential access control mechanisms. This omission permits any external entity to propose minting or burning operations without restrictions, leading to potential unauthorized manipulation of the token supply.

This design flaw exposes the ecosystem to severe risks, including inflation or deflation of the token supply through unauthorized minting or burning, which could undermine the token's value and the overall economic model.


function proposeMint(uint128 sid, uint256 synthAmount, address recipient, uint256 totalSupply, uint256 opId) external {
    bytes4 selector = bytes4(keccak256("HandleMint(bytes)"));
    bytes memory functionParams = abi.encode(sid, synthAmount, recipient, totalSupply, opId);
    aggregationSpotter.propose(protocolId, eobChainId, abi.encode(eventProcessorAddress), selector, functionParams);
}

function proposeBurn(uint128 sid, uint256 synthAmount, address from, uint256 totalSupply, uint256 opId) external {
    bytes4 selector = bytes4(keccak256("HandleBurn(bytes)"));
    bytes memory functionParams = abi.encode(sid, synthAmount, from, totalSupply, opId);
    aggregationSpotter.propose(protocolId, eobChainId, abi.encode(eventProcessorAddress), selector, functionParams);
}


Proof of Concept
    function testBrokenAccessControl() external {
        balanceManager.proposeMint(sid,1000000,address(this),1000000000,1);
        balanceManager.proposeBurn(sid,1000000,address(this),1000000000,1);
    }

BVSS
Recommendation

Introduce RBAC to restrict the ability to propose minting and burning operations to authorized roles only, such as contract administrators or designated accounts. Utilize OpenZeppelin's AccessControl library to manage roles and permissions effectively.


Remediation Plan: The Entangle team solved the issue by deprecating the feature.

Remediation Hash

4.10 Unsafe Casting Leads To Overflow/Underflow

// Critical

Description

The smart contract contains functions such as reinvest and withdraw that perform conversions between int128, int256, and uint256 without validating that these conversions are safe. Here's a step-by-step breakdown of how this could lead to vulnerabilities, using the reinvest function as an example:


  • In the reinvest function, there's a line where a uint256 variable amounts[i] is converted to int128 for a function call:

    run3(
        ..., 
        EXACTLY, int128(int256(amounts[0])),
        ...
    );

    run3( ..., EXACTLY, int128(int256(amounts[0])), ... );


  • Given uint256 can store values much larger than what int128 can hold, this conversion is unsafe without checks.

  • Assume a scenario where amounts[0] holds a value greater than 2**127 - 1 (the maximum positive value int128 can represent).

  • The direct casting to int128 would result in an overflow, leading to an incorrect, potentially negative value.

Proof of Concept
    it("Deposit -> wihdrawLP (tweaked)", async () => {
        /*  
            comment out balance manager part until it is deployed
        */
        const { amountUsdc, amountToken } = await fundProtoChef()
        const LP = new ethers.Contract(VelocoreConfig.Pools[0].LPTokenAddress, ERC20_ABI, depositorCompounder);

        await VelocoreChef.connect(depositorCompounder).deposit(poolId, [amountUsdc, amountToken])

        const lpAmountAfterDeposit = await MasterChef.getLpTokenTotalBalance(SID);
        // console.log(lpAmountAfterDeposit)
        // console.log(ethers.utils.formatEther(lpAmountAfterDeposit))
        expect(lpAmountAfterDeposit).to.be.gt(0);

        await MasterChef.connect(depositorCompounder).withdrawLP(SID, 115792089237316195423570985008687907853269984665640564039457584007913129639935 , depositorCompounder.address)
        const balanceAfterWithdraw = await MasterChef.getLpTokenTotalBalance(SID)
        expect(balanceAfterWithdraw).to.be.equal(0);

        const depositorLPBalance = await LP.balanceOf(depositorCompounder.address)
        expect(depositorLPBalance).to.equal(lpAmountAfterDeposit)

    });
BVSS
Recommendation

Implement or use an existing library designed for safe type conversions, such as OpenZeppelin's SafeCast.


Remediation Plan: The Entangle team solved the issue by using SafeCast on the codebase.

Remediation Hash

4.11 Vulnerability in Compounder Contract Allowing Compromised EOA to Trigger Self-Destruction

// High

Description

The compounder contract in the given system possesses a critical vulnerability that could lead to its self-destruction if the externally owned account (EOA) is compromised. This vulnerability is primarily due to the implementation of delegatecall in the compound function.


    function compound(address protocolSynthChef, uint32 poolId, PreconfiguredCallConfig[] calldata callConfigs, bool reinvest) external returns (uint) {
        require(msg.sender == masterSynthChef, "auth failed");
        IProtocolSynthChef(protocolSynthChef).harvest(poolId);

        for (uint i = 0; i < callConfigs.length; ++i) {
            PreconfiguredCallConfig memory _call = callConfigs[i];
            if (_call.isDelegateCall) {
                (bool success, bytes memory data) = _call.callee.delegatecall(_call.payload);
                _revertIfFailed(success, data);
            } else {
                (bool success, bytes memory data) = _call.callee.call(_call.payload);
                _revertIfFailed(success, data);
            }
        }

        address[] memory underlyings = IProtocolSynthChef(protocolSynthChef).getPoolTokens(poolId);

        for (uint i = 0; i < underlyings.length; ++i) {
            uint balance = IERC20(underlyings[i]).balanceOf(address(this));
            if (balance != 0) {
                SafeERC20.safeTransfer(IERC20(underlyings[i]), protocolSynthChef, balance);
            }
        }

        if (reinvest)
            return IProtocolSynthChef(protocolSynthChef).reinvest(poolId);
        return 0;
    }

    function _revertIfFailed(bool success, bytes memory data) internal pure {
        if (!success) {
            assembly {
              revert(add(data,32),mload(data))
            }
        }
    }
Proof of Concept
    contract A {
      function kill (address payable to) public {
        selfdestruct(to);
      }
    }
    // call run will excute selfdestract on contract B & fund eth to toAddress
    contract B {
      function run (address target, string calldata func, address to) public {
        target.delegatecall(abi.encodeWithSignature(func, to));
      }
    }
BVSS
Recommendation

Refrain from using delegatecall with dynamic inputs from users or EOAs. Restrict delegatecall to predefined, trusted contracts only.


Remediation Plan: The Entangle team accepted the risk of this issue.

4.12 Function reinvest from SynthChef contracts are prone to MEV bot attack

// High

Description

In decentralized finance (DeFi), transactions are transparent on the blockchain, allowing observers to anticipate trades and potentially exploit them. A notable exploit is the "sandwich attack," which leverages the predictability of transaction execution to manipulate market prices for profit. This proof of concept outlines how the reinvest function in SynthChef contracts, when called with amountMin parameter set to zero, can be vulnerable to such attacks.



Proof of Concept

Step 1: Observer Identifies a Vulnerable Transaction

  1. An MEV bot (the observer) monitors pending transactions in the mempool for calls to the reinvest function in SynthChef contracts where amountMin are set to zero.

Step 2: Attacker Prepares a Sandwich Attack

  1. Upon identifying a vulnerable transaction, the attacker prepares two transactions:

    • Front-run Transaction: Buys asset Y (the asset being bought in the victim's trade) before the victim's transaction, anticipating a price increase.

    • Back-run Transaction: Sells asset Y after the victim's trade, aiming to profit from the price increase caused by the victim's transaction.

Step 3: Execution of the Sandwich Attack

  1. The attacker submits the front-run transaction with a higher gas price to ensure it is mined before the victim's transaction.

  2. The victim's transaction executes, buying asset Y as intended but at a higher price due to the front-run transaction, causing slippage.

  3. The attacker submits the back-run transaction, also with a high gas price, to sell asset Y at the new, elevated price.

Step 4: Outcome and Impact

  1. The attacker profits from the price difference between the front-run and back-run transactions, exploiting the lack of slippage protection in the victim's trade.

  2. The victim experiences a worse trade outcome than expected, potentially losing a significant amount due to slippage and price manipulation.

BVSS
Recommendation

Always set reasonable amountMin values in liquidity addition/removal functions to provide slippage protection.


Remediation Plan: The Entangle team solved the issue by adding minimum return check.

Remediation Hash

4.13 Exploiting Zero amountOutMin in DexWrappers for MEV Attacks

// High

Description

In the context of DeFi protocols, DexWrappers like UniswapHandler facilitate interactions with decentralized exchanges (DEXes) such as Uniswap by providing functions to swap tokens, add liquidity, and remove liquidity. Crucially, these interactions require specifying a minimum output amount (amountOutMin) for trades to protect against undesirable slippage and prevent exploitation through sandwich attacks. This proof of concept details how failing to set amountOutMin appropriately in the removeLiquidity and swapExactTokensForTokens functions exposes the contract to MEV (Miner Extractable Value) attacks.

Proof of Concept

Step 1: Attacker Identifies the Transaction

  1. An attacker monitors the mempool for swapExactTokensForTokens calls made by UniswapHandler with amountOutMin set to zero.

Step 2: Preparing the Sandwich Attack

  1. Front-run Transaction: Upon spotting a vulnerable swap transaction from DAI to MemeToken, the attacker buys MemeToken before the UniswapHandler transaction is processed, expecting the price of MemeToken to increase.

  2. Back-run Transaction: The attacker prepares to sell MemeToken back into DAI after the UniswapHandler transaction, capitalizing on the increased price of MemeToken.

Step 3: Executing the Attack

  1. The attacker submits the front-run transaction with a higher gas fee to ensure it is executed before the UniswapHandler transaction.

  2. The UniswapHandler transaction executes, swapping DAI for MemeToken at an unfavorable rate due to the price manipulation caused by the front-run transaction.

  3. The attacker executes the back-run transaction, selling MemeToken for DAI at the inflated price, profiting from the price difference created by the sandwich attack.

BVSS
Recommendation
  • Modify the removeLiquidity and swapExactTokensForTokens functions to require a non-zero amountOutMin parameter, reflective of acceptable slippage for the operation.

  • Utilize on-chain data to dynamically calculate an appropriate amountOutMin based on current market conditions and the specific requirements of the trade or liquidity removal.

Remediation Plan: The issue is marked as “Not Applicable” due to the related code-base is deprecated.

4.14 Incomplete Implementation of HandleTokenMintAndSwap in MasterSpotter Contract

// High

Description

The MasterSpotter contract, which is part of a broader system designed to handle various blockchain events, includes the function HandleTokenMintAndSwap. This function is intended to process specific events related to token minting and swapping. Upon review, it is observed that the function contains a placeholder comment "TODO handle?" and does not implement any logic beyond decoding the event parameters. This incomplete implementation suggests that the functionality for handling the TokenMintAndSwap event is not fully developed, potentially leading to unprocessed events and missed actions that could be critical for the intended operation of the system.


    // #region Balance Spotter Handlers
    // ??? Huh will this ever be called
    function HandleTokenMintAndSwap(bytes calldata b) public onlyRole(SPOTTER) {
        (, , , , bytes memory eventParams) = abi.decode(b, (bytes32, uint256, uint256, bytes32, bytes));

        EntangleTestBridgeEvents.TokenMintAndSwap memory tokenMintAndSwap = EntangleTestBridgeEvents
            .DecodeTokenMintAndSwap(eventParams);
        // TODO handle?
        return;
    }
BVSS
Recommendation

Review the intended behavior and necessary actions for the TokenMintAndSwap events within the context of the MasterSpotter contract's role in the system. Develop and integrate the required logic to ensure that these events are processed appropriately, executing any necessary actions such as updating internal states, interacting with other contracts, or triggering further events.


Remediation Plan: The issue is marked as “Not Applicable” due to the related code-base is deprecated.

4.15 Re-entrancy causes draining of funds through withdraw & SwapTo Function of EntangleTestBridge Contract

// High

Description
Finding description placeholder
Proof of Concept
interface IERC677Receiver {
    function onTokenTransfer(address _sender, uint _value, bytes calldata _data) external;
}

interface IERC777TokensRecipient {
    function tokensReceived(address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData) external;
}

contract AttackerContract is IERC677Receiver, IERC777TokensRecipient {
    EntangleTestBridge public victim;
    IERC20 public attackToken;

    constructor(address _victimAddress, address _tokenAddress) {
        victim = EntangleTestBridge(_victimAddress);
        attackToken = IERC20(_tokenAddress);
    }

    // Fallback function to initiate the attack
    function onTokenTransfer(address _sender, uint _value, bytes calldata _data) external override {
        // Re-entrancy attack logic
        victim.withdraw(attackToken, _value);
    }

    function tokensReceived(address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData) external override {
        // Re-entrancy attack logic
        victim.withdraw(attackToken, amount);
    }

    function attack(uint256 amount) external {
        attackToken.transferFrom(msg.sender, address(victim), amount);
        // The callback from ERC677 or ERC777 token transfer will trigger the re-entrancy
    }
}

Attack Execution

  1. Initiate the Attack: The attacker calls the attack function of the AttackerContract with the amount of tokens they wish to exploit. This function transfers ERC677/ERC777 tokens to the EntangleTestBridge, which triggers the onTokenTransfer or tokensReceived callback in AttackerContract.

  2. Callback and Re-entrancy: The onTokenTransfer or tokensReceived function in AttackerContract is automatically called due to the token transfer, and it immediately invokes the vulnerable withdraw function in EntangleTestBridge, exploiting the re-entrancy vulnerability.

  3. Repeat Unauthorized Withdrawals: The callback and re-entrant call to withdraw allow the attacker to withdraw more tokens than they should be allowed, exploiting the contract's flawed state management.

BVSS
Recommendation

Restructure the withdraw function to update the contract's internal state (tokenStorage[address(token)] -= amount) before performing the external call to token.safeTransferFrom. This change ensures that the contract's state is consistent even if a re-entrancy attack is attempted.


Remediation Plan: The issue is marked as “Not Applicable” due to the related code-base is deprecated.

4.16 Inadequate Slippage Control in Liquidity Withdrawal

// High

Description

The withdraw function in the contract is designed to withdraw LP tokens from a farm, remove liquidity, and then transfer the resultant tokens to the Entangle MasterChef. A critical issue arises from setting both amountAMin and amountBMin parameters to 1 in the router.removeLiquidity call. This approach poses a significant risk of slippage, making the transaction susceptible to front-running attacks and potentially resulting in a significantly worse exchange rate than expected.

By setting the minimum received tokens (amountAMin and amountBMin) to 1, the contract effectively removes any meaningful slippage protection. This can lead to unfavorable rates for the withdrawn liquidity, especially in volatile or low-liquidity pools, where the actual amounts received can be drastically lower than the market rate. Such a vulnerability could be exploited by malicious actors performing front-running attacks, further worsening the slippage experienced by the contract.

Proof of Concept

The vulnerability arises from setting the minimum received amounts (amountAMin and amountBMin) to 1 in a liquidity removal operation. This practically removes any slippage protection and can be exploited through a front-running attack. Here's how an attacker could exploit this vulnerability:

An attacker monitors the mempool for pending transactions that call the withdraw function of the vulnerable contract. The goal is to identify transactions intending to remove liquidity from a pool.

Upon detecting a pending withdraw transaction, the attacker submits a transaction that buys a significant amount of one of the pool's tokens (either token0 or token1). This transaction is crafted with a higher gas price to ensure it is mined before the targeted withdraw transaction. The purchase increases the price of the bought token in the pool.

The original withdraw transaction is executed after the attacker's purchase transaction. Due to the slippage vulnerability (with amountAMin and amountBMin set to 1), the contract accepts whatever the current rate is, regardless of how unfavorable it has become due to the attacker's front-running transaction.


BVSS
Recommendation

Implement dynamic slippage control based on the current market conditions or set more conservative, realistic slippage bounds. This could involve querying the expected amounts using a method like getAmountsOut before executing the liquidity removal and setting the amountAMin and amountBMin parameters based on a permissible slippage percentage from those expected amounts.


Remediation Plan: The issue is marked as “Not Applicable” due to the code will be used for liquidation at the future. In the current design of protocol, the user is not able to remove liquidity.

4.17 Implement TWAP for More Accurate Price Retrieval in Price Oracle

// Medium

Description
Finding description placeholder
Proof of Concept
BVSS
Recommendation

Modify the existing setPrice function to calculate the TWAP. This might involve fetching multiple price points within a defined time window and computing their average.


Remediation Plan: The Entangle team solved the issue by calculating price with synth amount.

Remediation Hash

4.18 Unsafe ERC20 operation(s)

// Medium

Description

Currently, the contracts does not adequately handle atypical ERC20 tokens on the staking. According to the ERC20 specification, tokens should return "false" when a transfer fails, but it does not guarantee that the function will revert. This discrepancy could potentially lead to silent failures, making it difficult to detect issues when they occur.

For instance, from the below, It can be seen that [LDO](https://etherscan.io/token/0x5a98fcbea516cf06857215779fd812ca3bef1b32#code) does not revert If the user balance is not enough for the transfer.


```
File: EntangleDexV2.sol

144: lpToken.approve(address(masterSynthChef), lpAmount);

```

```
File: SynthChefs/StargateSynthChef.sol

76: pools[poolId].LPToken.approve(address(lpStaking), type(uint256).max);

```


```
File: SynthChefs/WombatSynthChef.sol

320: womToken.transfer(ICompounderAddressResolver(masterChefEnt).compounder(), womRewards);

350: currentToken.transfer(ICompounderAddressResolver(masterChefEnt).compounder(), currentAmount);

```


```
File: interfaces/SynthChefs/Velocore/lib/Token.sol

242: tok.transferFrom(from, to, amount);

```

BVSS
Recommendation

Consider using safeApprove & safeTransfer.


Remediation Plan: The Entangle team solved the issue by using safeApprove & safeTransfer.


Remediation Hash

4.19 Lack of Cross-Chain Validation in sellForLP Function of EntangleDexV2 Contract

// Medium

Description

In the EntangleDexV2 smart contract, a critical oversight has been identified in the sellForLP function. This function is intended to facilitate the selling of synthetic assets (synths) for LP (Liquidity Provider) tokens. However, the function lacks the necessary validation to check whether cross-chain operations are allowed for the specific synthetic asset (sid).
The contract already includes a modifier isCrossChainAllowed which is designed to perform this validation, but it is conspicuously absent in the sellForLP function. This oversight could potentially allow cross-chain synth transactions even when they are not permitted.


    function sellForLP(
        uint128 sid,
        uint256 synthAmount,
        address recipient
    ) public isPausedSID(sid) onlyLpStakingSid(sid) nonReentrant {
        if(synthAmount == 0) revert EntangleDex__E5();
        synthFactory.burn(sid, synthAmount, msg.sender, 0);
        uint price = synthFactory.getPrice(sid);
        uint lpAmount = (synthAmount * price) / 10 ** 18;
        if (block.chainid == SidLibrary.chainId(sid)) {
            masterSynthChef.withdrawLP(sid, lpAmount, recipient);
        } else {
            emit LPStackingSynthSell(sid, lpAmount, synthAmount, recipient);
            balanceManager.proposeSellForLp(sid, synthAmount, recipient);
        }
    }
BVSS
Recommendation

Implement the isCrossChainAllowed modifier within the sellForLP function to ensure that only synths that are explicitly allowed for cross-chain operations can be processed through this function.


Remediation Plan: The Entangle team solved the issue by deprecating the feature.

Remediation Hash

4.20 Robust Allowance

// Medium

Description

Some tokens do not correctly implement the EIP20 standard, and their approve function returns void instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert. Tokens that do not correctly implement the latest EIP20 spec, like USDT on Ethereum, will be unusable in the mentioned contracts as they revert the transaction because of the missing return value.

Some tokens also require that the allowance be set to 0 before issuing a new approve call. Calling the approve function when the allowance is not zero reverts the transaction with these types of tokens.


https://github.com/Entangle-Protocol/entangle-lsd-protocol/blob/48e025e4ddf330e8ac710e82114db704a76d2857/contracts/EntangleDexV2.sol#L144

   function buyForLP(uint128 sid, uint lpAmount) public onlyLpStakingSid(sid) onlyHomeChain(sid) isPausedSID(sid) nonReentrant {
       if (lpAmount == 0) revert EntangleDex__E5();
       IERC20 lpToken = IERC20(masterSynthChef.getLpToken(sid));
       lpToken.safeTransferFrom(msg.sender, address(this), lpAmount);
       lpToken.approve(address(masterSynthChef), lpAmount);
       masterSynthChef.depositLP(sid, lpAmount);
       uint price = synthFactory.getPrice(sid);
       uint amount = (lpAmount * 10 ** 18) / price;
       synthFactory.mint(sid, amount, msg.sender, 0);
   }
BVSS
Recommendation

It is recommended to use OpenZeppelin’s SafeERC20 and the forceApprove() function or safeApprove() with 0 first to also handle non-standard-compliant tokens.


Remediation Plan: The Entangle team solved the issue by approving with 0.

Remediation Hash

4.21 Absence of Price Staleness Check in proposeSetPrice Function

// Medium

Description

In the smart contract function proposeSetPrice, there is a critical oversight regarding the validation of data freshness, particularly concerning the prices derived from external sources or calculations based on potentially outdated state information. The function calculates a new price based on the lpTokenFarmBalance and synthTotalSupplyAllChains without performing any checks to ensure that these values are current and not stale. Price staleness can occur due to delays in data updates, network congestion, or failures in external dependencies, leading to the use of outdated information for crucial financial calculations.


    function proposeSetPrice(uint128 sid) external onlyRole(MASTER) {
        console.log("OracleBalanceSpotter: proposeSetPrice");
        (, StateParamsLibrary.EChefStateParams memory chefState) = stateSpotter.getChefState(sid);

        uint256 lpTokenFarmBalance = chefState.lpTokenFarmBalance;
        console.log("lpTokenFarmBalance = %d", lpTokenFarmBalance);
        uint256 synthTotalSupplyAllChains = stateSpotter.getSynthTotalSupplyAllChains(sid);

        console.log("synthTotalSupplyAllChains = %d", synthTotalSupplyAllChains);
        uint opId = newOperation("proposeSetPrice");

        uint256 price = (lpTokenFarmBalance * 10 ** 18) / synthTotalSupplyAllChains;
        console.log("price = %d", price);

        if (lpTokenFarmBalance == 0 || synthTotalSupplyAllChains == 0) {
            console.log("Set failsafe price");
            price = 10 ** 18;
            return;
        }

        bytes4 setPriceSelector = bytes4(keccak256("setPrice(bytes)"));
        for (uint i = 0; i < stateSpotter.getChainsLength(); i++) {
            uint64 chainId = stateSpotter.chains(i);

            // TODO: !!! apply new price in SynthState on all chains
            stateSpotter.setPrice(chainId, sid, price);
            proposer.propose(
                protocolId,
                chainId,
                chainInfo[chainId].balanceManager,
                setPriceSelector,
                abi.encode(sid, price, opId)
            );
        }
    }
BVSS
Recommendation

Introduce mechanisms to verify the freshness of the data used in price calculations. This could involve timestamp checks, versioning of data points, or validation against a known recent block height to ensure that the data used is up-to-date.


Remediation Plan: The issue is marked as “Not Applicable” due to the related code-base is deprecated.

4.22 Outdated OpenZeppelin Contracts Upgradeable Dependency in Entangle LSD Protocol

// Low

Description

The Entangle LSD Protocol's package.json references an outdated version of OpenZeppelin's @openzeppelin/contracts-upgradeable package (^4.8.2). As of the latest release (v4.9.5 on Dec 8, 2023), several improvements and critical security patches may not be present in the currently utilized version. Using outdated dependencies, especially those related to security and contract upgrades, poses a significant risk to the protocol's security, maintainability, and overall functionality.


    "devDependencies": {
        "@nomicfoundation/hardhat-chai-matchers": "^1.0.3",
        "@nomicfoundation/hardhat-ethers": "^3.0.4",
        "@nomicfoundation/hardhat-toolbox": "^1.0.2",
        "@nomiclabs/hardhat-ethers": "^2.0.6",
        "@nomiclabs/hardhat-etherscan": "^3.1.0",
        "@nomiclabs/hardhat-ganache": "^2.0.1",
        "@openzeppelin/contracts-upgradeable": "^4.8.2",
}
BVSS
Recommendation

Update the @openzeppelin/contracts-upgradeable dependency to the latest version (v4.9.5). Ensure that all contract imports are adjusted accordingly to accommodate any changes in the library's API.


Remediation Plan : The Entangle team solved the issue by updating dependency.

Remediation Hash

4.23 Incorrect Bit Masking and Shifting Operations for uint128 SID Handling

// Low

Description

The SidLibrary library is designed to perform operations on a uint128 SID (Security Identifier) by extracting specific components such as chain ID, protocol ID, and pool ID from the SID. However, the library defines constants MAXUINT64 and MAXUINT32 with bit lengths that are incompatible with the operations performed on a uint128 SID variable. The constants are used to mask and shift bits within the SID to extract parts of the identifier, but the MAXUINT64 constant, in particular, is used under the assumption that it represents a uint128 maximum value for 64 bits, which is not the case due to its definition as a uint128 with only 64 significant bits set.


//SPDX-License-Identifier: BSL 1.1
pragma solidity ^0.8.15;

library SidLibrary {
    uint128 constant MAXUINT64 = 0xFFFFFFFFFFFFFFFF;
    uint128 constant MAXUINT32 = 0xFFFFFFFF;

    function chainId(uint128 SID) internal pure returns (uint64) {
        return uint64((SID & (MAXUINT64 << 64)) >> 64);
    }

    function protocolId(uint128 SID) internal pure returns (uint32) {
        return uint32((SID & (MAXUINT32 << 32)) >> 32);
    }

    function poolId(uint128 SID) internal pure returns (uint32) {
        return uint32(SID & MAXUINT32);
    }

    function isLpStackingSynth(uint128 SID) internal pure returns (bool) {
        return (protocolId(SID) & (1 << 31)) != 0 ? true : false;
    }

    function unpack(uint128 SID) internal pure returns (uint64, uint32, uint32) {
        return (chainId(SID), protocolId(SID), poolId(SID));
    }
}
BVSS
Recommendation

Update the library to use correct bit lengths and operations that align with the uint128 SID size. This includes redefining constants with uint128 in mind and ensuring bit operations correctly handle the 128-bit size.

Remediation Plan : The Entangle team has partially solved the issue by changing the variable type to uint64.

Remediation Hash

4.24 Implementations Can Be Initialized

// Low

Description

The contracts are upgradable, inheriting from the Initializable contract. However, the current implementations are missing the _disableInitializers() function call in the constructors. Thus, an attacker can initialize the implementation. Usually, the initialized implementation has no direct impact on the proxy itself; however, it can be exploited in a phishing attack. In rare cases, the implementation might be mutable and may have an impact on the proxy.

BVSS
Recommendation

It is recommended to call [_disableInitializers](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/utils/Initializable.sol#L145) within the contract's constructor to prevent the implementation from being initialized.

Remediation Plan : The Entangle team has partially solved the issue by adding [_disableInitializers](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/utils/Initializable.sol#L145). However, SynthChefs are still missing function call.

Remediation Hash

4.25 Missing init functions on the upgradeable contracts

// Low

Description

The contracts, which are designed to be upgradeable using the UUPS pattern, seem to be missing proper initialization functions. The initial setup for a UUPS upgradeable contract is crucial to ensure that the contract can be safely upgraded in the future without losing state or introducing vulnerabilities.

BVSS
Recommendation

To address this concern, ensure that all base contracts are properly initialized in the initialize function.

Remediation Plan: The Entangle team solved the issue.

Remediation Hash

4.26 Incompatibility With Transfer-on-fee Or Deflationary Tokens

// Low

Description

It was identified that the several contracts assume that the safeTransferFrom() and safeTransfer() calls transfer the full amount of tokens. This may not be true if the tokens being transferred are fee-on-transfer tokens, causing the received amount to be lesser than the accounted amount. For example, DGX (Digix Gold Token) and CGT (CACHE Gold) tokens apply transfer fees, and the USDT (Tether) token also has a currently disabled fee feature.

It was also identified that the contract assumes that its token balance does not change over time without any token transfers, which may not be true if the tokens being transferred were deflationary/rebasing tokens. For example, the supply of AMPL (Ampleforth) tokens automatically increases or decreases every 24 hours to maintain the AMPL target price.

In these cases, the contracts may not have the full token amounts, and the associated functions may revert.

BVSS
Recommendation

It is recommended to get the exact received amount of the tokens being transferred by calculating the difference of the token balance before and after the transfer and using it to update all the variables correctly.

It is also recommended that all tokens are thoroughly checked and tested before they are used to avoid tokens that are incompatible with the contracts.

Remediation Plan: The issue is marked as “Not Applicable” due to the related code-base is deprecated.

4.27 Risk of Division by Zero in Price Calculation

// Low

Description

The proposeSetPrice function within the contract calculates a price based on the lpTokenFarmBalance and synthTotalSupplyAllChains. However, the division operation (lpTokenFarmBalance * 10 ** 18) / synthTotalSupplyAllChains is performed without ensuring that synthTotalSupplyAllChains is not zero. While there is a conditional check after the division operation to set a failsafe price if either lpTokenFarmBalance or synthTotalSupplyAllChains is zero, this check is misplaced and should occur before the division to prevent a potential division by zero error.


    function proposeSetPrice(uint128 sid) external onlyRole(MASTER) {
        console.log("OracleBalanceSpotter: proposeSetPrice");
        (, StateParamsLibrary.EChefStateParams memory chefState) = stateSpotter.getChefState(sid);

        uint256 lpTokenFarmBalance = chefState.lpTokenFarmBalance;
        console.log("lpTokenFarmBalance = %d", lpTokenFarmBalance);
        uint256 synthTotalSupplyAllChains = stateSpotter.getSynthTotalSupplyAllChains(sid);

        console.log("synthTotalSupplyAllChains = %d", synthTotalSupplyAllChains);
        uint opId = newOperation("proposeSetPrice");

        uint256 price = (lpTokenFarmBalance * 10 ** 18) / synthTotalSupplyAllChains;
        console.log("price = %d", price);

        if (lpTokenFarmBalance == 0 || synthTotalSupplyAllChains == 0) {
            console.log("Set failsafe price");
            price = 10 ** 18;
            return;
        }

        bytes4 setPriceSelector = bytes4(keccak256("setPrice(bytes)"));
        for (uint i = 0; i < stateSpotter.getChainsLength(); i++) {
            uint64 chainId = stateSpotter.chains(i);

            // TODO: !!! apply new price in SynthState on all chains
            stateSpotter.setPrice(chainId, sid, price);
            proposer.propose(
                protocolId,
                chainId,
                chainInfo[chainId].balanceManager,
                setPriceSelector,
                abi.encode(sid, price, opId)
            );
        }
    }
BVSS
Recommendation

To mitigate the risk of division by zero and ensure the robustness of the price calculation, the conditional check for zero values should be moved before the division operation. Additionally, consider setting a default or failsafe price more proactively to handle scenarios where lpTokenFarmBalance or synthTotalSupplyAllChains might be zero. Implementing these changes enhances the function's reliability and prevents potential disruptions.


Remediation Plan: The issue is marked as “Not Applicable” due to the related code-base is deprecated.

4.28 DexWrapper Contract Allows Setting Slippage to Zero

// Low

Description

The DexWrapper contract provides functionality to swap tokens. A crucial parameter in this process is slippage, which defines the maximum price movement a user is willing to tolerate for their swap. Currently, the contract includes a setSlippage function that allows the contract owner to update the slippage value. However, the contract does not prevent setting the slippage to zero. Allowing slippage to be set to zero can lead to swaps failing due to normal price fluctuations or expose users to potential front-running attacks, where attackers can manipulate the price to their advantage knowing the user's swap will accept any amount of slippage.


    function setSlippage(uint256 _newSlippage) external onlyOwner {
        slippage = _newSlippage;
    }
BVSS
Recommendation

To mitigate these risks, the DexWrapper contract should enforce a minimum slippage value. This can be achieved by adding a check in the setSlippage function to ensure that the slippage is not set to zero.


Remediation Plan: The issue is marked as “Not Applicable” due to the related code-base is deprecated.

4.29 Absence of Event Emission in Setter Functions Compromises Transparency and Auditability

// Informational

Description

The smart contract contains several setter functions (setPausedSid, setSynthFactory, setMasterSynthChef) that allow for critical updates to the contract's state, such as pausing a specific SID, updating the SynthFactory address, and setting a new MasterSynthChef. However, these functions do not emit events upon successful state changes. The absence of event emissions in these setter functions can significantly impact the transparency and auditability of the contract, making it challenging to track changes and updates in a decentralized and trustless manner.


    function setPausedSid(uint128 sid, bool enabled) public onlyRole(ADMIN_ROLE) {
        sidInfo[sid].isPausedSid = enabled;
    }

    function setSynthFactory(SynthFactory _synthFactory) public onlyRole(ADMIN_ROLE) {
        synthFactory = _synthFactory;
    }

    function setMasterSynthChef(MasterSynthChef _masterChef) public onlyRole(ADMIN_ROLE) {
        masterSynthChef = _masterChef;
    }
Score
Recommendation

Define and emit specific events within each setter function to log the changes.


Remediation Plan: The Entangle team acknowledged the issue.

4.30 TODO Left in the code

// Informational

Description

Several open TODO comments are identified in the contracts, indicating that their development is not finished.

./NglToken/BridgeRouter.sol:50:    /// @dev Calculates the bridge fee (TODO).
./NglToken/BridgeRouter.sol:52:        // TODO:
./test/SynthFactoryMock.sol:32:        // TODO: Translation
./oracle/OracleBalanceSpotter.sol:207:            // TODO: !!! apply new price in SynthState on all chains
./oracle/MasterSpotter.sol:48:        // TODO handle?
./SynthChefs/WombatSynthChef.sol:116:        // TODO: there will be some additional rewards after the last call
./MasterSynthChef.sol:35:    // TODO: probably not needed anymore...
./interfaces/SynthChefs/DODO/IDODOV2Pair.sol:36:    ) external returns (uint256 baseAmount, uint256 quoteAmount); // just a fucking meme TODO delete comment
Score
Recommendation

It is recommended to review the open TODO comments to make sure before release all required development steps are finished.


Remediation Plan: The Entangle team acknowledged the issue.

4.31 Missing/incomplete natspec can affect readability and ux

// Informational

Description

The contracts are missing/incomplete natspec which affects readability and UX.

Score
Recommendation

Consider adding Natspec to the functions/contracts.


Remediation Plan: The Entangle team acknowledged the issue.

4.32 Use Ownable2Step instead of Ownable

// Informational

Description

The current ownership transfer process for all the contracts inheriting from Ownable or OwnableUpgradeable involves the current owner calling the [transferOwnership()](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.8/contracts/access/Ownable.sol#L69-L72) function:


function transferOwnership(address newOwner) public virtual onlyOwner {

    require(newOwner != address(0), "Ownable: new owner is the zero address");

    _setOwner(newOwner);

}

If the nominated EOA account is not a valid account, it is entirely possible that the owner may accidentally transfer ownership to an uncontrolled account, losing the access to all functions with the onlyOwner modifier.

Score
Recommendation

It is recommended to implement a two-step process transfer ownership process where the owner nominates an account and the nominated account needs to call an acceptOwnership() function for the transfer of the ownership to fully succeed. This ensures the nominated EOA account is a valid and active account. This can be easily achieved by using OpenZeppelin's [Ownable2Step contract](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.8/contracts/access/Ownable2Step.sol) instead of Ownable


Remediation Plan: The Entangle team acknowledged the issue.

4.33 Incompatibility of Admin Role Management with Multisig Wallets in SynthFactory Smart Contract

// Informational

Description

The SynthFactory smart contract, as part of its design, incorporates role-based access control, leveraging the OpenZeppelin AccessControlUpgradeable for managing different roles such as ADMIN, MINTER, and PRICE_MANAGER. However, a potential issue arises with assigning and managing these roles, specifically the ADMIN role. Currently, the initialize function of the contract assigns the ADMIN and DEFAULT_ADMIN_ROLE roles to msg.sender (the address that deploys the contract). This straightforward setup could lead to centralized control and potential security risks, especially if the admin's private key is compromised. Moreover, the current implementation does not facilitate using multisig wallets for the ADMIN role, a common and more secure practice for administering key functions in mission-critical smart contracts.


https://github.com/Entangle-Protocol/entangle-lsd-protocol/blob/48e025e4ddf330e8ac710e82114db704a76d2857/contracts/SynthFactory.sol#L15


    function initialize(address _balanceManager) public initializer {
        __Ownable_init();
        __UUPSUpgradeable_init();
        _grantRole(ADMIN, msg.sender);
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        balanceManager = BalanceManager(_balanceManager);
    }

Score
Recommendation

It is recommended to define the address variable as a function parameter.


Remediation Plan: The Entangle team acknowledged the issue.

4.34 Unlocked Pragma

// Informational

Description

Contracts should be deployed with the same compiler version and flags used during development and testing. Locking the pragma helps to ensure that contracts do not accidentally get deployed using another pragma. For example, an outdated pragma version might introduce bugs that affect the contract system negatively.


Instances (1):
```solidity
File: interfaces/SynthChefs/Thena/IMasterChef.sol

3: pragma solidity >=0.7.0 <0.9.0;

```

Score
Recommendation

Consider locking the pragma version in the smart contracts. It is not recommended to use a floating pragma in production.

For example: pragma solidity 0.8.20;


Remediation Plan: The Entangle team solved the issue by deprecating the codebase.

Remediation Hash

4.35 Unoptimized Loops

// Informational

Description

When a loop iterates many times, it causes the amount of gas required to execute the function to increase significantly. In Solidity, excessive looping can cause a function to use more than the maximum allowed gas, which causes the function to fail.



``` File: Compounder.sol 21: for (uint i = 0; i < callConfigs.length; ++i) { 34: for (uint i = 0; i < underlyings.length; ++i) { ``` ``` File: NglToken/BaseEntangleToken.sol 87: for (uint256 i = 0; i < chainIds.length; ++i) { ``` ``` File: SynthChefs/CurveCompoundConvexSynthChef.sol 245: for (uint256 i = 0; i < pool.uTokens.length; i++) { 282: for (uint256 i; i < pool.uTokens.length; i++) { ``` ``` File: SynthChefs/FusionxSynthChef.sol 184: for (uint256 i = 0; i < rewardTokens.length; i++) { ``` ``` File: SynthChefs/FusionxSynthChefMainnet.sol 197: for (uint256 i = 0; i < rewards.length; ++i) { 205: for (uint256 i = 0; i < rewardTokens.length; i++) { ``` ``` File: SynthChefs/SpookySwapSynthChef.sol 212: for (uint i = 0; i < pool.poolTokens.length; i++) { ``` ``` File: SynthChefs/WombatSynthChef.sol 339: for (uint256 i = 0; i < additionalTokens.length; ++i) { ``` ``` File: dex-wrappers/DexWrapper.sol 82: for (uint i = 0; i < swapPath.length; ++i) { 119: for (uint256 i = 0; i < specifiedPaths.length; i++) { 184: for (uint256 i = 0; i < specifiedPaths.length; i++) { ``` ``` File: dex-wrappers/DodoDexWrapper.sol 130: for (uint i = 2; i < path.length; i++) { // skip first 2 addresses, because it is tokenFrom and tokenTo ``` ``` File: oracle/OracleStateSpotter.sol 197: chainIds[_chainId] = IdInfo(true, chains.length - 1); 227: sidIds[_sid] = IdInfo(true, sids.length - 1); 284: for (uint i = 0; i < chains.length; i++) { ```


Score
Recommendation

To reduce gas consumption, it is recommended to find ways to optimize the loop or potentially break the loop into smaller batches. The following pattern can also be used:


uint256 cachedLen = array.length;

for(uint i; i < cachedLen;){

unchecked {

++i;

}

}

Remediation Plan: The Entangle team acknowledged the issue.


4.36 Using `private` rather than `public` for constants, saves gas

// Informational

Description

If needed, the values can be read from the verified contract source code, or if there are multiple values there can be a single getter function that returns a tuple of the values of all currently-public constants. Saves 3406-3606 gas in deployment gas due to the compiler not having to create non-payable getter functions for deployment calldata, not having to store the bytes of the value outside of where it's used, and not adding another entry to the method ID table.

```

File: BalanceManager.sol

20: bytes32 public constant ADMIN = keccak256("ADMIN");

21: bytes32 public constant SPOTTER = keccak256("SPOTTER");

22: bytes32 public constant SELF = keccak256("SELF");

```

Score
Recommendation

Consider defining variable as private for gas optimization.


Remediation Plan: The Entangle team acknowledged the issue.

4.37 Use Custom Errors Instead Of Revert Strings To Save Gas

// Informational

Description

Custom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they’re hit by avoiding having to [allocate and store the revert string](https://blog.soliditylang.org/2021/04/21/custom-errors/#errors-in-depth). Not defining the strings also saves deployment gas.

Score
Recommendation

Consider replacing all revert strings with custom errors.


Remediation Plan: The Entangle team acknowledged the issue.

4.38 No Need To Initialize Variables With Default Values

// Informational

Description

Initialization to 0 or false is not necessary, as these are the default values in Solidity.


Score
Recommendation

Remove the initialization values of 0 or false.


Remediation Plan: The Entangle team acknowledged the issue.

4.39 Removal of Redundant console.log Statements from Codebase

// Informational

Description

The codebase currently contains several instances of console.log statements. While these logging statements can be invaluable during development and debugging phases, leaving them in the production code can lead to unnecessary clutter.

Score
Recommendation

Consider deleting console.log from the contracts.


Remediation Plan: The Entangle team acknowledged the issue.

4.40 Missing Access Control on withdraw Function in ATokenWrapper Contract

// Informational

Description

The ATokenWrapper contract provides a withdraw function that facilitates the withdrawal of all deposited funds from the AAVE protocol. This function directly interacts with the AAVE pool to withdraw assets to the contract itself. However, the function lacks a feature: an access control mechanism, such as an onlyCompounder modifier,


// SPDX-License-Identifier: BSL 1.1
pragma solidity ^0.8.15;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IAavePool {
    function withdraw(address asset, uint256 amount, address to) external returns (uint256);
}

interface IAToken {
    function UNDERLYING_ASSET_ADDRESS() external view returns (address);
    function POOL() external view returns (address);
}

contract ATokenWrapper {
    using SafeERC20 for IERC20;

    function withdraw(address token) external {
        // withdraws all deposited funds
        IAavePool(IAToken(token).POOL()).withdraw(IAToken(token).UNDERLYING_ASSET_ADDRESS(), type(uint).max, address(this));
    }

}
Score
Recommendation

Introduce an onlyCompounder modifier (or similarly named modifier) that restricts the execution of the withdraw function to addresses authorized to manage the contract's assets.


Remediation Plan: The Entangle team acknowledged the issue.

4.41 Removal of Hardhat Console Log Import in Mainnet Contracts

// Informational

Description

The import statement for hardhat/console.sol found in the Solidity contract is intended for development purposes, facilitating debugging during the development and testing phases by allowing developers to print variable values and contract states.

Score
Recommendation

Delete the import statement for hardhat/console.sol from the contract file. This change ensures that debugging tools are not inadvertently included in the production deployment process.


Remediation Plan: The Entangle team acknowledged the issue.

4.42 Redundant Import of SafeERC20 and IERC20 in ATokenWrapper Contract

// Informational

Description

The ATokenWrapper contract includes an import statement for @openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol, in addition to importing @openzeppelin/contracts/token/ERC20/IERC20.sol. Upon review of the contract's functionality, it is observed that the SafeERC20 library's functionality is not utilized within the contract's logic. The contract does not employ the safe methods provided by SafeERC20 for ERC20 token interactions, rendering this import unnecessary. This discrepancy introduces potential confusion regarding the contract's dependencies and suggests an opportunity to optimize the contract's code by removing unused imports.

Score
Recommendation

Since the SafeERC20 library's methods are not utilized within the ATokenWrapper contract, it is recommended to remove this import to clean up the code and clarify the contract's actual dependencies.


Remediation Plan: The Entangle team acknowledged the issue.

4.43 Deprecated Increaseallowance And Decreaseallowance

// Informational

Description

The token contracts currently include the increaseAllowance and decreaseAllowance functions. While these functions provide utility, they may introduce additional risks and are not aligned with the core EIP-20 specifications.

Here are some concerns:

- The increaseAllowance and decreaseAllowance functions are not defined within the original EIP-20 specifications. Including them in the primary ERC20 contract deviates from the standard.

- There have been instances where unsuspecting users were tricked into signing malicious increaseAllowance payloads, leading to significant financial losses. One such incident occurred recently where a user lost $24 million due to a deceptive payload (See transaction : [0xcbe7b32e62c7](https://etherscan.io/tx/0xcbe7b32e62c7d931a28f747bba3a0afa7da95169fcf380ac2f7d54f3a2f77913)).

Score
Recommendation

Consider removing increaseAllowance and decreaseAllowance from the primary token contracts. Instead, include them in an extension or auxiliary contract. This way, developers have the discretion to include these functions if they deem them necessary for their specific use case.


Remediation Plan: The Entangle team acknowledged the issue.

4.44 Obsolete SynapseBridge Contract Functionality

// Informational

Description

The contract contains a function, HandleLPStackingSynthSellAndWithdraw, which was designed to interact with the Synapse Bridge for executing liquidity pool (LP) stacking synth sell and withdrawal operations across chains. However, it has been identified that the SynapseBridge contract or its intended functionality has been deprecated or removed by the client, rendering this function obsolete. Continuing to include this function in the contract without the corresponding Synapse Bridge functionality can lead to confusion, potential security risks, and maintainability issues, as it references external dependencies and operations that no longer exist within the protocol.



    function HandleLPStackingSynthSellAndWithdraw(
        EntangleDexV2Events.LPStackingSynthSellAndWithdraw memory snw
    ) public onlyRole(MASTER) {
        //console.log("BalanceSpotter: HandleLPStackingSynthSellAndWithdraw");
        uint opId = newOperation("HandleLPStackingSynthSellAndWithdraw");

        uint64 chainId = SidLibrary.chainId(snw.sid);

        //console.log("BalanceSpotter: srcChainId %s, sid %s, lpAmount %s", chainId, snw.sid, snw.lpAmount);
        //console.log("BalanceSpotter: destChainId %s, recipient %s", snw.chainId, snw.recipient);

        SynapseBridgeParams memory bridgeParams = SynapseBridgeParams({
            to: snw.recipient,
            chainId: snw.chainID,
            NUSDToken: chainInfo[chainId].nusd,
            tokenIndexFrom: chainId == 250 ? 2 : 1, // token index 2 only on ftm
            tokenIndexTo: 0, // Synapse nusd
            dx: 0, // will set in balanceManager
            minDy: 0,
            deadline: type(uint64).max,
            swapTokenIndexFrom: 0, // Synapse nusd
            swapTokenIndexTo: snw.chainID == 250 ? 2 : 1,
            swapMinDy: 0,
            swapDeadline: type(uint64).max
        });

        BridgeParams memory bParamss = BridgeParams(0, abi.encode(bridgeParams));

        bytes4 withdrawLPAndBridgeSelector = bytes4(keccak256("withdrawLPFromChefAndBridge(bytes)"));
        proposer.propose(
            protocolId,
            chainId,
            chainInfo[chainId].balanceManager,
            withdrawLPAndBridgeSelector,
            abi.encode(snw.sid, snw.lpAmount, snw.synthAmount, bParamss, opId)
        );
    }
Score
Recommendation

Delete the HandleLPStackingSynthSellAndWithdraw function from the contract to eliminate references to the non-existent Synapse Bridge functionality. This cleanup will help focus on the active features and reduce maintenance overhead.


Remediation Plan: The Entangle team acknowledged the issue.

4.45 Non-Functional HandleBridgeExecuted Function

// Informational

Description

The HandleBridgeExecuted function within the MasterSpotter contract is intended to handle events related to the execution of bridge operations. However, upon inspection, it is observed that the function merely decodes the event parameters without performing any subsequent actions or updates to the contract state. This lack of functionality suggests that the intended purpose of processing bridge execution events is not being fulfilled.


   function HandleBridgeExecuted(bytes calldata b) public onlyRole(SPOTTER) {
       (, , , , bytes memory eventParams) = abi.decode(b, (bytes32, uint256, uint256, bytes32, bytes));
       BalanceManagerEvents.BridgeExecuted memory bridgeExecuted = BalanceManagerEvents.DecodeBridgeExecuted(eventParams);
       return;
   }
Score
Recommendation

Determine the original intent behind the HandleBridgeExecuted function. If it was meant to update the contract's state or perform specific actions based on bridge execution events, define the specific requirements and desired outcomes.


Remediation Plan: The Entangle team acknowledged the issue.

4.46 Redundant Functionality in HandleWithdrawLpAndExtract and HandleWithdrawLP

// Informational

Description

The MasterSpotter contract contains two functions, HandleWithdrawLpAndExtract and HandleWithdrawLP, which appear to serve similar purposes related to handling the withdrawal of liquidity provider (LP) tokens. Upon closer inspection, it is evident that both functions are designed to update the state of LP balances within the system based on withdrawal events. However, the existence of two separate functions for handling what essentially amounts to the same action introduces redundancy and potential confusion within the contract's design.

The presence of two functions with overlapping functionality increases the complexity of the contract, making it harder for developers and auditors to understand and maintain the codebase.

Score
Recommendation

Review the contract's requirements and operational logic to determine whether both HandleWithdrawLpAndExtract and HandleWithdrawLP are genuinely needed. If their functionalities overlap significantly, consider retaining only one of the functions.


Remediation Plan: The Entangle team acknowledged the issue.

4.47 Ambiguity in Debug Functions debug_ClearSynthState and debug_ClearChefState

// Informational

Description

In the OracleStateSpotter contract, two functions named debug_ClearSynthState and debug_ClearChefState are included with the purpose of clearing state information for synth and chef states, respectively. These functions are marked as debug, but it is not explicitly stated whether they are intended for use in production environments or solely for testing/debugging purposes. The presence of such functions in a contract deployed to a live network raises concerns regarding their potential misuse or unintended consequences.

Score
Recommendation

Clearly document the purpose of these debug functions. If they are intended for production use, explain their use cases and the safeguards in place to prevent misuse.


Remediation Plan: The Entangle team acknowledged the issue.

4.48 Inconsistent Version Checking in StateParamsLibrary

// Informational

Description

The StateParamsLibrary provides functionalities to decode various state parameters from bytes based on their version. However, there's an inconsistency observed in the version check across different decoding functions:

  • Both decodeESynthStateParams and decodeEChefStateParams functions strictly require the version to be 1 (i.e., _version == 1), rejecting any other version.

  • On the other hand, decodeEDexStateParams allows any version less than or equal to 1 (i.e., _version <= 1), accepting version 0 and 1.

This inconsistency in version checking can lead to potential issues in future updates or when interacting with data encoded with different versions, especially as the system evolves and new versions are introduced.

Score
Recommendation

Align the version checking mechanism across all decoding functions.


Remediation Plan: The Entangle team acknowledged the issue.

4.49 Inconsistent Use of OwnableUpgradeable and AccessControlUpgradeable Across Contracts

// Informational

Description

Across various contracts in the codebase, including DexWrapper and others, there is an inconsistent application of OwnableUpgradeable and AccessControlUpgradeable from OpenZeppelin's upgradeable contracts suite. Both modules serve to manage access controls and permissions, yet their concurrent deployment within the same contracts can introduce redundancy and potential for confusion regarding the hierarchy of control and permissions. This duality in access control mechanisms could complicate the governance and operational security of the contracts, making it harder to reason about the authority required for executing sensitive functions.

The coexistence of OwnableUpgradeable and AccessControlUpgradeable complicates the contract's access control scheme, potentially leading to governance issues. It raises questions about the clear delineation of roles and permissions, especially in scenarios requiring precise control over contract operations. This setup might also increase the risk of errors in permission assignments and checks, potentially affecting the contract's security posture.

Score
Recommendation

Assessing whether OwnableUpgradeable functionalities are strictly necessary or if they can be fully replaced by AccessControlUpgradeable's more granular permissions system.


Remediation Plan: The Entangle team acknowledged the issue.

4.50 Unnecessary Use of Assembly for Retrieving chainid

// Informational

Description

In the SynthFactory contract, there's a specific implementation for retrieving the current chain ID using inline assembly. The function __chainId() directly accesses the chainid opcode to obtain the chain ID. While this approach is valid and works correctly, it introduces unnecessary complexity and direct assembly use where a higher-level, safer alternative exists. Starting from Solidity 0.8.0, the block.chainid global variable is available and provides a straightforward, built-in method to access the chain ID, mitigating potential risks associated with inline assembly and aligning with best practices for code clarity and security.

Score
Recommendation

Replace the __chainId() function with direct usage of block.chainid to obtain the current chain ID. This change simplifies the contract, enhances code clarity, and leverages Solidity's built-in security features.


Remediation Plan: The Entangle team acknowledged the issue.

4.51 Missing Zero Address Validation in Contract Setters

// Informational

Description

Several contracts within the system lack necessary validations to prevent the assignment of the zero address (0x0) in their setter functions, specifically in setBalanceManager and setFeeCollector methods. This oversight could lead to issues, where critical functionalities could be disabled or misdirected to an address that is not usable, resulting in loss of control over the contract's intended behaviors. The absence of such checks increases the risk of accidental or malicious misconfiguration, potentially leading to loss of funds or access control.

Score
Recommendation

It is recommended to implement checks within the setter functions to ensure that the address being set is not the zero address.


Remediation Plan: The Entangle team acknowledged the issue.

5. Automated Testing

Introduction

Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contracts in scope. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified the smart contracts in the repository and was able to compile them correctly into their abis and binary format, Slither was run against the contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.

All issues identified by Slither were proved to be false positives or have been added to the issue list in this report.


Slither Output 1
Slither Output 2
Slither Output 3
Slither Output 4
Slither Output 5

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.

// Download the full report

* Use Google Chrome for best results

** Check "Background Graphics" in the print settings if needed

© Halborn 2024. All rights reserved.