EVM Bridge Contracts - Pontis


Prepared by:

Halborn Logo

HALBORN

Last Updated 01/28/2025

Date of Engagement: January 8th, 2025 - January 13th, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

11

Critical

0

High

0

Medium

1

Low

1

Informational

9


1. Introduction

Pontis engaged Halborn to conduct a security assessment on their smart contracts beginning on January 8th, 2025 and ending on January 13th, 2025. The security assessment was scoped to the smart contracts provided to Halborn. Commit hashes and further details can be found in the Scope section of this report.

The Pontis codebase in scope consist of 6 different smart contracts:

    • The pontis-bridge-controller.sol contract is responsible for managing access across the protocol.

    • The pontis-bridge-erc20.sol contract is responsible for minting and burning ERC20 tokens.

    • The pontis-bridge-nft.sol contract is responsible for minting and burning ERC721 tokens.

    • The pontis-bridge-fee-manager.sol contract is responsible for setting up fees and the fee receiver.

    • The pontis-bridge-v1.sol contract contains the main logic of the bridge and is the contract with which users interact.

    • The pontis-price-oracle.sol contract is responsible for providing accurate prices.

2. Assessment Summary

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

The purpose of the assessment is to:

    • Identify potential security issues within the smart contracts.

    • Ensure that smart contract functionality operates as intended.

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

    • Consider checking whether the user burning the ERC20 tokens is a user for whom a PegOutKeyUtxo have been added by the off-chain system.

    • Consider utilizing Chainlink or Pyth oracles for fetching the price

    • Consider using custom errors

    • Consider implementing a two-step ownership transfer.

3. Test Approach and Methodology

Halborn performed a combination of manual review of the code and automated security testing to balance efficiency, timeliness, 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 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 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.

    • Local testing with custom scripts (Foundry).

    • Fork testing against main networks (Foundry).

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

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: evm-bridge-contracts
(b) Assessed Commit ID: 7ce89ea
(c) Items in scope:
  • contracts/pontis-bridge-controller.sol
  • contracts/pontis-bridge-erc20.sol
  • contracts/pontis-bridge-fee-manager.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

0

Medium

1

Low

1

Informational

9

Security analysisRisk levelRemediation Date
Not sufficient restrictions may lead to a DOS of core functionsMediumRisk Accepted - 01/21/2025
The oracle implementation may return stale pricesLowRisk Accepted - 01/21/2025
Lack of a double-step transferOwnership patternInformationalPartially Solved - 01/21/2025
Custom Errors Should be UsedInformationalSolved - 01/21/2025
Use of transfer instead of callInformationalSolved - 01/21/2025
The length of an array is not cachedInformationalSolved - 01/21/2025
Consider Using Named MappingsInformationalSolved - 01/21/2025
Floating pragmaInformationalSolved - 01/21/2025
Important fields in events are not indexedInformationalSolved - 01/21/2025
An important state changing function doesn't emit an eventInformationalSolved - 01/21/2025
Use of memory instead of calldata for an unmodified function argumentInformationalSolved - 01/21/2025

7. Findings & Tech Details

7.1 Not sufficient restrictions may lead to a DOS of core functions

//

Medium

Description

In the pontis-bridge-v1.sol contract, the bridgeOutRunes() and the bridgeOutBtc() functions both call the getAndMarkKeyUtxo() function if the user supplied network parameter is equal to the KEY_UTXO_NETWORK constant. The getAndMarkKeyUtxo() function checks whether the currentKeyUtxo variable is less than the totalAmountKeyUtxo variable:

    function getAndMarkKeyUtxo() private {
        require(currentKeyUtxo < totalAmountKeyUtxo, "NoKeyUtxo");
        string memory keyUtxo = availablePegOutKeyUtxo[currentKeyUtxo];
        delete availablePegOutKeyUtxo[currentKeyUtxo];
        currentKeyUtxo++;
        emit KeyUtxoUsed(keyUtxo);
    }

The totalAmountKeyUtxo is a special counter that tracks how many PegOutKeyUtxo have been added by the authorizedBridgeOwner which is the off-chain system responsible for populating this list via the addPegOutKeyUtxo() function. However, a malicious actor can bridge out the minimum amounts required, and thus increase the currentKeyUtxo param, up to a point where non-malicious users for whom PegOutKeyUtxos have been added by the off-chain system won't be able to bridge out their tokens, when they call the bridgeOutRunes() or the bridgeOutBtc() functions. There are minimum amounts that have to be used, as well as fees that should be paid by the attacker. However, it still may result in the temporary DOS of the bridgeOutRunes() and the bridgeOutBtc() functions, thus resulting in dosing the main functionality of the protocol. Additional PegOutKeyUtxo can be added by the off-chain system. However, this may introduce separate vulnerabilities. Note that the off-chain system is not in the scope of this audit.

BVSS
Recommendation

Consider checking whether the user burning the ERC20 tokens is a user for whom a PegOutKeyUtxo have been added by the off-chain system.

Remediation

RISK ACCEPTED: The Pontis team has accepted the risk and chosen not to implement additional functionality and instead rely on the big fees an attacker will have to pay in order to execute this attack, in order to defer potential attempts to DOS the core functions of the protocol.

References

7.2 The oracle implementation may return stale prices

//

Low

Description

Users who want to utilize the bridgeOutRunes() function in the pontis-bridge-v1.sol contract, have to pay fees in the native token. First the fetchPrices() function is called which returns the runePrice and nativePrice. Both of those prices are fetched from an oracle implementation:

    function fetchPrices(string calldata runeId) private view returns (uint256 nativePrice, uint256 runePrice) {
        nativePrice = priceOracle.getPrice(NATIVE_TOKEN_NAME);
        runePrice = priceOracle.getPrice(runeId);
        require(nativePrice > 0, "Invalid Native Price");
        require(runePrice > 0, "Invalid Rune Price");
        return (nativePrice, runePrice);
    }

The getPrice() function fetches the price from a specific contract implementation - pontis-price-oracle.sol, where the price is set via the setPrices() function. According to the dev team the price will be updated once a day, this is a problem if the price is too volatile. As this will result in users paying more fees than they should, or the protocol receiving less in fees, depending on how the price fluctuates.

BVSS
Recommendation

Consider fetching the prices from oracles such as Chainlink or Pyth.

Remediation

RISK ACCEPTED: The Pontis team accepted the risk of this finding.

References

7.3 Lack of a double-step transferOwnership pattern

//

Informational

Description

The current ownership transfer process for the pontis-bridge-nft.sol , pontis-bridge-fee-manager.sol , pontis-bridge-controller.sol contracts utilizes a direct transfer of ownership, if the owner address is set incorrectly, all of the critical functions of the protocol will be disabled or compromised.

BVSS
Recommendation

Consider utilizing OpenZeppelin Ownable2Step.sol contract.

Remediation

PARTIALLY SOLVED: The Pontis team implemented the recommended solution in the pontis-bridge-nft.sol and pontis-bridge-fee-manager.sol contracts. However, they decided not to implement any changes in the pontis-bridge-controller.sol contract.

Remediation Hash
References

7.4 Custom Errors Should be Used

//

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.

BVSS
Recommendation

It is recommended to replace hard-coded revert strings in require statements for custom errors, which can be done following the logic below:


1. Standard require statement (to be replaced):

require(condition, "Condition not met");

2. Declare the error definition to state:

error ConditionNotMet();

3. As currently is not possible to use custom errors in combination with require statements, the standard syntax is:

if (!condition) revert ConditionNotMet();

More information about this topic in the Official Solidity Documentation.

Remediation

SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.

Remediation Hash
References

7.5 Use of transfer instead of call

//

Informational

Description

In the pontis-bridge-v1.sol contract when fees in the native tokens are paid, the solidity built in transfer() function is used to handle the native token transfer, it does this by forwarding a fixed amount of 2300 gas. Paying fees in the native token is function which is utilized by all of the core functionalities of the pontis-bridge-v1.sol contract. This is dangerous for two reasons:


  1. Gas costs of EVM instructions may change significantly during hard forks, which may previously assumed fixed gas costs. EIP 1884 as an example, broke several existing smart contracts due to a cost increase of the SLOAD instruction.

  2. If the recipient is a contract or a multisig safe, with a receive/fallback function which requires >2300 gas, e.g safes that execute extra logic in the receive/fallback function, the transfer function will always fail for them due to out of gas errors.

