Halborn Logo

Protocol - Holograph


Prepared by:

Halborn Logo

HALBORN

Last Updated 08/26/2024

Date of Engagement by: June 19th, 2024 - August 7th, 2024

Summary

86% of all REPORTED Findings have been addressed

All findings

14

Critical

0

High

1

Medium

1

Low

6

Informational

6


1. Introduction

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:

2. Assessment Summary

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.

3. Test Approach and Methodology

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)

4. Test cases

HolographGenesis:

Test

Output

Ensure that only approved deployers can call the deploy function.

PASS

Ensure that contracts are deployed with the correct bytecode and initialized correctly.

PASS

Ensure that the deploy function reverts when called with incorrect chain ID.

PASS

Ensure that the isApprovedDeployer function returns the correct status for deployers.

PASS

Ensure that only approved deployers can call the approveDeployer function.

PASS

Ensure that the approveDeployer function updates deployer approval status correctly.

PASS

Ensure that the approveDeployer function requires valid multi-signature verification.

PASS

Ensure that the contract reverts if approveDeployer is called with invalid nonce.

PASS

Ensure that the getMessageHash function returns the correct hash.

PASS

Ensure that the getEthSignedMessageHash function returns the correct prefixed hash.

PASS

Ensure that the recoverSigner function recovers the correct signer address.

PASS

Ensure that the splitSignature function splits the signature correctly.

PASS

Ensure that the getApproveDeployerNonce function returns the correct nonce value.

PASS

Ensure that the getVersion function returns the correct version number.

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 deploy function.

PASS

Ensure that contracts are deployed with the correct bytecode and initialized correctly.

PASS

Ensure that the deploy function reverts when called with incorrect chain ID.

PASS

Ensure that the isApprovedDeployer function returns the correct status for deployers.

PASS

Ensure that only approved deployers can call the approveDeployer function.

PASS

Ensure that the approveDeployer function updates deployer approval status correctly.

PASS

Ensure that the contract reverts if approveDeployer is called by an unapproved deployer.

PASS

Ensure that the getVersion function returns the correct version number.

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 getBridge function returns the correct bridge address.

PASS

Ensure that only admin can call the setBridge function.

PASS

Ensure that the getChainId function returns the correct chain ID.

PASS

Ensure that only admin can call the setChainId function.

PASS

Ensure that the getFactory function returns the correct factory address.

PASS

Ensure that only admin can call the setFactory function.

PASS

Ensure that the getHolographChainId function returns the correct Holograph chain ID.

PASS

Ensure that only admin can call the setHolographChainId function.

PASS

Ensure that the getInterfaces function returns the correct interfaces address.

PASS

Ensure that only admin can call the setInterfaces function.

PASS

Ensure that the getOperator function returns the correct operator address.

PASS

Ensure that only admin can call the setOperator function.

PASS

Ensure that the getRegistry function returns the correct registry address.

PASS

Ensure that only admin can call the setRegistry function.

PASS

Ensure that the getTreasury function returns the correct treasury address.

PASS

Ensure that only admin can call the setTreasury function.

PASS

Ensure that the getUtilityToken function returns the correct utility token address.

PASS

Ensure that only admin can call the setUtilityToken function.

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 getHolograph function returns the correct holograph address.

PASS

Ensure that only admin can call the setHolograph function.

PASS

Ensure that the getHolographableContracts function returns the correct list of contracts.

PASS

Ensure that the getHolographableContractsLength function returns the correct number of contracts.

PASS

Ensure that the getHolographedHashAddress function returns the correct address for a given hash.

PASS

Ensure that only the factory can call the setHolographedHashAddress function.

PASS

Ensure that the getHToken function returns the correct hToken address for a given chain ID.

PASS

Ensure that only admin can call the setHToken function.

PASS

Ensure that the getContractTypeAddress function returns the correct address for a given contract type.

PASS

Ensure that the setContractTypeAddress function sets the address correctly for reserved types.

PASS

Ensure that only admin can call the setContractTypeAddress function.

PASS

Ensure that the getReservedContractTypeAddress function returns the correct address for a reserved contract type.

PASS

Ensure that only admin can call the setReservedContractTypeAddress function.

PASS

Ensure that only admin can call the setReservedContractTypeAddresses function.

PASS

Ensure that the getUtilityToken function returns the correct utility token address.

PASS

Ensure that only admin can call the setUtilityToken function.

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 setHolograph function.

PASS

Ensure that the getHolograph function returns the correct holograph address.

PASS

Ensure that only the admin can call the setRegistry function.

PASS

Ensure that the getRegistry function returns the correct registry address.

