Halborn Logo

MonoX


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/25/2024

Date of Engagement by: May 3rd, 2021 - May 15th, 2021

Summary

100% of all REPORTED Findings have been addressed

All findings

11

Critical

0

High

3

Medium

1

Low

5

Informational

2


1. INTRODUCTION

MonoX is a new DeFi protocol using a single token design for liquidity pools (instead of using pool pairs). This is made possible by grouping deposited tokens into a virtual pair with the vUSD stablecoin.

MonoX engaged Halborn to conduct a security assessment on their smart contracts beginning on May 3rd, 2021 and ending May 15th, 2021. The security assessment was scoped to smart contracts implementing the core protocol and the staking mechanism, and an audit of the security risk and implications regarding the changes introduced by the development team at MonoX prior to its production release shortly following the assessments deadline.

Though this security audit's outcome is satisfactory, only the most essential aspects were tested and verified to achieve objectives and deliverables set in the scope due to time and resource constraints. It is essential to note the use of the best practices for secure smart-contract development.

2. AUDIT SUMMARY

The team at Halborn was provided two weeks for the engagement and assigned one full time security engineer to audit the security of the assets in scope. The 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 to achieve the following:

    • Ensure that smart contract functions are intended.

    • Identify potential security issues with the smart contracts.

In summary, Halborn identified few security risks, and recommends performing further testing to validate extended safety and correctness in context to the whole set of contracts. External threats, such as economic attacks, oracle attacks, and inter-contract functions and calls should be validated for expected logic and state.

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 the smart contract audit. While manual testing is recommended to uncover flaws in logic, process,and implementation; automated testing techniques help enhance coverage of the smart contract 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

    • 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 (Hardhat and manual deployments on Ganache)

    • Manual testing with custom Javascript.

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

    • Scanning of solidity files for vulnerabilities, security hotspots or bugs. (MythX)

    • Testnet deployment (Remix IDE)

4. SCOPE

IN-SCOPE: The security assessment was scoped to the smart contracts:

Monoswap Core:

    • Monoswap.sol

    • MonoXPool.sol

    • VUSD.sol

commit #c1e16f0b588aeb129d8e13abbc9d39ab3a3392c3

Monoswap Staking:

    • MonoswapStaking.sol

    • MonoToken.sol

commit #89115cd39237c496b60e8a71b07f46968bd854f2

OUT-OF-SCOPE: Dependencies and external libraries.

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

3

Medium

1

Low

5

Informational

2

Impact x Likelihood

HAL-08

HAL-01

HAL-02

HAL-03

HAL-05

HAL-06

HAL-04

HAL-07

HAL-09

HAL-10

HAL-11

Security analysisRisk levelRemediation Date
UNRESTRICTED POOL TOKEN MINTINGHighSolved - 05/24/2021
POOL BLOCKINGHighSolved - 05/24/2021
ROLE-BASED ACCESS CONTROL MISSINGHighSolved - 05/24/2021
INTEGER OVERFLOWMediumSolved - 06/02/2021
EXTERNAL FUNCTION CALLS WITHIN LOOPLowSolved - 06/01/2021
DIVIDE BEFORE MULTIPLYLowSolved - 07/15/2021
ADDRESS VALIDATION MISSINGLowPartially Solved - 05/24/2021
USE OF BLOCK.TIMESTAMPLowSolved - 06/02/2021
TAUTOLOGY EXPRESSIONSLowSolved - 05/24/2021
POSSIBLE MISUSE OF PUBLIC FUNCTIONSInformationalPartially Solved - 05/24/2021
IMPRECISION OF A CONSTANTInformationalAcknowledged - 07/15/2021

8. Findings & Tech Details

8.1 UNRESTRICTED POOL TOKEN MINTING

// High

Description

One of MonoX's main objectives is to allow users for listing ERC20 tokens without the need for providing liquidity. In order to keep track of users' shares in pools, a corresponding amount of liquidity pool tokens is minted to providers. The exact amount to be minted depends on e.g. the declared amount of ERC20 tokens added to the pool and the token price, intially set by the provider.

In the addLiquidityPair function MonoX use OpenZeppelin's safeTransferFrom to handle the token transfer. This function calls transferFrom in the token contract to actually execute the transfer. However, since the actual amount transferred ie. the delta of previous (before transfer) and current (after transfer) balance is not verified, a malicious user may list a custom ERC20 token with the transferFrom function modified in such a way that it e.g. does not transfer any tokens at all and the attacker is still going to have their liquidity pool tokens minted anyway.

Code Location

Attacker-controlled example ERC20 token contract

EvilERC20.sol

function transferFrom(
    address from,
    address to,
    uint256 value
  )
    public
    override
    returns (bool)
  {
    value = 1;
    require(value <= _balances[from]);
    require(value <= _allowed[from][msg.sender]);
    require(to != address(0));

    _balances[from] = _balances[from].sub(value);
    _balances[to] = _balances[to].add(value);
    _allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);
    emit Transfer(from, to, value);
    return true;
  }

MonoX

Monoswap.sol

function listNewToken (address _token, uint112 _price, 
    uint256 vusdAmount, 
    uint256 tokenAmount,
    address to) public returns(uint _pid, uint256 liquidity) {
    _pid = _createPool(_token, _price, PoolStatus.LISTED);
    liquidity = addLiquidityPair(_token, vusdAmount, tokenAmount, to);
}

Monoswap.sol

    _mintFee(pool.pid, pool.lastPoolValue, poolValue);
    uint256 _totalSupply = monoXPool.totalSupplyOf(pool.pid);
    IERC20(_token).safeTransferFrom(msg.sender, address(monoXPool), tokenAmount);
    if(vusdAmount>0){
      vUSD.safeTransferFrom(msg.sender, address(monoXPool), vusdAmount);
    }

    uint256 liquidityVusdValue = vusdAmount.add(tokenAmount.mul(pool.price)/1e18);

    if(_totalSupply==0){
      liquidity = liquidityVusdValue.sub(MINIMUM_LIQUIDITY);
      mint(owner(), pool.pid, MINIMUM_LIQUIDITY); // sorry, oz doesn't allow minting to address(0)
    }else{
      liquidity = _totalSupply.mul(liquidityVusdValue).div(poolValue);
    }

    mint(to, pool.pid, liquidity);
}

OpenZeppelin

SafeERC20.sol

library SafeERC20 {
    using Address for address;

    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
    }

    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
    }
Score
Impact: 4
Likelihood: 4
Recommendation

SOLVED: Fixed in commit #635a4cee2f2e50d854e06cac47c48aa0fafde2b0. The amount to be minted is calculated now based on the delta of account balance before and after transfer.

8.2 POOL BLOCKING

// High

Description

One of MonoX's main objectives is to allow users for listing ERC20 tokens without the need for providing liquidity. Users can set arbitrary prices for tokens they list because the Monoswap.sol contract does not verify them against third-party data sources. The price of a given token can be updated only if it has not been swapped for at least 6000 blocks since the last exchange. In consequence, since the contract does not enforce minimum or maximum transaction amount, a malicious user can list tokens, price them way above market rate and keep the price on that level by doing microexchanges once every 6000 blocks thus effectively DoSing the pool.

Code Location

Monoswap.sol

function listNewToken (address _token, uint112 _price, 
    uint256 vusdAmount, 
    uint256 tokenAmount,
    address to) public returns(uint _pid, uint256 liquidity) {
    _pid = _createPool(_token, _price, PoolStatus.LISTED);
    liquidity = addLiquidityPair(_token, vusdAmount, tokenAmount, to);
}

Monoswap.sol

function _createPool (address _token, uint112 _price, PoolStatus _status) lock internal returns(uint256 _pid)  {
    require(tokenPoolStatus[_token]==0, "Monoswap: Token Exists");
    require (_token != address(vUSD), "Monoswap: vUSD pool not allowed");
    _pid = poolSize;
    pools[_token] = PoolInfo({
      token: _token,
      pid: _pid,
      vusdCredit: 0,
      vusdDebt: 0,
      tokenBalance: 0,
      lastPoolValue: 0,
      status: _status,
      price: _price
    });

    poolSize = _pid.add(1);
    tokenPoolStatus[_token]=1;

    // initialze pool's lasttradingblocknumber as the block number on which the pool is created
    lastTradedBlock[_token] = block.number;
}

Monoswap.sol

function swapExactTokenForToken(
    address tokenIn,
    address tokenOut,
    uint amountIn,
    uint amountOutMin,
    address to,
    uint deadline
  ) external virtual ensure(deadline) returns (uint amountOut) {
    amountOut = swapIn(tokenIn, tokenOut, msg.sender, to, amountIn);
    require(amountOut >= amountOutMin, 'Monoswap: INSUFFICIENT_OUTPUT_AMOUNT');
}

Monoswap.sol

    // record last trade's block number in mapping: lastTradedBlock
    lastTradedBlock[_token] = block.number;

Monoswap.sol