BVSS
Recommendation

Consider using the call() method for transferring funds, as it is more flexible and handles the increase in gas cost associated with the transfer. Be careful not to introduce a reentrancy vulnerability.

Remediation

SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.

Remediation Hash
References

7.6 The length of an array is not cached

//

Informational

Description

In the pontis-bridge-v1.sol contract, the setRunesAndBtcActive() function loops through all of the provided addresses in the runes array. However, the length of the runes array is not cached before all request are looped trough. This increases the gas consumed by the setRunesAndBtcActive() function. The same scenario occurs in the pegOutOrdinalsBatch(), mintOrdinalsBatch(), mintRunesBatch() functions as well.

BVSS
Recommendation

Consider caching the length of the array, before you loop over it.

Remediation

SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.

Remediation Hash
References

7.7 Consider Using Named Mappings

//

Informational

Description

The project is using a Solidity version greater than 0.8.18, which supports named mappings. Using named mappings can improve the readability and maintainability of the code by making the purpose of each mapping clearer. This practice helps developers and auditors understand the mappings' intent more easily.

BVSS
Recommendation

Consider refactoring the mappings to use named arguments, which will enhance code readability and make the purpose of each mapping more explicit.


For example, in the pontis-bridge-v1.sol contract, instead of declaring:

mapping(address => bool) private runeTokensActive;

It could be declared as:

mapping(address runeTokenAddress => bool active) private runeTokensActive;
Remediation

SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.

Remediation Hash
References

7.8 Floating pragma

//

Informational

Description

All contracts in scope currently use floating pragma versions ^0.8.26 which means that the code can be compiled by any compiler version that is greater than or equal to 0.8.26, and less than 0.9.0.


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


In this aspect, it is crucial to select the appropriate EVM version when it's intended to deploy the contracts on networks other than the Ethereum mainnet, which may not support these opcodes. Failure to do so could lead to unsuccessful contract deployments or transaction execution issues.

BVSS
Recommendation

Lock the pragma version to the same version used during development and testing. Additionally, make sure to specify the target EVM version when using Solidity versions from 0.8.20 and above if deploying to chains that may not support newly introduced opcodes. Additionally, it is crucial to stay informed about the opcode support of different chains to ensure smooth deployment and compatibility.

Remediation

SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.

Remediation Hash
References

7.9 Important fields in events are not indexed

//

Informational

Description

None of the important fields in the events emitted in the pontis-bridge-fee-manager.sol contract and the pontis-bridge-v1.sol contracts are indexed.

BVSS
Recommendation

Index the appropriate fields in the events that are emitted in the pontis-bridge-fee-manager.sol and pontis-bridge-v1.sol contracts.

Remediation

SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.

Remediation Hash
References

7.10 An important state changing function doesn't emit an event

//

Informational

Description

In the pontis-bridge-nft.sol contract, the setBaseURI() function sets a new baseURI, which is used by the getTokenURI() function in order to return a correct URI address. However, no event is emitted when a new baseURI is set.

BVSS
Recommendation

Emit an event in the setBaseURI() function.

Remediation

SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.

Remediation Hash
References

7.11 Use of memory instead of calldata for an unmodified function argument

//

Informational

Description

In the pontis-bridge-nft.sol contract in the setBaseURI() function, the memory keyword is used for a parameter which is not modified instead of the calldata keyword.

BVSS
Recommendation

Consider using the calldata keyword instead of the memory for function arguments which are not modified.

Remediation

SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.

Remediation Hash
References

8. Automated Testing

Static Analysis Report

Description

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


The security team assessed all findings identified by the Slither software, however, most of the findings are not included in the below results for the sake of report readability.


Output

The findings obtained as a result of the Slither scan were reviewed, and the majority were not included in the report because they were determined as false positives.

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.