BTC Hardfork - Enhancement / Update - CoreDAO


Prepared by:

Halborn Logo

HALBORN

Last Updated 02/28/2025

Date of Engagement: January 6th, 2025 - January 10th, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

9

Critical

0

High

1

Medium

0

Low

3

Informational

5


1. Introduction

CoreDAO engaged Halborn to conduct a comprehensive security assessment of their smart contracts from January 6th to January 10th, 2025, focusing on modifications to the codebase between the commit 551393b and the commit c439750. The review specifically targets changes introduced during these commits, assuming the validity of the pre-existing logic and excluding verification of its security, e.g., input validation of previously existing parameters.


The scope of the security assessment was confined to the smart contracts provided to Halborn, with specific commit hashes and additional details available in the Scope section of this report.

2. Assessment Summary

The team at Halborn assigned a full-time security engineer to assess the security of the smart contracts. 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 assessment is to:

    • Ensure that smart contract functions operate as intended.

    • Identify potential security issues with the smart contracts.


In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were partially addressed by the CoreDAO team. The main ones were the following: 

    • Adjust the remain value when receiving rewards only after confirming that a transfer was successful.

    • Ensure that rewardMap entries are deleted whenever 'rewardUnclaimed' is non-zero, even if 'reward' and 'accStakedAmount' are zero.

    • Allow partial allocations in the receiveRewards function when 'remain' is less than 'toWhiteListValue'.

    • Integrate viewCollectReward function into BitcoinAgent contract if intended for use or remove it to improve code clarity and maintainability.

3. Test Approach and Methodology

Halborn performed a combination of manual review of the code and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of the smart contract assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of smart contracts 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 assessment:

    • Research into the architecture, purpose, and use of the platform.

    • Smart contract manual code review and walkthrough to identify any logic issue.

    • Thorough assessment of safety and usage of critical Solidity variables and functions in scope that could lead to arithmetic related vulnerabilities.

    • Manual testing by custom scripts.

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

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

    • Local or public testnet deployment (Foundry, Remix IDE).

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:
EXPLOITABILITY 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: core-genesis-contract
(b) Assessed Commit ID: c439750
(c) Items in scope:
  • contracts/BitcoinAgent.sol
  • contracts/BitcoinLSTStake.sol
  • contracts/BitcoinStake.sol
↓ Expand ↓
Out-of-Scope: The review specifically targets changes introduced during the commit 551393b and the commit c439750, assuming the validity of the pre-existing logic and excluding verification of its security, e.g., input validation of previously existing parameters.
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

1

Medium

0

Low

3

Informational

5

Security analysisRisk levelRemediation Date
Unaccounted funds in rewards distribution logicHighSolved - 01/14/2025
Incomplete reward map cleanupLowRisk Accepted - 02/27/2025
Skipping rewards allocation in fund distributionLowRisk Accepted - 01/14/2025
Unreachable function due to access restrictionLowRisk Accepted - 02/26/2025
Incomplete input validation in parameter updatesInformationalSolved - 01/14/2025
Potential inconsistent delegator data handlingInformationalSolved - 01/14/2025
Suboptimal gas usage due to post-increment in loopsInformationalAcknowledged - 01/14/2025
Unused and uncleared storage mappingInformationalAcknowledged - 01/14/2025
Duplicate functionsInformationalSolved - 01/14/2025

7. Findings & Tech Details

7.1 Unaccounted funds in rewards distribution logic

//

High

Description

The receiveRewards function in the SystemReward contract processes excess funds by distributing them to members of the whiteListSet based on their assigned percentage. However, when a transfer to a whitelist member fails, the remain value is still reduced by the intended transfer amount, regardless of whether the funds were successfully sent.


The mentioned scenario can result in unallocated funds being disproportionately redirected to other whitelist members, burned, or sent to the foundation. For example, if a transfer fails for one member, their share is effectively lost, and the remaining distribution does not account for this failure, potentially creating an imbalance in the intended allocation.


