Halborn Logo

Core & Bridge - YieldFi


Prepared by:

Halborn Logo

HALBORN

Last Updated 11/14/2024

Date of Engagement by: October 22nd, 2024 - November 7th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

15

Critical

0

High

3

Medium

1

Low

4

Informational

7


1. Introduction

YieldFi engaged Halborn to conduct a security assessment on their smart contracts beginning on October 22nd, 2024, and ending on November 7th, 2024. The security assessment was scoped to the smart contracts provided in the YieldFiLabs/contracts GitHub repository. Commit hashes and further details can be found in the Scope section of this report.

2. Assessment Summary

Halborn was provided 2 weeks for the engagement, and assigned one full-time security engineer to review the security of the smart contracts in scope. The engineer is a blockchain and smart contract security expert 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 multiple security issues, that were mostly addressed by the YieldFi team. The main security issues were:

    • Logic error in blacklist checks.

    • Insufficient _receiverGas validation leads to loss of funds.

    • Denial of Service via cooling period reset.

    • Missing oracle answer validation.

    • Centralization risk for trusted administrators.

    • Insufficient epoch management

    • Use SafeERC20.

    • Lack of minimum cooling period enforcement.


3. 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).

4. 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.

4.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

4.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}

4.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

5. SCOPE

Files and Repository
(a) Repository: contracts
(b) Assessed Commit ID: 62b61b3
(c) Items in scope:
  • ./core/Yield.sol
  • ./core/interface/ILockBox.sol
  • ./core/interface/IMinter.sol
↓ Expand ↓
Out-of-Scope:
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

3

Medium

1

Low

4

Informational

7

Security analysisRisk levelRemediation Date
Logic error in Blacklist checksHighSolved - 11/12/2024
Insufficient _receiverGas validation leads to loss of fundsHighSolved - 11/12/2024
Denial of Service via cooling period resetHighSolved - 11/12/2024
Missing oracle answer validationMediumSolved - 11/12/2024
Centralization risk for trusted administratorsLowSolved - 11/12/2024
Insufficient epoch managementLowSolved - 11/12/2024
Use SafeERC20LowSolved - 11/12/2024
Lack of minimum cooling period enforcementLowSolved - 11/12/2024
Misplaced `nonReentrant` modifierInformationalSolved - 11/12/2024
PUSH0 and newer opcodes might not be supported in all chainsInformationalAcknowledged - 11/12/2024
Modifiers used once can be packed into the functionInformationalSolved - 11/12/2024
Use custom errorsInformationalAcknowledged - 11/12/2024
Missing event emission after important operationsInformationalSolved - 11/12/2024
Redundant balance checksInformationalSolved - 11/12/2024
Expensive iterationsInformationalSolved - 11/12/2024

7. Findings & Tech Details

7.1 Logic error in Blacklist checks

// High

Description

Multiple contracts in-scope are using logical OR (||) operator instead of logical AND (&&) when checking if msg.sender or receiver is blacklisted. This allows blacklisted users to perform actions as long as either party is not blacklisted.


- contracts/core/SToken.sol

        require(!IBlackList(administrator).isBlackListed(msg.sender) || !IBlackList(administrator).isBlackListed(receiver), "blacklisted");

        require(!IBlackList(administrator).isBlackListed(msg.sender) || !IBlackList(administrator).isBlackListed(receiver), "blacklisted");

        require(!IBlackList(administrator).isBlackListed(from) || !IBlackList(administrator).isBlackListed(to), "blacklisted");


- contracts/core/YToken.sol

        require(!IBlackList(administrator).isBlackListed(caller) || !IBlackList(administrator).isBlackListed(receiver), "blacklisted");

        require(!IBlackList(administrator).isBlackListed(from) || !IBlackList(administrator).isBlackListed(to), "blacklisted");


- contracts/core/STokenL2.sol

        require(!IBlackList(administrator).isBlackListed(from) || !IBlackList(administrator).isBlackListed(to), "blacklisted");

