Halborn Logo

stkBNB - Persistence


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/26/2024

Date of Engagement by: July 10th, 2022 - August 3rd, 2022

Summary

100% of all REPORTED Findings have been addressed

All findings

9

Critical

0

High

0

Medium

1

Low

4

Informational

4


1. INTRODUCTION

\client engaged Halborn to conduct a security audit on their smart contracts beginning on 2022-07-10 and ending on 2022-08-03. The security assessment was scoped to the smart contracts provided to the Halborn team.

2. AUDIT SUMMARY

The team at Halborn was provided two weeks for the engagement and assigned a full-time security engineer to audit the security of the smart contract. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.

The purpose of this audit is to:

    • Ensure that smart contract functions operate as intended.

    • Identify potential security issues with the smart contracts.

In summary, Halborn identified some security risks that were mostly addressed by the \client team.

3. TEST APPROACH & 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 audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the contracts' solidity code and can quickly identify items that do not follow security best practices. The following phases and associated tools were used throughout the term of the audit:

    • Research into architecture and purpose.

    • Smart contract manual code review and walkthrough.

    • 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. (Hardhat).

    • Static Analysis of security for scoped contract, and imported functions manually.

    • Testnet deployment (Ganache).

4. SCOPE

IN-SCOPE:

The security assessment was scoped to the following smart contracts on the prepared branch for the audit:

Commit ID: bde7ee900aba18aecd0e8e0c0497121540dd5abb.

FIX Commit TREE/ID :

Main Branch Commit ID

5. RISK METHODOLOGY

Vulnerabilities or issues observed by Halborn are ranked based on the risk assessment methodology by measuring the LIKELIHOOD of a security incident and the IMPACT should an incident occur. This framework works for communicating the characteristics and impacts of technology vulnerabilities. The quantitative model ensures repeatable and accurate measurement while enabling users to see the underlying vulnerability characteristics that were used to generate the Risk scores. For every vulnerability, a risk level will be calculated on a scale of 5 to 1 with 5 being the highest likelihood or impact.
RISK SCALE - LIKELIHOOD
  • 5 - Almost certain an incident will occur.
  • 4 - High probability of an incident occurring.
  • 3 - Potential of a security incident in the long term.
  • 2 - Low probability of an incident occurring.
  • 1 - Very unlikely issue will cause an incident.
RISK SCALE - IMPACT
  • 5 - May cause devastating and unrecoverable impact or loss.
  • 4 - May cause a significant level of impact or loss.
  • 3 - May cause a partial impact or loss to many.
  • 2 - May cause temporary impact or loss.
  • 1 - May cause minimal or un-noticeable impact.
The risk level is then calculated using a sum of these two values, creating a value of 10 to 1 with 10 being the highest level of security risk.
Critical
High
Medium
Low
Informational
  • 10 - CRITICAL
  • 9 - 8 - HIGH
  • 7 - 6 - MEDIUM
  • 5 - 4 - LOW
  • 3 - 1 - VERY LOW AND INFORMATIONAL

6. SCOPE

Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

1

Low

4

Informational

4

Impact x Likelihood

HAL-03

HAL-04

HAL-01

HAL-06

HAL-07

HAL-08

HAL-09

HAL-02

HAL-05

Security analysisRisk levelRemediation Date
USAGE OF SELF-DESTRUCT MAY LEAD TO FUNDS LOSSMediumSolved - 08/02/2022
IMPROPER CHECK ON CLAIM FUNCTIONLowSolved - 08/02/2022
UNSAFE CASTINGLowSolved - 08/02/2022
FEE VAULT SHOULD NOT BE ABLE TO SEND TOKENS TO STAKEPOOL CONTRACTLowSolved - 08/02/2022
USE CALL INSTEAD OF SEND OR TRANSFERLowSolved - 08/02/2022
MISSING TWO-STEP TRANSFER OWNERSHIP PATTERNInformationalSolved - 08/02/2022
STORING VARIABLE CAN SAVE GASInformationalSolved - 08/02/2022
ADDING UNCHECKED DIRECTIVE CAN SAVE GASInformationalAcknowledged
PREFIX INCREMENTS ARE CHEAPER THAN POSTFIX INCREMENTSInformationalSolved - 08/02/2022