Code Location

  /// Receive funds from system, burn the portion which exceeds cap
  function receiveRewards() external payable override onlyInit {
    if (msg.value != 0) {
      if (address(this).balance > incentiveBalanceCap) {
        uint256 value = address(this).balance - incentiveBalanceCap;
        uint256 remain = value;
        for (uint256 i = 0; i < whiteListSet.length; i++) {
          uint256 toWhiteListValue = value * whiteListSet[i].percentage / SatoshiPlusHelper.DENOMINATOR;
          if (remain >= toWhiteListValue) {
            remain -= toWhiteListValue;
            bool success = payable(whiteListSet[i].member).send(toWhiteListValue);
            if (success) {
              emit whitelistTransferSuccess(whiteListSet[i].member, toWhiteListValue);
            } else {
              emit whitelistTransferFailed(whiteListSet[i].member, toWhiteListValue);
            }
          }
        }
        if (remain != 0) {
          if (isBurn) {
            IBurn(BURN_ADDR).burn{ value: remain }();
          } else {
            payable(FOUNDATION_ADDR).transfer(remain);
          }
        }
      }
      emit receiveDeposit(msg.sender, msg.value);
    }
  }
BVSS
Recommendation

The receiveRewards function should adjust the remain value only after confirming that a transfer was successful. For failed transfers, the function should log the amount and leave it in remain to ensure it is not inadvertently redirected or lost.

Remediation

SOLVED: The CoreDAO team solved the issue in the specified commit id.

Remediation Hash

7.2 Incomplete reward map cleanup

//

Low

Description

In the claimReward function of the BitcoinStake contract, the rewardMap entry for a delegator is only deleted if either reward or accStakedAmount is non-zero. However, there could be a scenario where both values are zero while rewardUnclaimed remains non-zero, leaving an entry in rewardMap that may no longer be necessary. This could result in stale data persisting in storage, potentially causing rewardUnclaimed to be processed again.


Code Location

  function claimReward(address delegator, uint256 settleRound, bool claim) external override onlyBtcAgent returns (uint256 reward, uint256 rewardUnclaimed, uint256 accStakedAmount) {
    reward = rewardMap[delegator].reward;
    rewardUnclaimed = rewardMap[delegator].unclaimedReward;
    accStakedAmount = rewardMap[delegator].accStakedAmount;
    if (reward != 0 || accStakedAmount != 0) {
      delete rewardMap[delegator];
    }

    bool expired;
    uint256 rewardPerTx;
    uint256 rewardUnclaimedPerTx;
    uint256 accStakedAmountPerTx;
    bytes32[] storage txids = delegatorMap[delegator].txids;
    for (uint256 i = txids.length; i != 0; i--) {
      (rewardPerTx, expired, rewardUnclaimedPerTx, accStakedAmountPerTx) = _collectReward(txids[i - 1], settleRound);
      reward += rewardPerTx;
      rewardUnclaimed += rewardUnclaimedPerTx;
      accStakedAmount += accStakedAmountPerTx;
      if (claim) {
        emit claimedRewardPerTx(txids[i - 1], rewardPerTx, expired, accStakedAmountPerTx, rewardUnclaimedPerTx);
      } else {
        emit storedRewardPerTx(txids[i - 1], rewardPerTx, expired, accStakedAmountPerTx, rewardUnclaimedPerTx);
      }

      if (expired) {
        if (i != txids.length) {
          txids[i - 1] = txids[txids.length - 1];
        }
        txids.pop();
      }
    }
  }
BVSS
Recommendation

It is recommended to ensure that rewardMap entries are deleted whenever rewardUnclaimed is non-zero, even if reward and accStakedAmount are zero.

Remediation

RISK ACCEPTED: The CoreDAO team accepted this risk of this finding.

7.3 Skipping rewards allocation in fund distribution

//

Low

Description

In the receiveRewards function of the SystemReward contract, the logic skips processing a whitelist member if the remaining balance (remain) is less than the calculated allocation (toWhiteListValue). Instead of partially allocating the available funds to the current member, the function moves to the next whitelist member, leaving the previous member with no distribution.