PASS

Ensure that the deployHolographableContract function deploys the contract correctly.

PASS

Ensure that the deployHolographableContract function checks for valid signature.

PASS

Ensure that the deployHolographableContract function reverts if the contract is already deployed.

PASS

Ensure that the bridgeIn function deploys a holographable contract correctly via bridge request.

PASS

Ensure that the bridgeOut function returns the correct selector and payload.

PASS

Ensure that only the factory can call the setHolographedHashAddress function.

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 setBridge function.

PASS

Ensure that the getBridge function returns the correct bridge address.

PASS

Ensure that only the admin can call the setHolograph function.

PASS

Ensure that the getHolograph function returns the correct holograph address.

PASS

Ensure that only the admin can call the setInterfaces function.

PASS

Ensure that the getInterfaces function returns the correct interfaces address.

PASS

Ensure that only the admin can call the setMessagingModule function.

PASS

Ensure that the getMessagingModule function returns the correct messaging module address.

PASS

Ensure that only the admin can call the setRegistry function.

PASS

Ensure that the getRegistry function returns the correct registry address.

PASS

Ensure that only the admin can call the setUtilityToken function.

PASS

Ensure that the getUtilityToken function returns the correct utility token address.

PASS

Ensure that only the admin can call the setMinGasPrice function.

PASS

Ensure that the getMinGasPrice function returns the correct minimum gas price.

PASS

Ensure that the recoverJob function can be called by the admin to recover a failed job.

PASS

Ensure that the executeJob function executes the job correctly and rewards the operator.

PASS

Ensure that the crossChainMessage function can only be called by the messaging module.

PASS

Ensure that the jobEstimator function returns the correct gas amount remaining.

PASS

Ensure that the send function can be called by the bridge to send cross-chain messages.

PASS

Ensure that the getJobDetails function returns the correct job details.

PASS

Ensure that the getTotalPods function returns the correct number of pods.

PASS

Ensure that the getPodOperatorsLength function returns the correct number of operators in a pod.

PASS

Ensure that the getPodOperators function returns the correct list of operators in a pod.

PASS

Ensure that the getPodOperators function with pagination returns the correct list of operators.

PASS

Ensure that the getPodBondAmounts function returns the correct base and current bond amounts.

PASS

Ensure that the getBondedAmount function returns the correct bonded amount for an operator.

PASS

Ensure that the getBondedPod function returns the correct pod number for an operator.

PASS

Ensure that the getBondedPodIndex function returns the correct pod index for an operator.

PASS

Ensure that the topupUtilityToken function allows operators to top up their bonded amount.

PASS

Ensure that the bondUtilityToken function allows operators to bond utility tokens correctly.

PASS

Ensure that the unbondUtilityToken function allows operators to unbond their tokens correctly.

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 setUtilityToken() function is implemented correctly.

FAIL


HolographBridge:

Test

Output

Ensure that the contract can be initialized correctly.

PASS

Ensure that only the operator can call the bridgeInRequest function.

PASS

Ensure that the bridgeInRequest function handles valid requests correctly.

PASS

Ensure that the bridgeOutRequest function handles valid requests correctly.

PASS

Ensure that the revertedBridgeOutRequest function reverts as expected.

PASS

Ensure that the getBridgeOutRequestPayload function returns the correct payload.

PASS

Ensure that the getMessageFee function returns the correct fee.

PASS

Ensure that only the admin can call the setFactory function.

PASS

Ensure that the getFactory function returns the correct factory address.

PASS

Ensure that only the admin can call the setHolograph function.

PASS

Ensure that the getHolograph function returns the correct holograph address.

PASS

Ensure that the getJobNonce function returns the correct job nonce.

PASS

Ensure that only the admin can call the setOperator function.

PASS

Ensure that the getOperator function returns the correct operator address.

PASS

Ensure that only the admin can call the setRegistry function.

PASS

Ensure that the getRegistry function returns the correct registry address.

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 setBridge function.

PASS

Ensure that the getBridge function returns the correct bridge address.

PASS

Ensure that only the admin can call the setHolograph function.

PASS

Ensure that the getHolograph function returns the correct holograph address.

PASS

Ensure that only the admin can call the setOperator function.

PASS

Ensure that the getOperator function returns the correct operator address.

PASS

Ensure that only the admin can call the setRegistry function.

PASS

Ensure that the getRegistry function returns the correct registry address.

PASS

Ensure that the getHolographMintFee function returns the correct fee.

PASS

Ensure that only the admin can call the setHolographMintFee function.

PASS

Ensure that only the admin can call the withdraw function.