8. Findings & Tech Details

8.1 USAGE OF SELF-DESTRUCT MAY LEAD TO FUNDS LOSS

// Medium

Description

The usage of the selfdestruct function erases the contract code from the blockchain. Although it is understandable under certain circumstances, this function if executed inappropriately can lead to the destruction of the token contract making users loose all the deposited money and making the decentralized application broken.

Code Location

StakeBNBToken.sol

function selfDestruct(address addr) external onlyRole(DEFAULT_ADMIN_ROLE) whenPaused {
    selfdestruct(payable(addr));
    }
Score
Impact: 3
Likelihood: 3
Recommendation

SOLVED: The persistenceOne team fixed the above issue by using the timelockcontract from openzeppelin on the commit ID d059bccbb368158a63767107b37894d47009c385. As a result, this gives time to the users to call the withdraw function in case the self-destruct function is triggered.

Moreover, the persistenceOne team ensures that this function will only be called in case a major upgrade occurs on the Binance ecosystem and gets the compromise to re-balance the stkBNB tokens to every user account if this situation happens.

8.2 IMPROPER CHECK ON CLAIM FUNCTION

// Low

Description

The _claim function checks the amount to withdraw from the protocol is less than the current contract balance. However, this check is not correct, and it can lead to revert the transaction as the claimReserve can be 0, although the contract balance is not 0. So on the subtraction on L786, the contract reverts with an unhandled arithmetic exception.

Code Location

StakePool.sol

if (address(this).balance < req.weiToReturn) {
    revert InsufficientFundsToSatisfyClaim();
}
Score
Impact: 1
Likelihood: 3
Recommendation

SOLVED: The persistenceOne team fixed the above issue on the commit ID d059bccbb368158a63767107b37894d47009c385.

8.3 UNSAFE CASTING

// Low

Description

The StakePool contracts performs unsafe casting from uint256 to int256 that can lead to overflow. Although the risk is minimum as the amount of BNB is still far from reaching those numbers.

Code Location

StakePool.sol