The mentioned approach can lead to inefficient use of available funds and unfair distribution, particularly when funds are limited. Members with larger allocations earlier in the whitelist may receive nothing, while members later in the list may still receive their full share.


Code Location

  /// Receive funds from system, burn the portion which exceeds cap
  function receiveRewards() external payable override onlyInit {
    if (msg.value != 0) {
      if (address(this).balance > incentiveBalanceCap) {
        uint256 value = address(this).balance - incentiveBalanceCap;
        uint256 remain = value;
        for (uint256 i = 0; i < whiteListSet.length; i++) {
          uint256 toWhiteListValue = value * whiteListSet[i].percentage / SatoshiPlusHelper.DENOMINATOR;
          if (remain >= toWhiteListValue) {
            remain -= toWhiteListValue;
            bool success = payable(whiteListSet[i].member).send(toWhiteListValue);
            if (success) {
              emit whitelistTransferSuccess(whiteListSet[i].member, toWhiteListValue);
            } else {
              emit whitelistTransferFailed(whiteListSet[i].member, toWhiteListValue);
            }
          }
        }
        if (remain != 0) {
          if (isBurn) {
            IBurn(BURN_ADDR).burn{ value: remain }();
          } else {
            payable(FOUNDATION_ADDR).transfer(remain);
          }
        }
      }
      emit receiveDeposit(msg.sender, msg.value);
    }
  }
BVSS
Recommendation

The receiveRewards function should allow for partial allocations when remain is less than toWhiteListValue. By sending the remaining balance to the current whitelist member, the function would ensure better utilization of funds and a fairer distribution.

Remediation

RISK ACCEPTED: The CoreDAO team accepted this risk and stated the following:

There is check (_checkPercentage()) when setting the whitelist items which guarentees the total percentages combined in the whitelist won't exceeds 100%. Value check is made in receiveRewards() to make the implementation more sound, however in theory the issue mentioned here won't actually happen. As a result, we will keep the code as is now.

7.4 Unreachable function due to access restriction

//

Low

Description

The viewCollectReward function in the BitcoinStake contract is restricted by the onlyBtcAgent modifier, allowing only the BitcoinAgent contract to call it. However, a review of the codebase indicates that BitcoinAgent does not invoke this function, making it effectively unreachable. This results in dead code that does not contribute to the contract’s functionality and may mislead developers into assuming that an external view function is available for retrieving reward calculations. Additionally, if this function was intended for use but is currently unused, it could indicate an incomplete implementation or an oversight in integrating it into BitcoinAgent.


Code Location

  /// Exposed for staking API to do readonly calls, restricted to onlyBtcAgent() for safety reasons.
  /// @param txid the BTC stake transaction id
  /// @param drRound the start round
  /// @param settleRound the settlement round
  /// @return reward reward of the BTC stake transaction
  /// @return expired whether the stake is expired
  /// @return rewardUnclaimed unclaimed reward of the BTC stake transaction
  /// @return accStakedAmount accumulated stake amount (multiplied by days), used for grading calculation
  function viewCollectReward(bytes32 txid, uint256 drRound, uint256 settleRound) external onlyBtcAgent returns (uint256 reward, bool expired, uint256 rewardUnclaimed, uint256 accStakedAmount) {
    return _collectReward(txid, drRound, settleRound);
  }
BVSS
Recommendation

It is recommended to either integrate viewCollectReward into BitcoinAgent contract if intended for use or remove it to improve code clarity and maintainability.

Remediation

RISK ACCEPTED: The CoreDAO team accepted this risk and stated the following:

It is clearly stated that this finding pertains to the Staking API. Given that the function viewCollectReward is specifically designed for read-only calls related to staking, it is essential to recognize its intended purpose

7.5 Incomplete input validation in parameter updates

//

Informational

Description