Blacklisted users can deposit, withdraw, transfer tokens and perform basically any operation that a non-blacklisted low-privileged user could perform, bypassing compliance measures.

Proof of Concept

In order to reproduce this issue, refer to the following Foundry test, which will perform the following actions:

  • Call the blacklistUsers in the Administrator contract, passing a valid array of addresses.

  • As a Black-listed user, call the claim function, which is using the OR (||) logic operator.

  • The call will succeed, even if one of the addresses is blacklisted.


  • PoC Code:

    function testBlacklistedUserCanClaim() public {
        // Deal enough tokens for participants
        deal(address(usdt), user, 3_000_000 * 1e6);
        deal(address(stoken), user, 3_000_000 * 1e18);
        deal(address(usdt), address(stoken), 1_000_000 * 1e25);

        // Blacklist user
        vm.startPrank(deployer);
        address[] memory blackListedUsers = new address[](1);
        blackListedUsers[0] = u1;
        administrator.blackListUsers(blackListedUsers);
        vm.stopPrank();

        // User attempts to deposit
        vm.startPrank(user);
        usdt.approve(address(stoken), type(uint256).max - 1);
        stoken.deposit(1_000_000 * 1e6, user);

        stoken.withdrawRequest(666_666 * 1e18, user, user);

        (, uint256 withdrawalAmount) = stoken.withdrawals(user);
        console.log("Withdraw request amount is:");
        console.logUint(withdrawalAmount);

        uint256 sTokenContractUSDTBalanceBeforeClaim = usdt.balanceOf(address(stoken));
        console.log("sToken Contract USDT Balance Before Claim is:");
        console.logUint(sTokenContractUSDTBalanceBeforeClaim);
        vm.stopPrank();

        // Fast forward time to after cooling period
        vm.warp(block.timestamp + 1 days + 1);  

        vm.startPrank(u1);
        stoken.claim(user);
        vm.stopPrank();
    }

  • Logs:

blacklistLogicError(01)
BVSS
Recommendation

It is recommended to change the logical OR (||) operator for the logical AND (&&) operator, in order to check both parties of the transaction against the blacklist.

Remediation

SOLVED: The YieldFi team has solved this issue as recommended. The commit hash for reference is 9e2e3842a08a08f0d1226300ef7eea3f8e68703d.

Remediation Hash

7.2 Insufficient _receiverGas validation leads to loss of funds

// High

Description

The send functions in the BridgeLR and BridgeMB contracts, as well as the quote function in the Bridge contract, allow users to specify the _receiverGas parameter, which determines how much gas should be used when calling lzReceive on the destination endpoint.

The current condition require(_receiverGas > 0, "!gas"); is not sufficient to prevent users from providing a very small value for _receiverGas. This can lead to a situation where the safeTransferFrom function is executed on the source chain, but the message is not processed on the destination chain due to insufficient gas, resulting in a loss of funds.


- contracts/bridge/BridgeMB.sol

    function send(
        address _srcToken,
        uint32 _dstId,
        address _receiver,
        uint256 _amount,
        uint128 _receiverGas
    ) external payable notBlacklisted(msg.sender) notBlacklisted(_receiver) notPaused {
        require(_amount > 0, "!amount");
        require(tokens[_srcToken][_dstId] != address(0), "!desToken");
        require(IERC20(_srcToken).balanceOf(msg.sender) >= _amount, "!balance");
        require(_receiver != address(0), "!receiver");
        require(_receiverGas > 0, "!gas");
      
        IMinter(_srcToken).burn(msg.sender, _amount);

        bytes memory _options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(_receiverGas, 0);

        bytes memory _encodedMessage = abi.encode(
            _dstId,
            _receiver,
            tokens[_srcToken][_dstId],
            _amount,
            Constants.BRIDGE_SEND_HASH
        );

        _lzSend(
            _dstId,
            _encodedMessage,
            _options,
            // Fee in native gas and ZRO token.
            MessagingFee(msg.value, 0),
            // Refund address in case of failed source message.
            payable(msg.sender)
        );
    }