PASS

Ensure that only the admin can call the withdrawTo function.

PASS

Ensure that the withdraw function transfers the correct balance to the admin.

PASS

Ensure that the withdrawTo function transfers the correct balance to the specified recipient.

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 updateUriPrepend function.

PASS

Ensure that the getUriPrepend function returns the correct prepend.

PASS

Ensure that only the admin can call the updateUriPrepends function.

PASS

Ensure that updateUriPrepends updates multiple prepends correctly.

PASS

Ensure that only the admin can call the updateChainIdMap function.

PASS

Ensure that the getChainId function returns the correct chain ID.

PASS

Ensure that only the admin can call the updateChainIdMaps function.

PASS

Ensure that updateChainIdMaps updates multiple chain IDs correctly.

PASS

Ensure that only the admin can call the updateInterface function.

PASS

Ensure that the supportsInterface function returns the correct support status.

PASS

Ensure that only the admin can call the updateInterfaces function.

PASS

Ensure that updateInterfaces updates multiple interfaces correctly.

PASS

Ensure that contractURI returns the correct base64 encoded JSON string.

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 setBridge function.

PASS

Ensure that only the admin can call the setInterfaces function.

PASS

Ensure that only the admin can call the setLZEndpoint function.

PASS

Ensure that only the admin can call the setOperator function.

PASS

Ensure that only the admin can call the setOptimismGasPriceOracle function.

PASS

Ensure that only the admin can call the setGasParameters function.

PASS

Ensure that getBridge function returns the correct bridge address.

PASS

Ensure that getInterfaces function returns the correct interfaces address.

PASS

Ensure that getLZEndpoint function returns the correct endpoint address.

PASS

Ensure that getOperator function returns the correct operator address.

PASS

Ensure that getOptimismGasPriceOracle function returns the correct oracle address.

PASS

Ensure that getGasParameters function returns the correct gas parameters.

PASS

Ensure that send function can only be called by the operator.

PASS

Ensure that lzReceive function can only be called by the approved LayerZero endpoint.

PASS

Ensure that getMessageFee function returns the correct fee values.

PASS

Ensure that getHlgFee function returns the correct Holograph fee.

PASS

Ensure that the contract reverts when receiving ether.

PASS

Ensure that the contract reverts when calling an undefined function.

PASS

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

5.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:
EXPLOITABILIY 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

5.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}

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

6. SCOPE

Files and Repository
(a) Repository: holograph-protocol
(b) Assessed Commit ID: 724122d
(c) Items in scope:
  • src/HolographTreasury.sol
  • src/HolographBridge.sol
  • src/HolographGenesis.sol
↓ Expand ↓
Out-of-Scope:
Remediation Commit ID:
  • 232c78c
  • eca3fb9
  • a6e9d0f
Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

0

High

1

Medium

1

Low

6

Informational

6

Security analysisRisk levelRemediation Date
Gas price spikes cause the selected operator to be vulnerable to front-running and be slashedHighRisk Accepted
Centralization risk in Admin.adminCall() functionMediumRisk Accepted
HolographFactory.deployHolographableContract() function is vulnerable to crosschain signature replay attacksLowRisk Accepted
HolographOperator.recoverJob() function does not follow the check effects interactions patternLowSolved - 08/16/2024
Updating the utility token through the HolographOperator.setUtilityToken() function will break the _bondedAmounts[operator] mappingLowPartially Solved - 08/19/2024
Initialization functions can be front-run if the contracts are not deployed and initialized atomicallyLowRisk Accepted
Incompatibility with non-standard ERC20 tokensLowSolved - 08/16/2024
Use call instead of transfer to transfer native assetsLowRisk Accepted
Lack of a double step transfer ownership pattern in the Admin contractInformationalAcknowledged
Direct usage of ecrecover allows for signature malleabilityInformationalAcknowledged
Use of unlicensed smart contractsInformationalAcknowledged
Operator can bribe the miner and steal honest operator's bond amount in all the chains that do not implement the EIP-1559InformationalFuture Release
Low level calls previous to solidity version 0.8.14 can trigger an optimizer bugInformationalAcknowledged
Use of block.timestamp as a source of entropyInformationalFuture Release

8. Findings & Tech Details

8.1 Gas price spikes cause the selected operator to be vulnerable to front-running and be slashed

// High

Description

In the HolographOperatorcontract, 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.

BVSS
Recommendation

Adjust the operator node software to queue transactions immediately with the gas price specified in bridgeInRequestPayload during a gas spike.

Remediation

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

8.2 Centralization risk in Admin.adminCall() function

// Medium

Description

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.