The updateParam function in the SystemReward contract does not validate the length of the value parameter for the addWhiteList and modifyWhiteList options. Unlike other options, such as incentiveBalanceCap and removeWhiteList, which enforce specific length checks, these two options rely on _decodeWhiteList to parse value without explicitly verifying its size beforehand. While _decodeWhiteList attempts to decode value, the absence of upfront validation could result in runtime errors if the provided value does not conform to the expected structure.


For example, if the value is larger than expected, it could lead to unexpected scenarios where only the first two elements of the list are parsed, potentially ignoring additional unintended data. Conversely, if the value is smaller than expected, decoding operations could fail entirely, resulting in a runtime error.


Code Location

  function updateParam(string calldata key, bytes calldata value) external override onlyInit onlyGov {
    if (Memory.compareStrings(key, "incentiveBalanceCap")) {
      if (value.length != 32) {
        revert MismatchParamLength(key);
      }
      uint256 newIncentiveBalanceCap = value.toUint256(0);
      if (newIncentiveBalanceCap == 0) {
        revert OutOfBounds(key, newIncentiveBalanceCap, 1, type(uint256).max);
      }
      incentiveBalanceCap = newIncentiveBalanceCap;
    } else if (Memory.compareStrings(key, "isBurn")) {
      if (value.length != 1) {
        revert MismatchParamLength(key);
      }
      uint8 newIsBurn = value.toUint8(0);
      if (newIsBurn > 1) {
        revert OutOfBounds(key, newIsBurn, 0, 1);
      }
      isBurn = newIsBurn == 1;
    } else if (Memory.compareStrings(key, "addOperator")) {
      if (value.length != 20) {
        revert MismatchParamLength(key);
      }
      address newOperator = value.toAddress(0);
      if (!operators[newOperator]) {
        operators[newOperator] = true;
        numOperator++;
      }
    } else if (Memory.compareStrings(key, "addWhiteList")) {
      (address member, uint32 percentage) = _decodeWhiteList(key, value);
      require(whiteLists[member] == 0, "whitelist member already exists");
      whiteListSet.push(WhiteList(member, percentage));
      whiteLists[member] = whiteListSet.length;
      _checkPercentage();
    } else if (Memory.compareStrings(key, "modifyWhiteList")) {
      (address member, uint32 percentage) = _decodeWhiteList(key, value);
      require(whiteLists[member] != 0, "whitelist member does not exist");
      whiteListSet[whiteLists[member] - 1].percentage = percentage;
      _checkPercentage();
    } else if (Memory.compareStrings(key, "removeWhiteList")) {
      if (value.length != 20) {
        revert MismatchParamLength(key);
      }
      address member = value.toAddress(0);
      uint256 index = whiteLists[member];
      require(index != 0, "whitelist member does not exist");
      if (index != whiteListSet.length) {
        WhiteList storage whiteList = whiteListSet[whiteListSet.length - 1];
        whiteListSet[index - 1] = whiteList;
        whiteLists[whiteList.member] = index;
      }
      whiteListSet.pop();
      delete whiteLists[member];
    } else {
      revert UnsupportedGovParam(key);
    }
    emit paramChange(key, value);
  }
BVSS
Recommendation

It is recommended to explicitly verify the length of value before decoding it in the addWhiteList and modifyWhiteList options within the updateParam function. For example, require that value matches the expected size for RLP-encoded data containing an address and a 32-bit integer.

Remediation

SOLVED: The CoreDAO team solved the issue in the specified commit id.

Remediation Hash

7.6 Potential inconsistent delegator data handling

//

Informational

Description

In the getDelegator function of the PledgeAgent contract, there is a potential for inconsistent data updates. The current implementation only updates values when realtimeAmount is non-zero, potentially missing scenarios where stakedAmount is non-zero but realtimeAmount is zero. Additionally, directly assigning cd.changeRound to changeRound might overwrite a more recent value, leading to incorrect round tracking. While these scenarios have a low probability of occurring due to typical protocol behavior, they are worth mentioning to ensure robustness in edge cases.