- contracts/bridge/BridgeLR.sol

    function send(
        address _srcToken,
        uint32 _dstId,
        address _receiver,
        uint256 _amount,
        uint128 _receiverGas
    ) external payable notBlacklisted(msg.sender) notBlacklisted(_receiver) notPaused {
        require(_amount > 0, "!amount");
        require(lockboxes[_srcToken] != address(0), "!token !lockbox");
        require(IERC20(_srcToken).balanceOf(msg.sender) >= _amount, "!balance");
        require(_receiver != address(0), "!receiver");
        require(tokens[_srcToken][_dstId] != address(0), "!destination");
        require(_receiverGas > 0, "!gas");

        bytes memory _options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(_receiverGas, 0);
        bytes memory _encodedMessage = abi.encode(
            _dstId,
            _receiver,
            tokens[_srcToken][_dstId],
            _amount,
            Constants.BRIDGE_SEND_HASH
        );

        // Lock the tokens
        IERC20(_srcToken).safeTransferFrom(msg.sender, lockboxes[_srcToken], _amount);
        emit ILockBox.Lock(msg.sender, _srcToken, msg.sender, _amount);

        // Sends the message to the destination endpoint
        _lzSend(
            _dstId,
            _encodedMessage,
            _options,
            // Fee in native gas and ZRO token.
            MessagingFee(msg.value, 0),
            // Refund address in case of failed source message.
            payable(msg.sender)
        );
    }


- contracts/bridge/Bridge.sol

    function quote(
        uint32 _dstEid,
        bytes memory _payload,
        bool _payInLzToken,
        uint128 _receiverGas
    ) public view returns (MessagingFee memory fee) {
        bytes memory _options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(_receiverGas, 0);
        fee = _quote(_dstEid, _payload, _options, _payInLzToken);
    }
Proof of Concept

In order to reproduce this vulnerability, call the send function passing as function parameter a small value for _receiverGas, such as 1 wei. The transaction will not revert in source chain, effectively burning user's tokens. However, it will not be processed in the destination chain, considering insufficient gas provided.


  • PoC Code:

    function testSendWithLowReceiverGas() public {
        vm.startPrank(user);

        // Approve the bridge to spend user's tokens
        token.approve(address(bridgeSrc), 100 * 1e6); // User wants to send 100 tokens

        // Set low _receiverGas
        uint128 receiverGas = 1;

        // Capture the balance before sending
        uint256 userBalanceBefore = token.balanceOf(user);

        // Execute the send function
        bridgeSrc.send{value: 0}(
            address(token),    // _srcToken
            100,               // _dstId
            user,              // _receiver
            100 * 1e6,         // _amount
            receiverGas        // _receiverGas (set to 1)
        );

        // Capture the balance after sending
        uint256 userBalanceAfter = token.balanceOf(user);

        vm.stopPrank();

        // Assert that the user's tokens have been burned
        assertEq(userBalanceAfter, userBalanceBefore - 100 * 1e6, "Tokens were not burned");
    }

  • Logs:

insufficientGas(01)
BVSS
Recommendation

It is recommended to enforce a minimum _receiverGas value in order to ensure transactions will be processed in the destination chain.

Remediation

SOLVED: The YieldFi team has solved this issue as recommended. The commit hash for reference is 9e2e3842a08a08f0d1226300ef7eea3f8e68703d.

Remediation Hash

7.3 Denial of Service via cooling period reset

// High

Description

The withdrawRequest function is permissionless, allowing anyone to request a withdrawal on behalf of an owner to a receiver, as long as the msg.sender has sufficient allowance. This can lead to a Denial of Service (DoS) condition, even if the msg.sender has a relatively small amount of tokens.

