Prepared by:
HALBORN
Last Updated 07/04/2024
Date of Engagement by: May 16th, 2024 - May 23rd, 2024
100% of all REPORTED Findings have been addressed
All findings
14
Critical
0
High
0
Medium
2
Low
4
Informational
8
The Entangle team
engaged Halborn
to conduct a security assessment on their smart contracts beginning on 05-16-2024 and ending on 05-23-2024. The security assessment was scoped to the smart contracts provided in the https://github.com/Entangle-Protocol/gorples-evm/commit/29cfaa7512c9d4103cd04d173a95ec800ca2f7da GitHub repository. Commit hashes and further details can be found in the Scope section of this report. The Gorples Coin codebase in scope consists of a deflationary token with equal cross-chain distribution.
Halborn
was provided 1 week 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 penetration testing and 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 accepted and addressed by the Entangle Team
. The main identified issues were:
Missing verification of chain of origin while redeeming. (RISK ACCEPTED)
Inconsistent burn application to sender due to recipient exclusion. (RISK ACCEPTED)
Missing pause mechanism prevents intended halting of core bridging functionality. (SOLVED)
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 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 led 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
).
External libraries and financial-related attacks.
New features/implementations after/within the remediation commit IDs.
Changes that occur outside of the scope of PRs.
The following files, while they affect the in-scope code, were not specifically reviewed as part of the scope and are assumed to not contain any issues:
contracts/ChestManager.sol
@entangle_protocol/oracle-sdk/contracts/lib/PhotonFunctionSelectorLib.sol
EXPLOITABILIY 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
2
Low
4
Informational
8
Security analysis | Risk level | Remediation Date |
---|---|---|
Missing verification of chain of origin while redeeming | Medium | Risk Accepted |
Inconsistent burn application to sender due to recipient exclusion | Medium | Risk Accepted |
Minting and burning functions available during contract paused state | Low | Risk Accepted |
Missing pause mechanism prevents intended halting of core bridging functionality | Low | Solved - 05/23/2024 |
Risk of EVM version incompatibility across chains | Low | Partially Solved - 06/23/2024 |
Replay attack due to unprotected TX hash for redeems | Low | Risk Accepted |
Unoptimized loops | Informational | Solved - 05/23/2024 |
Missing events in setter functions | Informational | Acknowledged |
Missing state variables visibility modifiers | Informational | Solved - 05/23/2024 |
Missing error description | Informational | Acknowledged |
Floating pragma | Informational | Solved - 05/23/2024 |
Unfollowed naming conventions | Informational | Solved - 05/23/2024 |
Incomplete NATSPEC documentation | Informational | Solved - 05/23/2024 |
Public functions not called within the contract can be made external | Informational | Solved - 05/23/2024 |
// Medium
In the redeem()
function, the _fromChain
parameter decoded from the b
calldata parameter is not validated for correctness. This function is designed to facilitate cross-chain token transfers, but this lack of validation could lead to the completion of a bridging interaction with an incorrect or incompatible chain id.
In the following scenario, an arbitrary and invalid chain id is used to execute the redeem()
function:
function setupContract() public {
vm.startPrank(ADMIN);
borpaToken.setChestManager(address(chestManager));
borpaToken.setFeeCollector(feeCollector);
borpaToken.setEndPoint(address(endpoint));
borpaToken.setBridgeRouterAddress(BRIDGE_ROUTER);
borpaToken.setBorpaMaster(BORPA_MASTER);
uint256[] memory chainIds = new uint256[](1);
uint256[] memory minAmounts = new uint256[](1);
chainIds[0] = _eobChainId;
minAmounts[0] = 1;
borpaToken.setMinBridgeAmount(chainIds, minAmounts);
vm.stopPrank();
}
function test_redeemInvalidFromChain(uint amount, uint fee) public {
setupContract();
vm.startPrank(address(endpoint));
bytes32 testBytes = keccak256("bytes32Value");
bytes memory recipientInBytes = abi.encode(alice);
amount = bound(amount, 1, type(uint128).max);
fee = bound(fee, 0, amount);
uint amountMinusFee = amount - fee;
uint arbitraryInvalidFromChain = 1234567890;
bytes memory parameters = abi.encode(recipientInBytes, amount, fee, [testBytes, testBytes], 1234567890, testBytes);
bytes memory b = abi.encode(testBytes, 0, 0, [testBytes, testBytes], parameters);
borpaToken.redeem(b);
uint256 expectedAliceBalance = amountMinusFee - ((amountMinusFee * 100) / 10000);
if (amount < 100) expectedAliceBalance = amountMinusFee;
assertEq(borpaToken.balanceOf(alice), expectedAliceBalance);
uint256 expectedfeeCollectorBalance = fee - (fee * 100) / 10000;
if (fee < 100) expectedfeeCollectorBalance = fee;
assertEq(borpaToken.balanceOf(feeCollector), expectedfeeCollectorBalance);
}
Ensure that the _fromChain
parameter is indeed a valid chain id that the protocol supports.
RISK ACCEPTED: The Entangle team has made a business decision to accept the risk of this finding and not alter the contracts, stating that:
The "fromChain" parameter is transmitted by the agent transmitter and would fail if chain id is not equal to chain id of network where event is fetched. On source network, active chains are also regulated with the minBridgeAmount parameter, which also is a flag of network support. If destination chain is not supported tx should fail on src chain with min bridge amount is not set.
// Medium
The _update()
function in the GorplesCoin
contract performs the calculation of tokens to burn prior to updating the balances of the involved addresses.
However, the calculation of the fees to burn is not executed when either of the two addresses involved is excluded from the burn mechanism. This results in the burn not being applied even if only the recipient (to
address) is excluded from the burn, while the intention seems to be to burn tokens from the from
address.
function _update(address from, address to, uint256 amount) internal override {
if (blacklisted[from] || blacklisted[to]) revert BorpaToken__E11();
if (!isExcludedFromBurn[from] && !isExcludedFromBurn[to]) {
uint256 fees = (amount * BURN_PERCENTAGE) / BURN_DENOMINATOR;
if (fees > 0) {
amount -= fees;
totalSystemBurnedAmount = totalSystemBurnedAmount + fees;
super._update(from, address(0), fees);
}
}
super._update(from, to, amount);
}
In the following scenario, the recipients of the minted tokens are excluded from burning and the fee is not calculated for the sender:
function test_recipientExcludedFromBurn(uint _mintAmount, uint _fee, uint256 _burnAmount) public {
_mintAmount = bound(_mintAmount, 1, type(uint128).max);
_fee = bound(_fee, 0, _mintAmount);
setupContract();
//Exclude alice from burning
vm.startPrank(ADMIN);
address[] memory addresses = new address[](2);
bool[] memory status = new bool[](2);
addresses[0] = alice;
addresses[1] = feeCollector;
status[0] = true;
status[1] = true;
borpaToken.setBurnExcluded(addresses, status);
vm.stopPrank();
bytes32 testBytes = keccak256("bytes32Value");
bytes memory recipientInBytes = abi.encode(alice);
bytes memory parameters = abi.encode(recipientInBytes, _mintAmount, _fee, [testBytes, testBytes], 0, testBytes);
bytes memory b = abi.encode(testBytes, 0, 0, [testBytes, testBytes], parameters);
vm.startPrank(address(endpoint));
borpaToken.redeem(b);
assertEq(borpaToken.balanceOf(alice), _mintAmount - _fee);
assertEq(borpaToken.balanceOf(feeCollector), _fee);
}
To ensure that the burn fee is applied correctly based on the from
address alone, the logic should be modified to check and exclude fee calculation only if the from
address is excluded from the burn mechanism, e.g.,:
if (!isExcludedFromBurn[from]) {
...
}
RISK ACCEPTED: The Entangle team has made a business decision to accept the risk of this finding and not alter the contracts, stating that:
It was designed to take fee only on the path, where both from and to are NOT excluded. So, if 1 of these addresses is excluded (e.g. one of our smart contracts) - fee will not be taken even if the excluded address is "to". You can imagine approve+deposit situation for this example.
// Low
In the GorplesCoin
contract, there are several state-changing functions that remain accessible even when the contract is paused, as they lack the whenNotPaused()
modifier.
These functions allow for tokens to be minted, approved, transferred or burned while the contract is paused, which can lead to several issues, such as inconsistent state management, off-chain monitoring discrepancies and potential security risks, as the paused state is typically intended to halt all operations that can modify the contract’s state, providing a safe window to address issues.
The affected functions are:
claimMasterRewards()
burn()
systemBurn()
approve()
transfer()
transferFrom()
It is recommended to verify the paused
state of the contract across all crucial state-changing functions by applying the whenNotPaused
modifier. This ensures that these operations are halted when the contract is paused, maintaining the integrity and security of the contract.
RISK ACCEPTED: The Entangle team has made a business decision to accept the risk of this finding and not alter the contracts, stating that:
Pause/unpause functionality was designed specifically for the bridge functions to not have transactions "stuck" in between networks during updates of agent transmitters network of Photon CCM. When bridge is paused we want Borpa ecosystem to continue functioning.
// Low
The GorplesCoin
contract inherits from OpenZeppelin's PausableUpgradeable
contract and implements the whenNotPaused
modifier in the redeem()
and bridge()
functions. However, the contract does not include an external method to enable the pause mechanism to temporarily halt the contract's functionality.
A pause mechanism is essential for emergency situations, such as discovering a critical bug or vulnerability, and can prevent further damage to the contract and its users. Without a pause mechanism, the contract may be vulnerable to exploitation or unintended behavior in case of an emergency, and the whenNotPaused
modifier will have no effect, as the paused
state is unreachable with current implementation.
Add an external method to enable the pause mechanism. This method should only be accessible by authorized addresses.
SOLVED: The Entangle team has addressed the finding in commit fe8eb2b
by following the mentioned recommendation.
// Low
From Solidity versions 0.8.20
through 0.8.24
, the default target EVM version is set to Shanghai
, which results in the generation of bytecode that includes PUSH0
opcodes. Starting with version 0.8.25
, the default EVM version shifts to Cancun
, introducing new opcodes for transient storage, TSTORE
and TLOAD
.
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, particularly over cross-chain functionality like the codebase in scope.
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.
PARTIALLY SOLVED: The Entangle team has partially solved the issue by setting an explicit Solidity compiler version to, but0.8.24
lacks a specific EVM version for compilation and deployment. Further testing is required to ensure cross-chain compatibility.
// Low
In the GorplesCoin
contract, the redeem()
function is susceptible to a replay attack, where the txHash
parameter could potentially be used multiple times to execute the function with the same data.
This txHash
parameter, as decoded from the b
calldata, is used as part of the bridging transaction confirmation without any mechanism to ensure its uniqueness or to prevent its reuse. As a result, the contract may be susceptible to a replay attack, where the same txHash
could potentially be used to execute the redeem()
function multiple times, leading to unauthorized or unintended transactions.
function redeem(bytes calldata b) external onlyRole(ENDPOINT) whenNotPaused {
(, , , , bytes memory params) = abi.decode(b, (bytes32, uint256, uint256, bytes32[2], bytes));
(bytes memory _to, uint256 _amount, uint256 _fee, bytes32[2] memory _txhash, uint256 _fromChain, bytes32 _marker) = abi.decode(
params,
(bytes, uint256, uint256, bytes32[2], uint256, bytes32)
);
address to = abi.decode(_to, (address));
_mint(to, _amount - _fee);
_mint(feeCollector, _fee);
emit BridgeDone(to, _amount, _txhash, _fromChain, _marker);
}
It is worth mentioning that the ENDPOINT
contract that executes the redeem()
function will verify if the hash of the complete OperationData has been executed previously, as referenced in the following out-of-scope contract: https://github.com/Entangle-Protocol/photon-cross-chain-messaging/blob/develop/contracts/EndPoint.sol#L421.
In the following scenario, the same txHash
is used twice to execute the same function call:
function test_redeemTxReplayAttack(uint amount, uint fee) public {
setupContract();
vm.startPrank(address(endpoint));
bytes32 testBytes = keccak256("bytes32Value");
bytes memory recipientInBytes = abi.encode(alice);
amount = bound(amount, 1, type(uint128).max);
fee = bound(fee, 0, amount);
uint amountMinusFee = amount - fee;
bytes memory parameters = abi.encode(recipientInBytes, amount, fee, [testBytes, testBytes], 0, testBytes);
bytes memory b = abi.encode(testBytes, 0, 0, [testBytes, testBytes], parameters);
borpaToken.redeem(b);
borpaToken.redeem(b);
}
Introduce additional validation in the redeem()
function to verify that each txHash
has not been used in previous transactions.
RISK ACCEPTED: The Entangle team has made a business decision to accept the risk of this finding and not alter the contracts, stating that:
Protected by MSC (Master Smart Contract in Photon CCM) and EndPoint contract, which calls Borpa on destination chain.
// Informational
Throughout the GorplesCoin
contract, there are several instances of unoptimized for
loop declarations that may incur in higher gas costs than necessary. The affected functions are:
setMinBridgeAmount()
setBlacklisted()
setBurnExcluded()
pendingEmission()
Optimize the for
loop declarations to reduce gas costs. Best practices include: the non-redundant initialization of the iterator with a default value (i=0
instead of simply i
), the use of the pre-increment operator (++i
), and the unchecked
keyword for incrementing by 1
. For example:
for (uint256 i; i < loopAmount;) {
// code logic
unchecked { ++i; }
}
Additionally, it can be considered to remove unneeded declaration of local variables inside loops. Particularly, in the setMinBridgeAmount()
function:
uint256 key = chainIds[i];
uint256 value = minAmounts[i];
minBridgeAmounts[key] = value;
Here, the key
and value
variables can be omitted if the intention is to use these values only once:
minBridgeAmounts[chainIds[i]] = minAmounts[i];
SOLVED: The Entangle team has addressed the finding in commit c116124
by following the mentioned recommendation.
// Informational
Throughout the GorplesCoin
contract, there are several instances where administrative functions change contract state by modifying core state variables. However, these changes are not reflected in any event emission.
These functions include:
setBorpaMaster()
setMinBridgeAmount()
setBlacklisted()
setBurnExcluded()
setEmissionStart()
setChestManager()
setEndPoint()
setBridgeRouterAddress()
setFeeCollector()
Introduce event declarations and ensure their emission within the aforementioned functions. This change will enhance transparency and facilitate accurate off-chain monitoring.
ACKNOWLEDGED: The Entangle team made a business decision to acknowledge this finding and not alter the contracts, stating:
Not sure if we need that since all functions are meant to be invoked once, and every address of setting is public by default (or will be after fixes).
// Informational
In the GorplesCoin
contract, some of the state variables declared are not explicitly declared with any visibility modifiers. In Solidity, the compiler will assume any state variable declared in the contract as internal
by default. However, it is considered best practice to explicitly specify visibility to enhance clarity and prevent ambiguity. Clearly labeling the visibility of all variables and functions will help in maintaining clear and understandable code.
The affected variables are: protocolId
, eobChainId
, endPoint
and minBridgeAmounts
.
Explicitly define the visibility for each state variable within the contract.
SOLVED: The Entangle team has addressed the finding in commit efdbc68
by following the mentioned recommendation.
// Informational
In the setFeeCollector()
function, the if (_feeCollector == address(0)) revert();
statement is missing an error description in case of not meeting the requirements.
Providing clear error descriptions helps developers and users quickly identify issues when transactions do not proceed as expected.
Add an error description to the aforementioned statement.
ACKNOWLEDGED: The Entangle team made a business decision to acknowledge this finding and not alter the contracts, stating that:
The function is only called by admin with one possible error.
// Informational
The file in scope currently use floating pragma version ^0.8.20
, which means that the code can be compiled by any compiler version that is greater than or equal to 0.8.20
, 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.
Lock the pragma version to the same version used during development and testing.
SOLVED: The Entangle team has addressed the finding in commit 077dbcc
by following the mentioned recommendation.
// Informational
In Solidity, it is a common practice to name internal functions with a single underscore prefix. This is to indicate that these functions are not meant to be used outside of the contract. However, the emitAllocations()
function in the GorplesCoin
contract does not follow this naming convention.
Additionally, the startRecepient
variable from the BorpaToken
contract appears to be misnamed. While this typo does not affect the functionality of the contract, it can lead to confusion when reading through the codebase.
To adhere to best practices and improve code readability, it is recommended to rename the internal function emitAllocations()
to _emitAllocations()
. Additionally, rename the startRecepient
variable to startRecipient
.
SOLVED: The Entangle team has addressed the finding in commit a16656a
by following the mentioned recommendation.
// Informational
While the majority of the functions in the GorplesCoin
have NATSPEC comments in functions, there are still some functions and state variables that are missing these comments, which may their readability and the overall user experience. Proper NATSPEC documentation is crucial for enhancing the clarity and usability of smart contracts, especially for developers and end-users interacting with them.
Add proper NATSPEC documentation to the entirety of the contract.
SOLVED: The Entangle team has addressed the finding in commit 4388085
by following the mentioned recommendation.
// Informational
The GorplesCoin
contract currently defines the currentEmissionRate()
function with public
visibility, even though it is not called from within the smart contract, resulting in higher gas costs than necessary.
Modify the aforementioned function with the external
visibility modifier.
SOLVED: The Entangle team has addressed the finding in commit f0bac73
by following the mentioned recommendation.
Halborn
used automated testing techniques to enhance the coverage of certain areas of the scoped contracts. 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 abi and binary format, Slither was run against the contracts. 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, findings with related to external dependencies 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 not included in the report because they were determined as false positives.
The original repository used the Foundry environment to develop and test the smart contracts. All original tests were executed successfully. Additionally, these tests were extended locally to generate additional fuzz tests against a local fork of the Ethereum main network that covered ~100,000 runs per test. These additional tests were run successfully.
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