Code Location

  function getDelegator(address agent, address delegator) external view returns (CoinDelegator memory cd) {
      cd = agentsMap[agent].cDelegatorMap[delegator];
      (bool success, bytes memory result) = CORE_AGENT_ADDR.staticcall(abi.encodeWithSignature("getDelegator(address,address)", agent, delegator));
      require (success, "call CORE_AGENT_ADDR.getDelegator() failed");
      (uint256 stakedAmount, uint256 realtimeAmount, uint256 transferredAmount, uint256 changeRound) = abi.decode(result, (uint256,uint256,uint256,uint256));
      if (realtimeAmount != 0) {
        cd.deposit = cd.newDeposit + stakedAmount;
        cd.newDeposit += realtimeAmount;
        cd.changeRound = changeRound;
        cd.transferOutDeposit = transferredAmount;
        cd.transferInDeposit = 0;
      }
  }
BVSS
Recommendation

Modify the logic to update values when either stakedAmount or realtimeAmount is non-zero. Use cd.changeRound = max(cd.changeRound, changeRound) to ensure the most recent round is retained, enhancing data consistency and system reliability.

Remediation

SOLVED: The CoreDAO team solved the issue in the specified commit id. They also stated the following:

It is not necessary to max(cd.changeRound, changeRound) because changeRound is fetched from COREAgent.sol which is definitely >= the value left in PledgeAgent which has not been changed since data moved to COREAgent.sol.

Remediation Hash

7.7 Suboptimal gas usage due to post-increment in loops

//

Informational

Description

In multiple functions across the codebase, the for loops use i++ (post-increment) for incrementing the loop counter. In Solidity, post-increment (i++) is slightly less efficient than pre-increment (++i) because i++ requires storing the original value of i in a temporary variable before incrementing, which consumes more gas. Although the gas difference is minimal, especially in recent Solidity versions, it becomes noticeable in larger loops or frequently executed functions, leading to inefficiencies in contract execution. The affected functions are the following:

  • StakeHub.init

  • StakeHub.claimReward

  • StakeHub.proxyClaimReward

  • StakeHub.calculateReward

  • SystemReward.receiveRewards

  • SystemReward._checkPercentage

BVSS
Recommendation

To optimize gas usage, especially when iterating over large arrays or loops, it is recommendd to replace i++ with ++i. Pre-increment (++i) does not require storing the old value of i, making it slightly more efficient in terms of gas consumption.

Remediation

ACKNOWLEDGED: The CoreDAO team acknowledged this issue and stated the following:

We will consider in some future releases.

References
contracts/StakeHub.sol#L115, L233, L243, L259, L269, L290
contracts/SystemReward.sol#L68, L198

7.8 Unused and uncleared storage mapping

//

Informational

Description

In the PledgeAgent contract, the btcReceiptMap mapping is declared but not used anywhere in the contract’s logic. While it is marked as deprecated in the comments, its values are not cleared, leaving data potentially lingering in storage. This can lead to inefficiencies in gas usage and confusion for future developers or auditors. Without clearing the mapping, unnecessary storage costs are incurred, and the lack of clarity around its presence may cause misinterpretations.

BVSS
Recommendation

If the btcReceiptMap mapping is truly deprecated and no longer relevant to the contract’s functionality, it should either be explicitly cleared during a migration process or removed entirely in a future update to optimize storage and avoid confusion.

Remediation

ACKNOWLEDGED: The CoreDAO team acknowledged this issue and stated the following:

We will consider in some future releases.

References
contracts/PledgeAgent.sol#L69

7.9 Duplicate functions

//

Informational

Description

The functions getExpireList and getExpireInfo in the PledgeAgent contract produce the same information by returning the agentAddrList from round2expireInfoMap. This redundancy introduces unnecessary code duplication, making the contract less maintainable and potentially confusing for developers and auditors.

BVSS
Recommendation

It is recommended to remove one of the redundant functions (getExpireInfo or getExpireList) to streamline the contract and avoid duplication.

Remediation

SOLVED: The CoreDAO team solved the issue in the specified commit id.

Remediation Hash

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 2025. All rights reserved.