function updatePoolPrice(address _token, uint112 _newPrice) public onlyOwner {
    require(_newPrice > 0, 'Monoswap: zeroPriceNotAccept');
    require(tokenPoolStatus[_token] != 0, "Monoswap: PoolNotExist");

    PoolInfo storage pool = pools[_token];
    require(pool.price != _newPrice, "Monoswap: SamePriceNotAccept");

    require(block.number > lastTradedBlock[_token].add(6000), "Monoswap: PoolPriceUpdateLocked");
    pool.price = _newPrice;
    lastTradedBlock[_token] = block.number;
}
Score
Impact: 4
Likelihood: 4
Recommendation

SOLVED: Fixed in commit #635a4cee2f2e50d854e06cac47c48aa0fafde2b0. Contract owner can now pause pools and temporarily disable swapping so that users with the PriceAdjuster role (assigned by the contract owner) can update prices.

8.3 ROLE-BASED ACCESS CONTROL MISSING

// High

Description

In smart contracts, implementing a correct Access Control policy is an essential step to maintain security and decentralization for permissions on a token. All the features of the smart contract , such as mint/burn tokens and pause contracts are given by Access Control. For instance, Ownership is the most common form of Access Control. In other words, the owner of a contract (the account that deployed it by default) can do some administrative tasks on it. Nevertheless, other authorization levels are required to follow the principle of least privilege, also known as least authority. Briefly, any process, user or program only can access to the necessary resources or information. Otherwise, the ownership role is useful in a simple system, but more complex projects require the use of more roles by using Role-based access control.

Code Location

Monoswap.sol

function setFeeTo (address _feeTo) onlyOwner external {
    feeTo = _feeTo;
}

function setFees (uint16 _fees) onlyOwner external {
    require(_fees<1e3, "fees too large");
    fees = _fees;
}

function setDevFee (uint16 _devFee) onlyOwner external {
    require(_devFee<1e3, "devFee too large");
    devFee = _devFee;
}

// update status of a pool. onlyOwner.
function updatePoolStatus(address _token, PoolStatus _status) public onlyOwner {
    PoolInfo storage pool = pools[_token];
    pool.status = _status;
}

