Prepared by:
HALBORN
Last Updated 01/28/2025
Date of Engagement: January 8th, 2025 - January 13th, 2025
100% of all REPORTED Findings have been addressed
All findings
11
Critical
0
High
0
Medium
1
Low
1
Informational
9
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.
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.
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
).
EXPLOITABILITY METRIC () | METRIC VALUE | NUMERICAL 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 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL 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 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
0
Medium
1
Low
1
Informational
9
Security analysis | Risk level | Remediation Date |
---|---|---|
Not sufficient restrictions may lead to a DOS of core functions | Medium | Risk Accepted - 01/21/2025 |
The oracle implementation may return stale prices | Low | Risk Accepted - 01/21/2025 |
Lack of a double-step transferOwnership pattern | Informational | Partially Solved - 01/21/2025 |
Custom Errors Should be Used | Informational | Solved - 01/21/2025 |
Use of transfer instead of call | Informational | Solved - 01/21/2025 |
The length of an array is not cached | Informational | Solved - 01/21/2025 |
Consider Using Named Mappings | Informational | Solved - 01/21/2025 |
Floating pragma | Informational | Solved - 01/21/2025 |
Important fields in events are not indexed | Informational | Solved - 01/21/2025 |
An important state changing function doesn't emit an event | Informational | Solved - 01/21/2025 |
Use of memory instead of calldata for an unmodified function argument | Informational | Solved - 01/21/2025 |
//
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.
Consider checking whether the user burning the ERC20 tokens is a user for whom a PegOutKeyUtxo
have been added by the off-chain system.
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.
//
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.
Consider fetching the prices from oracles such as Chainlink or Pyth.
RISK ACCEPTED: The Pontis team accepted the risk of this finding.
//
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.
Consider utilizing OpenZeppelin Ownable2Step.sol
contract.
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.
//
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.
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.
SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.
//
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:
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.
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.
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.
SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.
//
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.
Consider caching the length of the array, before you loop over it.
SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.
//
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.
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;
SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.
//
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.
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.
SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.
//
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.
Index the appropriate fields in the events that are emitted in the pontis-bridge-fee-manager.sol
and pontis-bridge-v1.sol
contracts.
SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.
//
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.
Emit an event in the setBaseURI()
function.
SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.
//
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.
Consider using the calldata keyword instead of the memory for function arguments which are not modified.
SOLVED: The Pontis team has followed the recommendation and successfully resolved the issue.
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.
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.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed