Prepared by:
HALBORN
Last Updated 08/26/2024
Date of Engagement by: June 19th, 2024 - August 7th, 2024
86% of all REPORTED Findings have been addressed
All findings
14
Critical
0
High
1
Medium
1
Low
6
Informational
6
Holograph
engaged Halborn to conduct a security assessment on their smart contracts beginning on June 19th, 2024 and ending on August 7th, 2024. The security assessment was scoped to the smart contracts provided in the following GitHub repository:
The team at Halborn was provided 7 weeks for the engagement and assigned a full-time security engineer to verify 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 low security risks that were mostly addressed by the Holograph team
.
Halborn performed a combination of manual 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 the code and can quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the assessment:
- 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.
- Manual testing by custom scripts.
- Static Analysis of security for scoped contract, and imported functions. (Slither
)
- Testnet deployment. (Foundry
)
HolographGenesis
:
Test | Output |
---|---|
Ensure that only approved deployers can call the | PASS |
Ensure that contracts are deployed with the correct bytecode and initialized correctly. | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that only approved deployers can call the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the contract reverts if | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that contracts can be deployed in any chain. | PASS |
Ensure that signatures are not malleable. | FAIL |
HolographGenesisLocal
:
Test | Output |
---|
Ensure that only approved deployers can call the | PASS |
Ensure that contracts are deployed with the correct bytecode and initialized correctly. | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that only approved deployers can call the | PASS |
Ensure that the | PASS |
Ensure that the contract reverts if | PASS |
Ensure that the | PASS |
Ensure that contracts can be deployed in any chain. | FAIL |
Holograph
:
Test | Output |
---|---|
Ensure that the contract can be initialized correctly. | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the contract reverts when receiving ether. | PASS |
HolographRegistry
:
Test | Output |
---|---|
Ensure that the contract can be initialized correctly. | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that only the factory can call the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the | PASS |
Ensure that only admin can call the | PASS |
Ensure that the contract reverts when receiving ether. | PASS |
Ensure that the contract reverts when calling an undefined function. | PASS |
HolographFactory
:
Test | Output |
---|---|
Ensure that the contract can be initialized correctly. | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that only the factory can call the | PASS |
Ensure that the contract reverts when receiving ether. | PASS |
Ensure that the contract reverts when calling an undefined function. | PASS |
Ensure that signatures are correctly verified and can not be replayed. | FAIL |
HolographOperator
:
Test | Output |
---|---|
Ensure that the contract can be initialized correctly. | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the contract reverts when calling an undefined function. | PASS |
Ensure that the contract correctly receives ether. | PASS |
Ensure that gas price spikes are handled correctly. | FAIL |
Ensure best-coding practices are followed. | FAIL (Lack of check-effects-interactions pattern) |
Ensure that the | FAIL |
HolographBridge
:
Test | Output |
---|
Ensure that the contract can be initialized correctly. | PASS |
Ensure that only the operator can call the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that the contract reverts when calling an undefined function. | PASS |
Ensure that the contract correctly prevents ether transfers. | PASS |
HolographTreasury
:
Test | Output |
---|
Ensure that the contract can be initialized correctly. | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that the | PASS |
Ensure that the contract correctly accepts ether transfers. | PASS |
Ensure that the contract reverts when calling an undefined function. | PASS |
HolographInterfaces
:
Test | Output |
---|
Ensure that the contract can be initialized correctly. | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that | PASS |
Ensure that only the admin can call the | PASS |
Ensure that the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that | PASS |
Ensure that | PASS |
Ensure that the contract reverts when receiving ether. | PASS |
Ensure that the contract reverts when calling an undefined function. | PASS |
LayerZeroModule
:
Test | Output |
---|
Ensure that the contract can be initialized correctly. | PASS |
Ensure that only the admin can call the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that only the admin can call the | PASS |
Ensure that | PASS |
Ensure that | PASS |
Ensure that | PASS |
Ensure that | PASS |
Ensure that | PASS |
Ensure that | PASS |
Ensure that | PASS |
Ensure that | PASS |
Ensure that | PASS |
Ensure that | PASS |
Ensure that the contract reverts when receiving ether. | PASS |
Ensure that the contract reverts when calling an undefined function. | PASS |
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
1
Medium
1
Low
6
Informational
6
Security analysis | Risk level | Remediation Date |
---|---|---|
Gas price spikes cause the selected operator to be vulnerable to front-running and be slashed | High | Risk Accepted |
Centralization risk in Admin.adminCall() function | Medium | Risk Accepted |
HolographFactory.deployHolographableContract() function is vulnerable to crosschain signature replay attacks | Low | Risk Accepted |
HolographOperator.recoverJob() function does not follow the check effects interactions pattern | Low | Solved - 08/16/2024 |
Updating the utility token through the HolographOperator.setUtilityToken() function will break the _bondedAmounts[operator] mapping | Low | Partially Solved - 08/19/2024 |
Initialization functions can be front-run if the contracts are not deployed and initialized atomically | Low | Risk Accepted |
Incompatibility with non-standard ERC20 tokens | Low | Solved - 08/16/2024 |
Use call instead of transfer to transfer native assets | Low | Risk Accepted |
Lack of a double step transfer ownership pattern in the Admin contract | Informational | Acknowledged |
Direct usage of ecrecover allows for signature malleability | Informational | Acknowledged |
Use of unlicensed smart contracts | Informational | Acknowledged |
Operator can bribe the miner and steal honest operator's bond amount in all the chains that do not implement the EIP-1559 | Informational | Future Release |
Low level calls previous to solidity version 0.8.14 can trigger an optimizer bug | Informational | Acknowledged |
Use of block.timestamp as a source of entropy | Informational | Future Release |
// High
In the HolographOperator
contract, gas price spikes expose the selected operator to frontrunning and slashing vulnerabilities. The current code includes:
require(gasPrice >= tx.gasprice, "HOLOGRAPH: gas spike detected");
// operator slashing logic
_bondedAmounts[job.operator] -= amount;
_bondedAmounts[msg.sender] += amount;
This mechanism aims to prevent operators from being slashed due to gas spikes. However, it allows other operators to front-run by submitting their transactions with a higher gas price, resulting in the selected operator being slashed if they delay their transaction.
Adjust the operator node software to queue transactions immediately with the gas price specified in bridgeInRequestPayload
during a gas spike.
RISK ACCEPTED: The Holograph team accepted the risk of this finding.
// Medium
The Admin
contract implements the function adminCall()
:
function adminCall(address target, bytes calldata data) external payable onlyAdmin {
assembly {
calldatacopy(0, data.offset, data.length)
let result := call(gas(), target, callvalue(), 0, data.length, 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
This function allows any admin to perform a low-level call impersonating the actual contract that inherits from the Admin
contract itself. Moreover, this function is the one that was abused by the privileged 0xc0ffee address that performed the Holograph exploit, last June:
"The exploit was due to unauthorized admin access of a proxy wallet by a disgruntled former contractor who minted approximately $14 million worth of new HLG & sold it on the open market, crashing the price".
During this exploit, the attacker, who was an admin, called the LayerZeroModuleProxy.adminCall()
to call the crossChainMessage()
function in the HolographOperator
contract on behalf of the LayerZero module. This way, he managed to create jobs to mint HLG tokens.
The adminCall()
makes the system vulnerable to abuse by any trusted admin.
Consider removing the adminCall()
function from the Admin
contract.
RISK ACCEPTED: The Holograph team accepted the risk of this finding. The Holograph team plans are to remove this function as the protocol grows older. This issue is known, but also by design, so the Holograph Protocol team can administer the protocol and upgrade as necessary. Additionally, this requires multiple parties to sign off on adminCall
transactions via the Protocol Management Multisig on each network.
Eventually, the Holograph team plans to progressively decentralize and remove adminCall
and hand over changes to the protocol to the community via governance.
// Low
The HolographFactory
implements the function deployHolographableContract()
:
/**
* @notice Deploy a holographable smart contract
* @dev Using this function allows to deploy smart contracts that have the same address across all EVM chains
* @param config contract deployement configurations
* @param signature that was created by the wallet that created the original payload
* @param signer address of wallet that created the payload
*/
function deployHolographableContract(
DeploymentConfig memory config,
Verification memory signature,
address signer
) public {
address registry;
address holograph;
assembly {
holograph := sload(_holographSlot)
registry := sload(_registrySlot)
}
/**
* @dev the configuration is encoded and hashed along with signer address
*/
bytes32 hash = keccak256(
abi.encodePacked(
config.contractType,
config.chainType,
config.salt,
keccak256(config.byteCode),
keccak256(config.initCode),
signer
)
);
/**
* @dev the hash is validated against signature
* this is to guarantee that the original creator's configuration has not been altered
*/
require(_verifySigner(signature.r, signature.s, signature.v, hash, signer), "HOLOGRAPH: invalid signature");
/**
* @dev check that this contract has not already been deployed on this chain
*/
bytes memory holographerBytecode = type(Holographer).creationCode;
address holographerAddress = address(
uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), hash, keccak256(holographerBytecode)))))
);
require(!_isContract(holographerAddress), "HOLOGRAPH: already deployed");
/**
* @dev convert hash into uint256 which will be used as the salt for create2
*/
uint256 saltInt = uint256(hash);
address sourceContractAddress;
bytes memory sourceByteCode = config.byteCode;
assembly {
/**
* @dev deploy the user created smart contract first
*/
sourceContractAddress := create2(0, add(sourceByteCode, 0x20), mload(sourceByteCode), saltInt)
}
assembly {
/**
* @dev deploy the Holographer contract
*/
holographerAddress := create2(0, add(holographerBytecode, 0x20), mload(holographerBytecode), saltInt)
}
/**
* @dev initialize the Holographer contract
*/
require(
InitializableInterface(holographerAddress).init(
abi.encode(abi.encode(config.chainType, holograph, config.contractType, sourceContractAddress), config.initCode)
) == InitializableInterface.init.selector,
"initialization failed"
);
/**
* @dev update the Holograph Registry with deployed contract address
*/
HolographRegistryInterface(registry).setHolographedHashAddress(hash, holographerAddress); //@audit overwritten if holograph is changed
/**
* @dev emit an event that on-chain indexers can easily read
*/
emit BridgeableContractDeployed(holographerAddress, hash);
}
This function makes use of a signature. The signed hash is built as:
bytes32 hash = keccak256(
abi.encodePacked(
config.contractType,
config.chainType,
config.salt,
keccak256(config.byteCode),
keccak256(config.initCode),
signer
)
);
As the hash is built without any domain separator, this signature could be used across multiple chains. For example, signatures created by Bob in the Sepolia test network could be re-used also in Ethereum mainnet.
Consider including a domain separator in the HolographFactory.deployHolographableContract()
function signed hash.
RISK ACCEPTED: The Holograph team accepted the risk of this finding.
// Low
The HolographOperator
contract implements the function recoverJob()
:
/**
* @notice Recover failed job
* @dev If a job fails, it can be manually recovered
* @param bridgeInRequestPayload the entire cross chain message payload
*/
function recoverJob(bytes calldata bridgeInRequestPayload) external payable onlyAdmin {
bytes32 hash = keccak256(bridgeInRequestPayload);
require(_failedJobs[hash], "HOLOGRAPH: invalid recovery job");
(bool success, ) = _bridge().call{value: msg.value}(bridgeInRequestPayload);
require(success, "HOLOGRAPH: recovery failed");
delete (_failedJobs[hash]);
}
This function can only be executed by an admin in case of a failed job. However, the _failedJobs
mapping is deleted after the _bridge().call()
and consequently, it is entirely possible that if the bridgeInRequestPayload
contains a call to the recoverJob()
function, the job is re-entered and executed more than once. Note that this exploit requires also a malicious admin doing the call.
Consider updating the recoverJob()
function as shown below, following the check-effects-interactions pattern:
/**
* @notice Recover failed job
* @dev If a job fails, it can be manually recovered
* @param bridgeInRequestPayload the entire cross chain message payload
*/
function recoverJob(bytes calldata bridgeInRequestPayload) external payable onlyAdmin {
bytes32 hash = keccak256(bridgeInRequestPayload);
require(_failedJobs[hash], "HOLOGRAPH: invalid recovery job");
delete (_failedJobs[hash]);
(bool success, ) = _bridge().call{value: msg.value}(bridgeInRequestPayload);
require(success, "HOLOGRAPH: recovery failed");
}
SOLVED: The Holograph team solved the issue by implementing the recommended solution.
// Low
The HolographOperator
contract implements the function setUtilityToken()
:
/**
* @notice Update the Holograph Utility Token address
* @param utilityToken address of the Holograph Utility Token smart contract to use
*/
function setUtilityToken(address utilityToken) external onlyAdmin {
assembly {
sstore(_utilityTokenSlot, utilityToken)
}
}
However, when a new utility token is set by an admin, the _bondedAmounts
mapping is not reset to 0, breaking all the slashing infrastructure.
When a new utility token is set by an admin all the _bondedAmounts
keys should be reset to 0. On the other hand, as the previous suggestion is nearly impossible to implement without very high gas costs, consinder simply removing the setUtilityToken()
function.
PARTIALLY SOLVED: The Holograph team partially solved the issue as:
The team is totally aware of the inconsistency caused if this function is ever called.
The following warning was added as a comment at smart contract level: "This function should only be used in the event of a token migration which should never happen. Updating this will break all the slashing and bonding logic. To update the utility token in a safe way before calling this function, the bondedAmounts and operatorPods arrays should be reset to zero".
// Low
All the upgradeable contracts across the Holograph codebase implement initialize functions. However, as these contracts do not implement the disableInitializers()
modifier their initialization could be front-run if they were not deployed and initialized atomically. Often, this would require to perform a new deployment.
Consider calling the _disableInitializers
function in all the contracts' constructor:
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
RISK ACCEPTED: The Holograph team accepted the risk of this finding. The Holograph team states that Holograph Protocol contracts are always deployed and initialized atomically via the deployment scripts, so this isn’t a real concern, but they will consider the suggestion for future contracts that do not go through the existing deployment pipeline.
// Low
In the HolographOperator
, multiple calls to transfer()
or transferFrom()
are executed using the IERC20
interface:
_utilityToken().transfer((isBonded ? msg.sender : address(_holograph().getTreasury())), amount);
_utilityToken().transfer(job.operator, leftovers);
require(_utilityToken().transferFrom(msg.sender, address(this), amount), "HOLOGRAPH: token transfer failed");
require(_utilityToken().transferFrom(msg.sender, address(this), amount), "HOLOGRAPH: token transfer failed");
require(_utilityToken().transfer(recipient, amount), "HOLOGRAPH: token transfer failed");
In some tokens like USDT the transfer()
and transferFrom()
functions do not return a bool
:
/**
* @dev transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address _to, uint _value) public onlyPayloadSize(2 * 32) {
uint fee = (_value.mul(basisPointsRate)).div(10000);
if (fee > maximumFee) {
fee = maximumFee;
}
uint sendAmount = _value.sub(fee);
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(sendAmount);
if (fee > 0) {
balances[owner] = balances[owner].add(fee);
Transfer(msg.sender, owner, fee);
}
Transfer(msg.sender, _to, sendAmount);
}
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint the amount of tokens to be transferred
*/
function transferFrom(address _from, address _to, uint _value) public onlyPayloadSize(3 * 32) {
var _allowance = allowed[_from][msg.sender];
// Check is not needed because sub(_allowance, _value) will already throw if this condition is not met
// if (_value > _allowance) throw;
uint fee = (_value.mul(basisPointsRate)).div(10000);
if (fee > maximumFee) {
fee = maximumFee;
}
if (_allowance < MAX_UINT) {
allowed[_from][msg.sender] = _allowance.sub(_value);
}
uint sendAmount = _value.sub(fee);
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(sendAmount);
if (fee > 0) {
balances[owner] = balances[owner].add(fee);
Transfer(_from, owner, fee);
}
Transfer(_from, _to, sendAmount);
}
IERC20
interface expects a bool
as a return of the transfer()
and transferFrom()
calls. In these situations, if the token used was, for example, USDT, the IERC20.transfer()
or IERC20.transferFrom()
calls would revert.
It is recommended to use the OpenZeppelin's safeTransfer() and safeTransferFrom() functions instead of transfer()
& transferFrom()
.
SOLVED: The Holograph team solved the issue by implementing the recommended solution.
// Low
The HolographOperator
contract transfers native assets paid by the operator using the Solidity low-level transfer():
try
HolographOperatorInterface(address(this)).nonRevertingBridgeCall{value: msg.value}(
msg.sender,
bridgeInRequestPayload
)
{
/// @dev do nothing
} catch {
/// @dev return any payed funds in case of revert
payable(msg.sender).transfer(msg.value); //@audit use call instead of transfer
_failedJobs[hash] = true;
emit FailedOperatorJob(hash);
}
In Solidity, the call()
function is often preferred over transfer()
for sending Ether due to some gas limit considerations:
transfer
: Imposes a fixed gas limit of 2300 gas. This limit can be too restrictive, especially if the receiving contract is a multisig wallet that executes more complex logic in its receive()
function. For example, native transfer()
calls to Gnosis Safe multisigs will always revert with an out-of-gas error in Binance Smart Chain.
call
: Allows specifying a custom gas limit, providing more flexibility and ensuring that the receiving contract can perform necessary operations.
It should be noted that using call
also requires explicit reentrancy protection mechanisms (e.g., using checks-effects-interactions pattern or the ReentrancyGuard
contract from OpenZeppelin).
Consider using call()
over transfer()
to transfer native assets in order to ensure compatibility with any type of multisig wallet. As for the reentrancy risks, these are currently mitigated in the executeJobs()
function as _failedJobs
is updated after the actual transfer.
RISK ACCEPTED: The Holograph team accepted the risk of this finding.
// Informational
The current ownership transfer process for all the contracts inheriting from Admin
involves the current admin calling the setAdmin()
function:
function setAdmin(address adminAddress) public onlyAdmin {
assembly {
sstore(_adminSlot, adminAddress)
}
}
If the nominated EOA account is not a valid account, it is entirely possible that the owner may accidentally transfer ownership to an uncontrolled account, losing the access to all functions with the onlyAdmin
modifier.
It is recommended to implement a two-step process where the owner nominates an account and the nominated account needs to call an acceptAdmin()
function for the transfer of the ownership to fully succeed. This ensures the nominated EOA account is a valid and active account.
ACKNOWLEDGED: The Holograph team acknowledged this finding.
// Informational
In the HolographGenesis
contract, the recoverSigner()
function calls the Solidity ecrecover
function directly to verify the given signature. However, the ecrecover
EVM opcode allows malleable(non-unique) signatures and thus is susceptible to replay attacks.
Although a replay attack is not possible here as it is prevented with the _approveDeployerNonce
state variable, ensuring that signatures are not malleable is considered a best practice.
Consider using the [recover() function from OpenZeppelin’s ECDSA library](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol#L75-L93) for signature verification.
ACKNOWLEDGED: The Holograph team acknowledged this finding.
// Informational
All the Holograph smart contracts are marked as unlicensed, as indicated by the SPDX license identifier at the top of the files:
// SPDX-License-Identifier: UNLICENSED
Using unlicensed contract can lead to legal uncertainties and potential conflicts regarding the usage, modification and distribution rights of the code. This may deter other developers from using or contributing to the project and could potentially lead to legal issues in the future.
It is recommended to choose and apply an appropriate open-source license to the smart contracts. Some popular options for blockchain and smart contract projects include:
MIT License: A permissive license that allows for reuse with minimal restrictions.
GNU General Public License (GPL): A copyleft license that ensures derivative works are also open-source.
Apache License 2.0: A permissive license that provides an express grant of patent rights from contributors to users.
ACKNOWLEDGED: The Holograph team acknowledged this finding.
// Informational
Operators in Holograph perform tasks using the executeJob()
function with bridged bytes from the source chain. If the primary operator fails to execute the job within the allocated block, a bond is taken and transferred to the operator doing the job. The presence of a gas spike is checked with tx.gasprice
:
require(gasPrice >= tx.gasprice, "HOLOGRAPH: gas spike detected");
However, attackers can exploit this by submitting a flashbots bundle with executeJob()
at a low gas price and an additional bribe to miners, enabling them to steal the bond from honest operators. This is not theoretical, as MEV bots exploit such opportunities regularly:
See coinbase.transfer().
See Bundle selection.
Therefore, a dishonest operator can seize an honest operator's bond even when gas prices are high.
Avoid using current tx.gasprice
for previous block gas price inference. It is recommended to use a gas price oracle instead.
PENDING: The Holograph team will consider addressing this in a future release.
// Informational
All the contracts in scope are using low-level calls with the 0.8.13 solidity compiler version, which can trigger an optimizer bug. See the following article.
The bug involves the Solidity compiler's optimizer making overly aggressive assumptions about certain storage writes and reads. Specifically, the optimizer may incorrectly assume that a storage slot being read from has not been written to earlier in the same function, leading to discrepancies between the actual storage content and the optimizer's expectations. For example, let's imagine a smart contract function that writes to a storage slot and later reads from it within the same function. Due to the optimizer's incorrect assumptions, the read operation might return the old value instead of the updated one.
Consider updating your contracts to a newer solidity version.
ACKNOWLEDGED: The Holograph team acknowledged this finding.
// Informational
Using block.timestamp
as a source of entropy to generate randomness in smart contracts is not recommended as it can be influenced by miners to a certain extent, making it a weak and predictable source of randomness. This can result in the manipulation of supposedly random outcomes. The block.timestamp
is a value that miners can manipulate within a reasonable range (typically up to 15 seconds). This flexibility allows miners to adjust the timestamp in their favor, especially when there is an economic incentive to do so.
In the HolographOperator
contract, the function crossChainMessage()
uses block.timestamp
as a source of entropy to generate a random number:
uint256 random = uint256(keccak256(abi.encodePacked(jobHash, _jobNonce(), block.number, block.timestamp)));
Consider removing the block.number
and the block.timestamp
from the source of entropy used to generate the random number in the HolographOperator.crossChainMessage()
function.
PENDING: The Holograph team will consider addressing this in a future release.
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 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.
HolographGenesis.sol
HolographGenesisLocal.sol
Holograph.sol
HolographRegistry.sol
HolographFactory.sol
HolographOperator.sol
HolographBridge.sol
HolographTreasury.sol
HolographInterfaces.sol
LayerZeroModule.sol
No major issues were found by Slither.
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