/**
 @dev update pools price if there were no active trading for the last 6000 blocks
 @notice Only owner callable, new price can neither be 0 nor be equal to old one
 @param _token pool identifider (token address)
 @param _newPrice new price in wei (uint112)
*/
function updatePoolPrice(address _token, uint112 _newPrice) public onlyOwner {
    require(_newPrice > 0, 'Monoswap: zeroPriceNotAccept');
Score
Impact: 4
Likelihood: 4
Recommendation

SOLVED: Fixed in commit #635a4cee2f2e50d854e06cac47c48aa0fafde2b0. Several new roles were introduced.

8.4 INTEGER OVERFLOW

// Medium

Description

An overflow happens when an arithmetic operation reaches the maximum size of a type. For instance, in Monoswap.sol, the getAmountOut method is subtracting fees from a fixed number and may end up overflowing the integer since the resulting value is not checked to be greater or equal 0. In computer programming, an integer overflow occurs when an arithmetic operation attempts to create a numeric value that is outside of the range that can be represented with a given number of bits – either larger than the maximum or lower than the minimum representable value.

Code Location

Monoswap.sol

function getAmountOut(address tokenIn, address tokenOut, 
    uint256 amountIn) public view returns (uint256 tokenInPrice, uint256 tokenOutPrice, 
    uint256 amountOut, uint256 tradeVusdValue) {
    require(amountIn > 0, 'Monoswap: INSUFFICIENT_INPUT_AMOUNT');

    uint256 amountInWithFee = amountIn.mul(1e5-fees)/1e5;
    address vusdAddress = address(vUSD);

Monoswap.sol

function getAmountIn(address tokenIn, address tokenOut, 
    uint256 amountOut) public view returns (uint256 tokenInPrice, uint256 tokenOutPrice, 
    uint256 amountIn, uint256 tradeVusdValue) {
    require(amountOut > 0, 'Monoswap: INSUFFICIENT_INPUT_AMOUNT');

    uint256 amountOutWithFee = amountOut.mul(1e5+fees)/1e5;
    address vusdAddress = address(vUSD);
Score
Impact: 3
Likelihood: 3
Recommendation

SOLVED: MonoX is certain the integers reported will not overflow since the fees variable cannot be assigned value greater than 1e3.

8.5 EXTERNAL FUNCTION CALLS WITHIN LOOP

// Low

Description

Calls inside a loop might lead to a denial-of-service attack. In on of the functions discovered there is a for loop on variable pid that iterates up to the poolInfo array length. If this integer is evaluated at extremely large numbers this can cause a DoS.

Code Location

MonoswapStaking.sol

function massUpdatePools() public {
    uint256 length = poolInfo.length;
    for (uint256 pid = 0; pid < length; ++pid) {
        PoolInfo storage pool = poolInfo[pid];
        if (pool.bActive)
            updatePool(pid);
    }
}
Score
Impact: 3
Likelihood: 2
Recommendation

SOLVED: MonoX is certain the DoS scenario is highly unlikely here since all external calls in this loop are made to MonoX-controlled contracts.

8.6 DIVIDE BEFORE MULTIPLY

// Low

Description

Solidity integer division might truncate. As a result, performing multiplication before division can sometimes avoid loss of precision. In this audit, there are multiple instances found where division is being performed before multiplication operation in contract file.

Code Location

MonoswapStaking.sol

if (user.oldReward > 0) {
    monoReward = monoReward.add(user.oldReward.mul(stakedAmount).div(user.amount).mul(1e12));
}
Score
Impact: 3
Likelihood: 2
Recommendation

8.7 ADDRESS VALIDATION MISSING

// Low

Description

Address validation is missing in multiple functions in contracts Monoswap.sol and MonoXPool.sol. This may result with users irreversibly locking their tokens when incorrect address is provided.

Code Location

Monoswap.sol

function mint (address account, uint256 id, uint256 amount) internal {
    monoXPool.mint(account, id, amount);
}

function burn (address account, uint256 id, uint256 amount) internal {
    monoXPool.burn(account, id, amount);
}

MonoXPool.sol

constructor (address _WETH) {
      WETH = _WETH;
}

MonoXPool.sol

function mint (address account, uint256 id, uint256 amount) public onlyOwner {
    totalSupply[id]=totalSupply[id].add(amount);
    _mint(account, id, amount, "");
}

function burn (address account, uint256 id, uint256 amount) public onlyOwner {
    totalSupply[id]=totalSupply[id].sub(amount);
    _burn(account, id, amount);
}
Score
Impact: 2
Likelihood: 2
Recommendation

PARTIALLY SOLVED: Vulnerable function calls in Monoswap.sol have been removed but address validation is missing in MonoXPool.sol.

8.8 USE OF BLOCK.TIMESTAMP

// Low

Description

block.timestamp can be influenced by miners to a certain degree, so the testers should be warned that this may have some risk if miners collude on time manipulation to influence the price oracles.

Code Location

Monoswap.sol

modifier ensure(uint deadline) {
    require(deadline >= block.timestamp, 'Monoswap: EXPIRED');
    _;
}  
Score
Impact: 4
Likelihood: 1
Recommendation

SOLVED: MonoX does not require timestamps to be extremely precise here (timescales are greater than 900 seconds)

8.9 TAUTOLOGY EXPRESSIONS

// Low

Description

In contract Monoswap.sol, tautology expressions have been detected. Such expressions are of no use since they always evaluate true/false regardless of the context they are used in.

Code Location

Monoswap.sol

if(_poolStatus == PoolStatus.LISTED){

      require (_vusdCredit>=0 && _vusdDebt==0, "Monoswap: unofficial pool cannot bear debt");
}
Score
Impact: 2
Likelihood: 2
Recommendation

SOLVED: Tautology Expression was removed in commit #635a4cee2f2e50d854e06cac47c48aa0fafde2b0.

8.10 POSSIBLE MISUSE OF PUBLIC FUNCTIONS

// Informational

Description

In public functions, array arguments are immediately copied to memory, while external functions can read directly from calldata. Reading calldata is cheaper than memory allocation. Public functions need to write the arguments to memory because public functions may be called internally. Internal calls are passed internally by pointers to memory. Thus, the function expects its arguments being located in memory when the compiler generates the code for an internal function. Also, methods do not necessarily have to be public if they are only called within the contract-in such case they should be marked internal.

Code Location

Monoswap.sol

function updatePoolStatus(address _token, PoolStatus _status) public onlyOwner {
    PoolInfo storage pool = pools[_token];
    pool.status = _status;
}

Monoswap.sol

function updatePoolPrice(address _token, uint112 _newPrice) public onlyOwner {
    require(_newPrice > 0, 'Monoswap: zeroPriceNotAccept');
    require(tokenPoolStatus[_token] != 0, "Monoswap: PoolNotExist");

    PoolInfo storage pool = pools[_token];

Monoswap.sol

function listNewToken (address _token, uint112 _price, 
    uint256 vusdAmount, 
    uint256 tokenAmount,
    address to) public returns(uint _pid, uint256 liquidity) {
    _pid = _createPool(_token, _price, PoolStatus.LISTED);
    liquidity = addLiquidityPair(_token, vusdAmount, tokenAmount, to);
  }
}

MonoswapStaking.sol

function set(
    uint256 _pid,
    uint256 _allocPoint,
    bool _withUpdate
) public onlyOwner {
    if (_withUpdate) {
        massUpdatePools();
    }

MonoswapStaking.sol

function stopPool(uint256 _pid) public onlyOwner {
    updatePool(_pid);

MonoswapStaking.sol

function migratePool(uint256 _oldPid, uint256 _newPid) public {
    PoolInfo storage oldPool = poolInfo[_oldPid];
    PoolInfo storage newPool = poolInfo[_newPid];

MonoswapStaking.sol

function deposit(uint256 _pid, uint256 _amount) public {
    PoolInfo storage pool = poolInfo[_pid];
    UserInfo storage user = userInfo[_pid][msg.sender];

MonoswapStaking.sol

function withdraw(uint256 _pid, uint256 _amount) public {
    PoolInfo storage pool = poolInfo[_pid];
    UserInfo storage user = userInfo[_pid][msg.sender];

MonoswapStaking.sol

function emergencyWithdraw(uint256 _pid) public {
    PoolInfo storage pool = poolInfo[_pid];
    UserInfo storage user = userInfo[_pid][msg.sender];
Score
Impact: 1
Likelihood: 1

8.11 IMPRECISION OF A CONSTANT

// Informational

Description

During the audit, it has been observed that integers with scientific notations are directly compared with function arguments.

Code Location

Monoswap.sol

function setFees (uint16 _fees) onlyOwner external {
    require(_fees<1e3, "fees too large");
    fees = _fees;
}

function setDevFee (uint16 _devFee) onlyOwner external {
    require(_devFee<1e3, "devFee too large");
    devFee = _devFee;
}

Monoswap.sol

function _mintFee (uint256 pid, uint256 lastPoolValue, uint256 newPoolValue) internal {

    uint256 _totalSupply = monoXPool.totalSupplyOf(pid);
    if(newPoolValue>lastPoolValue && lastPoolValue>0) {
      // safe ops, since newPoolValue>lastPoolValue
      uint256 deltaPoolValue = newPoolValue - lastPoolValue; 

      // safe ops, since newPoolValue = deltaPoolValue + lastPoolValue > deltaPoolValue
      uint256 devLiquidity = _totalSupply.mul(deltaPoolValue).mul(devFee).div(newPoolValue-deltaPoolValue)/1e5;
      monoXPool.mint(feeTo, pid, devLiquidity);
    }

}

Also lines #584 and #638 in Monoswap.sol.

Monoswap.sol

function getPool (address _token) view public returns (uint256 poolValue, 
    uint256 tokenBalanceVusdValue, uint256 vusdCredit, uint256 vusdDebt) {
    PoolInfo memory pool = pools[_token];
    vusdCredit = pool.vusdCredit;
    vusdDebt = pool.vusdDebt;
    tokenBalanceVusdValue = uint(pool.price).mul(pool.tokenBalance)/1e18;

    poolValue = tokenBalanceVusdValue.add(vusdCredit).sub(vusdDebt);
}

Also lines #258, #297, #322, #326, #569, #570, #590, #600, #605, #614, #628, #645, #656, #661, #671, #684, #715 and #767 in Monoswap.sol.

MonoswapStaking.sol

function initialize(
        MonoToken _mono,
        uint256 _monoPerPeriod,
        uint256 _blockPerPeriod,
        uint256 _decay
    ) public initializer {
        OwnableUpgradeable.__Ownable_init();
        __ERC1155Holder_init();
        mono = _mono;
        monoPerPeriod = _monoPerPeriod;
        blockPerPeriod = _blockPerPeriod;
        decay = _decay;
        startBlock = block.number;
        currentPeriod = 0;
        ratios[currentPeriod] = 1e12;
        totalAllocPoint = 0;
}

Also lines #176, #185, #211, #221, #232, #265, #297, #299, #316, #317, #336, #337, #359, #377, #378 and #389 in MonoswapStaking.sol.

Score
Impact: 1
Likelihood: 1
Recommendation

ACKNOWLEDGED: MonoX refrain from introducing extra variables as it increases the contract size quite a bit and increase gas usage as well. Therefore they are trying not to have a variable unless it's necessary.

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.