if (_bnbToUnbond > int256(excessBNB)) {

StakePool.sol

_bnbToUnbond -= int256(shortCircuitAmount);

StakePool.sol

_bnbToUnbond -= int256(bnbUnbonding_);

StakePool.sol

_bnbToUnbond += int256(weiToReturn);
Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: The persistenceOne team fixed the above issue on the commit ID d059bccbb368158a63767107b37894d47009c385. The final implementation contains the recommended library to avoid overflows on arithmetic operations when casting.

8.4 FEE VAULT SHOULD NOT BE ABLE TO SEND TOKENS TO STAKEPOOL CONTRACT

// Low

Description

The claimStkBNB function from the FeeVault contract can send the fees to the StakePool contract. Although this action is restricted by the onlyOwner modifier, this action may lead the fees to be locked on the StakePool contract forever. This happens because there is no way to call the claim function from the FeeVault contract.

Code Location

FeeVault.sol

function claimStkBNB(address recipient, uint256 amount) external override onlyOwner {
        IStakedBNBToken(addressStore.getStkBNB()).send(recipient, amount, "");

        emit Withdraw(msg.sender, recipient, amount);
    }
Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: The persistenceOne team fixed the above issue on the commit ID d059bccbb368158a63767107b37894d47009c385.

8.5 USE CALL INSTEAD OF SEND OR TRANSFER

// Low

Description

The usage of send or transfer limits the amount of gas send in the transaction to 2300. This can be considered a safeguard to avoid reentrancy. However, given the current protections of the contracts, it does seem feasible to use call function without further risk. This implies that the system expects to be used by some contracts that can execute more complex code on the fallback function.

Code Location

StakePool.sol

    function _claim(uint256 index) internal returns (bool) {
        if (index >= claimReqs[msg.sender].length) {
            revert IndexOutOfBounds(index);
        }

        // find the requested claim
        ClaimRequest memory req = claimReqs[msg.sender][index];

        if (!_canBeClaimed(req)) {
            return false;
        }
        if (_claimReserve < req.weiToReturn) {
            revert InsufficientFundsToSatisfyClaim();
        }

        // update _claimReserve
        _claimReserve -= req.weiToReturn;

        // delete the req, as it has been fulfilled (swap deletion for O(1) compute)
        claimReqs[msg.sender][index] = claimReqs[msg.sender][claimReqs[msg.sender].length - 1];
        claimReqs[msg.sender].pop();

        // return BNB back to user
        payable(msg.sender).transfer(req.weiToReturn);
        emit Claim(msg.sender, req, block.timestamp);
        return true;
    }
Score
Impact: 1
Likelihood: 3
Recommendation

SOLVED: The persistenceOne team fixed the above issue on the commit ID d059bccbb368158a63767107b37894d47009c385.

8.6 MISSING TWO-STEP TRANSFER OWNERSHIP PATTERN

// Informational

Description

The AddressStore contract use Ownable from OpenZeppelin which is a simple mechanism to transfer the ownership not supporting a two-step transfer ownership pattern. Ownable is a simpler mechanism with a single owner "role" that can be assigned to a single account. This simpler mechanism can be useful for quick tests, but projects with production concerns are likely to outgrow it. Transferring ownership is a critical operation and this could lead to transferring it to an inaccessible wallet or renouncing the ownership, e.g. mistakenly.

Code Location

AddressStore Contract

AddressStore.sol

contract AddressStore is IAddressStore, Ownable
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The persistenceOne team does not require a fix for this, as the platform will use a multi-signature wallet when developed on production.

8.7 STORING VARIABLE CAN SAVE GAS

// Informational

Description

Calling a contract, although a view function, still requires some gas from the contract. The UndelegationHolder contract performs the call to the StakePool contract twice instead of storing the result on a variable.

Code Location

UndelegationHolder contract

UndelegationHolder.sol

if (amountToSend > IStakePoolBot(stakePool).bnbUnbonding()) {
    amountToSend = IStakePoolBot(stakePool).bnbUnbonding();
}
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The persistenceOne team fixed the above issue on the commit ID d059bccbb368158a63767107b37894d47009c385.

8.8 ADDING UNCHECKED DIRECTIVE CAN SAVE GAS

// Informational

Description

For the arithmetic operations that will never over/underflow, using the unchecked directive (Solidity v0.8 has default overflow/underflow checks) can save some gas from the unnecessary internal over/underflow checks.

Code Location

StakePool contract

StakePool.sol

_TOKEN_HUB.transferOut{ value: excessBNB }(
    _ZERO_ADDR,
    config.bcStakingWallet,
    transferOutAmount,
    uint64(block.timestamp + 3600)
);
Score
Impact: 1
Likelihood: 1
Recommendation

ACKNOWLEDGED: The persistenceOne team acknowledged this issue.

8.9 PREFIX INCREMENTS ARE CHEAPER THAN POSTFIX INCREMENTS

// Informational

Description

The function getPaginatedClaimRequests uses i++ which costs more gas than ++i, especially in a loop. In the loops below, postfix (e.g. i++) operators were used to increment or decrement variable values. It is known that, in loops, using prefix operators (e.g. ++i) costs less gas per iteration than using postfix operators.

Code Location

StakePool Contract

StakePool.sol

for (uint256 i = 0; i < to - from; i++) {
            paginatedClaimRequests[i] = claimReqs[user][from + i];
}
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The persistenceOne team fixed the above issue on the commit ID d059bccbb368158a63767107b37894d47009c385.

9. Automated Testing

STATIC ANALYSIS REPORT

Description

Halborn used automated testing techniques to enhance coverage of certain areas of the scoped contract. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified all the contracts in the repository and was able to compile them correctly into their ABI and binary formats. 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.

Results

addressstore.pngfeevault1.pngstakepool.pngstkbnb.pngundeholder.png

As a result of the tests completed with the Slither tool, some results were obtained and these results were reviewed by Halborn. In line with the reviewed results, it was decided that some vulnerabilities were false-positive and these results were not included in the report. The actual vulnerabilities found by Slither are already included in the findings on the 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.