BVSS
Recommendation

Consider removing the adminCall() function from the Admin contract.

Remediation

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.

8.3 HolographFactory.deployHolographableContract() function is vulnerable to crosschain signature replay attacks

// Low

Description

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.

BVSS
Recommendation

Consider including a domain separator in the HolographFactory.deployHolographableContract() function signed hash.

Remediation

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

8.4 HolographOperator.recoverJob() function does not follow the check effects interactions pattern

// Low

Description

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.

BVSS
Recommendation

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");
}

Remediation

SOLVED: The Holograph team solved the issue by implementing the recommended solution.

Remediation Hash
232c78cb9d0d4d656cb4f574da69323ddc12ecaf

8.5 Updating the utility token through the HolographOperator.setUtilityToken() function will break the _bondedAmounts[operator] mapping

// Low

Description

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.


BVSS
Recommendation

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.

Remediation

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

Remediation Hash
eca3fb95663610bdf3d4c22fd26587285fd56ab9

8.6 Initialization functions can be front-run if the contracts are not deployed and initialized atomically

// Low

Description

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.

BVSS
Recommendation

Consider calling the _disableInitializers function in all the contracts' constructor:

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
    _disableInitializers();
}

Remediation

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.

8.7 Incompatibility with non-standard ERC20 tokens

// Low

Description

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.

BVSS
Recommendation

It is recommended to use the OpenZeppelin's safeTransfer() and safeTransferFrom() functions instead of transfer()& transferFrom().

Remediation

SOLVED: The Holograph team solved the issue by implementing the recommended solution.

Remediation Hash
a6e9d0f94bfa77c833f17f9be240a26a003c545f

8.8 Use call instead of transfer to transfer native assets

// Low

Description

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

BVSS
Recommendation

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.

Remediation

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

8.9 Lack of a double step transfer ownership pattern in the Admin contract

// Informational

Description

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.

BVSS
Recommendation

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.

Remediation

ACKNOWLEDGED: The Holograph team acknowledged this finding.

8.10 Direct usage of ecrecover allows for signature malleability

// Informational

Description

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.

Score
Impact:
Likelihood:
Recommendation

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.

Remediation

ACKNOWLEDGED: The Holograph team acknowledged this finding.

8.11 Use of unlicensed smart contracts

// Informational

Description

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.

Score
Impact:
Likelihood:
Recommendation

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:

  1. MIT License: A permissive license that allows for reuse with minimal restrictions.

  2. GNU General Public License (GPL): A copyleft license that ensures derivative works are also open-source.

  3. Apache License 2.0: A permissive license that provides an express grant of patent rights from contributors to users.

Remediation

ACKNOWLEDGED: The Holograph team acknowledged this finding.

8.12 Operator can bribe the miner and steal honest operator's bond amount in all the chains that do not implement the EIP-1559

// Informational

Description

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:

Therefore, a dishonest operator can seize an honest operator's bond even when gas prices are high.

Score
Impact:
Likelihood:
Recommendation

Avoid using current tx.gasprice for previous block gas price inference. It is recommended to use a gas price oracle instead.

Remediation

PENDING: The Holograph team will consider addressing this in a future release.

8.13 Low level calls previous to solidity version 0.8.14 can trigger an optimizer bug

// Informational

Description

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.

Score
Impact:
Likelihood:
Recommendation

Consider updating your contracts to a newer solidity version.

Remediation

ACKNOWLEDGED: The Holograph team acknowledged this finding.

8.14 Use of block.timestamp as a source of entropy

// Informational

Description

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)));
Score
Impact:
Likelihood:
Recommendation

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.

Remediation

PENDING: The Holograph team will consider addressing this in a future release.

9. Automated Testing

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

HolographGenesis #1

HolographGenesisLocal.sol

HolographGenesisLocal #1

Holograph.sol

Holograph #1Holograph #2

HolographRegistry.sol

HolographRegistry #1HolographRegistry #2

HolographFactory.sol

HolographFactory #1HolographFactory #2HolographFactory #3

HolographOperator.sol

HolographOperator #1HolographOperator #2HolographOperator #3HolographOperator #4HolographOperator #5HolographOperator #6

HolographBridge.sol

HolographBridge #1HolographBridge #2HolographBridge #3

HolographTreasury.sol

HolographTreasury #1HolographTreasury #2HolographTreasury #3

HolographInterfaces.sol

HolographInterfaces #1HolographInterfaces #2

LayerZeroModule.sol

LayerZeroModule #1LayerZeroModule #2LayerZeroModule #3
  • 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.

© Halborn 2024. All rights reserved.