Each withdrawal request resets the cooling period and accumulates the withdrawal amount, as indicated in the following code snippet:


- contracts/core/SToken.sol

withdrawals[receiver].coolingPeriod = block.timestamp + COOLING_PERIOD;

This means that even a minimal request of 1 wei, performed thousands of times (still less than 1 token, i.e., 1e18), would keep resetting the cooling period.


An attacker can continuously reset the cooling period by making new withdrawal requests, preventing users from claiming their funds.

Proof of Concept

To reproduce this issue, deposit as u1 and call multiple times the withdrawRequest as user (with sufficient allowance). This will effectively increase the cooldown period for u1, leading to a denial of service.


  • PoC Code:

    function testCoolingPeriodNotResetOnMultipleRequests() public {
        // User deposits and requests withdrawal
        vm.startPrank(u1);
        usdt.approve(address(stoken), 1_000_000e6);
        stoken.approve(address(user), 1_000_000 * 1e18);
        stoken.deposit(1_000_000e6, u1);
        stoken.withdrawRequest(500_000e18, u1, u1);
        (uint256 cooldownBefore, ) = stoken.withdrawals(u1);
        vm.stopPrank();

        vm.startPrank(user);
        
        for (uint8 i = 0; i < type(uint8).max - 1; i++) {
            // Warp 10 seconds ahead
            vm.warp(block.timestamp + 10);
            stoken.withdrawRequest(1, u1, u1);
        }
        
        vm.stopPrank();

        // Check that the cooling period is greater
        (uint256 cooldownAfter, ) = stoken.withdrawals(u1);

        assertGt(cooldownAfter, cooldownBefore);
    }

  • Logs:

dosCoolingReset(01)
BVSS
Recommendation

It is recommended to review the cooldown mechanism, and also the permissionless property of the claim function, in order to prevent malicious users to DoS legitimate transactions (claims).

Remediation

SOLVED: The YieldFi team has solved this issue by implementing a NFT receipt mechanism. The commit hash for reference is 9e2e3842a08a08f0d1226300ef7eea3f8e68703d.

Remediation Hash

7.4 Missing oracle answer validation

// Medium

Description

The YTokenL2.sol contract calls the getPrice function from the oracle but does not validate the returned price against zero. This lack of validation can lead to a division by zero error in both the deposit and withdraw functions. Specifically, if the price returned by the oracle is zero, the calculation of shares will result in a division by zero.


- contracts/core/YTokenL2.sol

        uint256 _price = IOracle(oracle).getPrice(address(this));
        shares = (sAmount * Constants.PINT) / _price ;
        _mint(receiver, shares);

This can lead to a scenario of consistent reverts because of [Revert] panic: division or modulo by zero (0x12).

Proof of Concept

In order to reproduce this vulnerability, it is needed to mock an Oracle answer of 0 (zero). Then, call the withdraw or deposit functions in the affected contract. The transaction will revert (panic), because of division by zero.


  • PoC Code:

    function testDepositWithZeroPrice() public {
        vm.startPrank(user);

        // Approve YTokenL2 to spend user's USDT
        usdt.approve(address(yTokenL2), 100 * 1e6);

        // Attempt to deposit USDT
        uint256 depositAmount = 100 * 1e6; // 100 USDT

        // Capture balances before deposit
        uint256 userUSDTBalanceBefore = usdt.balanceOf(user);
        uint256 sTokenUSDTBalanceBefore = usdt.balanceOf(address(sToken));

        // Expect the transaction to revert due to division by zero
        vm.expectRevert();

        // Call deposit function
        yTokenL2.deposit(address(usdt), depositAmount, user);

        vm.stopPrank();

        // Check balances after failed deposit
        uint256 userUSDTBalanceAfter = usdt.balanceOf(user);
        uint256 sTokenUSDTBalanceAfter = usdt.balanceOf(address(sToken));

        // Assert that user's USDT balance decreased
        assertEq(
            userUSDTBalanceAfter,
            userUSDTBalanceBefore - depositAmount,
            "User's USDT balance should have decreased"
        );

        // Assert that sToken received the USDT
        assertEq(
            sTokenUSDTBalanceAfter,
            sTokenUSDTBalanceBefore + depositAmount,
            "sToken should have received the USDT"
        );

        // Assert that user's YTokenL2 balance did not increase
        uint256 userYTokenBalance = yTokenL2.balanceOf(user);
        assertEq(userYTokenBalance, 0, "User's YTokenL2 balance should be zero");
    }

  • Logs:

panicDivisionByZero(01)
BVSS
Recommendation

To resolve this issue, add a check to ensure that the price returned by the oracle is not zero before performing the division. If the price is zero, the transaction should be reverted.

Remediation

SOLVED: The YieldFi team has solved this issue as recommended. The commit hash for reference is 9e2e3842a08a08f0d1226300ef7eea3f8e68703d.

Remediation Hash

7.5 Centralization risk for trusted administrators

// Low

Description

Several contracts within the scope of this audit use modifiers to restrict certain functions to administrators, owners, and other high-privileged parties. These functions involve direct interactions with token amounts, such as transferring or managing funds.

This design introduces a significant centralization risk, as malicious administrators can potentially drain the protocol. Additionally, if the private keys of these privileged accounts are leaked, it could lead to permanent loss of funds.

BVSS
Recommendation

Use multi-signature wallets for administrative functions. This ensures that multiple parties must approve any administrative action, reducing the risk of a single malicious actor or leaked private key.

Remediation

SOLVED: The YieldFi team has solved this issue as recommended, using multi-signature wallets.

Remediation Hash

7.6 Insufficient epoch management

// Low

Description

In the Yield contract, the distributeYield function is checking if the provided payload.epoch is greater than the last recorded epoch for the receiver. This allows epochs to be skipped.

Because of missing input validation, if the rewarder submits a payload with a significantly larger epoch value, future legitimate rewards will be unclaimable because the epoch has been artificially advanced.


- contracts/core/Yield.sol

    function distributeYield(bytes calldata data, bytes memory proofSignature) external notPaused onlyRewarder {
        bytes32 proofHash = keccak256(data);
        require(!trxns[proofHash], "!new txcn");
        validate(proofHash, proofSignature);

        RewardPayload memory payload = Codec.decodeReward(data);

        require(payload.epoch > epoch[payload.receiver], "!epoch");

        trxns[proofHash] = true;
        epoch[payload.receiver] = payload.epoch;
        
        if (payload.rewardType == 1) { // profit 
             _distributeYield(payload.receiver, payload.amount, true);
            profit[payload.receiver] += payload.amount;
        } else { // loss
            _distributeYield(payload.receiver, payload.amount, false);
            loss[payload.receiver] += payload.amount;
        }

        emit DistributeYield(IERC4626(payload.receiver).asset(), payload.receiver, payload.amount, payload.rewardType == 1);
    }
BVSS
Recommendation

It is recommended to add a more granular control, enforcing sequential epochs. This can be achieved by modifying the require statement as follows:

require(payload.epoch == epoch[payload.receiver] + 1, "!epoch");
Remediation

SOLVED: The YieldFi team has solved this issue as recommended. The commit hash for reference is 9e2e3842a08a08f0d1226300ef7eea3f8e68703d.

Remediation Hash

7.7 Use SafeERC20

// Low

Description

The LockBox contract within the scope of this audit uses the standard ERC20 function approve, which may not behave as expected in all scenarios.

To ensure robust and secure interactions with ERC20 tokens, it is recommended to use OpenZeppelin's SafeERC20 library. This library provides wrapper functions that include additional checks and safeguards, ensuring that token transfers and other operations are performed safely and correctly.

BVSS
Recommendation

It is recommended to use the SafeERC20 method forceApprove, instead of unsafe ERC20 approve.

Remediation

SOLVED: The YieldFi team has solved this issue as recommended. The commit hash for reference is 9e2e3842a08a08f0d1226300ef7eea3f8e68703d.

Remediation Hash

7.8 Lack of minimum cooling period enforcement

// Low

Description

During the analysis of the SToken contract, it was identified that the setCoolingPeriod function allows setting the cooling period to very low values, which might undermine its purpose.


- contracts/core/SToken.sol

    function setCoolingPeriod(uint256 period) external onlyAdmin {
        require(period > 0 && period <= Constants.MAX_COOLDOWN_PERIOD, "!period");
        COOLING_PERIOD = period;
    }
BVSS
Recommendation

It is recommended to define a state variable for a reasonable minimum cooling period.

Remediation

SOLVED: The YieldFi team has solved this issue as recommended. The commit hash for reference is 9e2e3842a08a08f0d1226300ef7eea3f8e68703d.

Remediation Hash

7.9 Misplaced `nonReentrant` modifier

// Informational

Description

Some contracts in-scope are utilizing the ReentrancyGuard from OpenZeppelin for reentrancy locks (mutex), which is best practice. However, the nonReentrant modifier should be placed before all other modifiers, to effectively prevent reentrancy on the function's modifiers.


- contracts/core/LockBox.sol

function unlock(address token, address to, uint256 amount) external notPaused nonReentrant onlyBridge {

- contracts/core/SToken.sol

) public notPaused nonReentrant returns(uint256 sAmount) {

- contracts/core/SToken.sol

) public notPaused nonReentrant {

- contracts/core/SToken.sol

) public notPaused nonReentrant {

- contracts/core/STokenL2.sol

) public notPaused nonReentrant returns(uint256 sAmount) {

- contracts/core/YToken.sol

function transferInRewards(uint256 amount, bool profit) external notPaused nonReentrant onlyYield {

- contracts/core/YToken.sol

) public notPaused nonReentrant returns(uint256 shares) {

Score
Recommendation

It is recommended to place the nonReentrant modifier before all other modifiers.

Remediation

SOLVED: The YieldFi team has solved this issue as recommended. The commit hash for reference is 9e2e3842a08a08f0d1226300ef7eea3f8e68703d.

Remediation Hash

7.10 PUSH0 and newer opcodes might not be supported in all chains

// Informational

Description

Solc compiler version 0.8.20 switches the default target EVM version to Shanghai. The generated bytecode will include PUSH0 opcodes. The recently released Solc compiler version 0.8.25 switches the default target EVM version to Cancun, so it is also important to note that it also adds-up new opcodes such as TSTORE, TLOAD and MCOPY.

Be sure to select the appropriate EVM version in case you intend to deploy on a chain apart from Ethereum mainnet, like L2 chains that may not support PUSH0, TSTORE, TLOAD and/or MCOPY, otherwise deployment of your contracts will fail.

Score
Recommendation

It is important to consider the targeted deployment chains before writing immutable contracts because, in the future, there might exist a need for deploying the contracts in a network that could not support new opcodes from Shanghai or Cancun EVM versions.

Remediation

ACKNOWLEDGED: The YieldFi team has acknowledged this finding.

7.11 Modifiers used once can be packed into the function

// Informational

Description

The Access and YToken contracts contain modifiers that are invoked only once within the contracts. While modifiers are useful for reusing code and improving readability, their use in this context is inefficient and can make the code harder to understand.

Integrating the modifier's logic directly into the function's body can improve performance and maintainability.



- contracts/administrator/Access.sol

modifier onlyBridge() {


- contracts/core/YToken.sol

modifier onlyYield() {
Score
Recommendation

It is recommended to integrate the modifier's logic directly into the function's body.

Remediation

SOLVED: The YieldFi team has solved this issue as recommended. The commit hash for reference is 9e2e3842a08a08f0d1226300ef7eea3f8e68703d.

Remediation Hash

7.12 Use custom errors

// Informational

Description

In Solidity smart contract development, replacing hard-coded revert message strings with the Error() syntax is an optimization strategy that can significantly reduce gas costs. Hard-coded strings, stored on the blockchain, increase the size and cost of deploying and executing contracts.

The Error() syntax allows for the definition of reusable, parameterized custom errors, leading to a more efficient use of storage and reduced gas consumption. This approach not only optimizes gas usage during deployment and interaction with the contract but also enhances code maintainability and readability by providing clearer, context-specific error information.

Score
Recommendation

It is recommended to replace hard-coded revert strings in require statements for custom errors, for example:


error ConditionNotMet();
Remediation

ACKNOWLEDGED: The YieldFi team has acknowledged this finding.

7.13 Missing event emission after important operations

// Informational

Description

The contracts within the scope of this audit do not emit events after performing important operations, such as transferring funds, updating state variables, or executing critical functions.

Event emission is crucial for transparency and auditability, as it allows external observers and off-chain services to track and verify the contract's state changes. The absence of event emissions can hinder monitoring, debugging, and integration with other systems, potentially leading to reduced trust and security in the contract's operations.

The following functions are not emitting relevant events:

  • Bridge::setAdministrator

  • Access:setAdministrator

  • MPC::setMPCs

  • YToken::rescue


Score
Recommendation

It is recommended to emit events after performing important operations. This will enhance transparency and allow external observers to track and verify the contract's state of changes.

Remediation

SOLVED: The YieldFi team has solved this issue as recommended. The commit hash for reference is d051ac1c4a9e6ac736f6586a21ec582b4144b184.

Remediation Hash

7.14 Redundant balance checks

// Informational

Description

Multiple contracts within the scope of this audit perform redundant balance checks immediately before transferring funds using SafeERC20 methods.

These balance checks are unnecessary because the SafeERC20 methods already include internal checks to ensure that the transfer does not exceed the available balance. These redundant checks consume additional gas and can be safely removed to optimize the contract's performance.


- contracts/bridge/BridgeLR.sol

        require(IERC20(_srcToken).balanceOf(msg.sender) >= _amount, "!balance");


- contracts/bridge/BridgeMB.sol

        require(IERC20(_srcToken).balanceOf(msg.sender) >= _amount, "!balance");


- contracts/core/SToken.sol

        require(IERC20(usdt).balanceOf(msg.sender) >= amount, "!amount");


- contracts/core/STokenL2.sol

        require(IERC20(usdt).balanceOf(msg.sender) >= amount, "!amount");


- contracts/core/YToken.sol

        require(IERC20(usdt).balanceOf(msg.sender) >= amount, "!amount");
Score
Recommendation

It is recommended to remove the redundant balance checks before calling the SafeERC20 methods.

The SafeERC20 methods already handle balance checks internally, ensuring that the transfer does not exceed the available balance.

Remediation

SOLVED: The YieldFi team has solved this issue as recommended. The commit hash for reference is d051ac1c4a9e6ac736f6586a21ec582b4144b184.

Remediation Hash

7.15 Expensive iterations

// Informational

Description

Some of the smart-contracts in-scope contain loops that perform increment operations using checked arithmetic and the post-increment operator (i++).

In Solidity versions 0.8.0 and above, arithmetic operations are checked for over/underflows by default, which introduces additional gas costs. Additionally, using the post-increment operator is less gas-efficient, compared to the pre-increment operator (++i).

Score
Recommendation

For enhanced gas consumption, consider implementing loops following the example:

for (uint256 i; i < n;) {
   
    /// Loop Logic

    unchecked {
        ++i
    }

}
Remediation

SOLVED: The YieldFi team has solved this issue as recommended. The commit hash for reference is d051ac1c4a9e6ac736f6586a21ec582b4144b184.

Remediation Hash

8. Automated Testing

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.


slither(01)

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

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.

© Halborn 2024. All rights reserved.