GithubHelp home page GithubHelp logo

2022-10-holograph-findings's Introduction

Holograph Protocol Contest

Unless otherwise discussed, this repo will be made public after contest completion, sponsor review, judging, and two-week issue mitigation window.

Contributors to this repo: prior to report publication, please review the Agreements & Disclosures issue.


Contest findings are submitted to this repo

As a sponsor, you have four critical tasks in the contest process:

  1. Weigh in on severity.
  2. Respond to issues.
  3. Share your mitigation of findings.

Let's walk through each of these.

High and Medium Risk Issues

Please note: because wardens submit issues without seeing each other's submissions, there will always be findings that are duplicates. For all issues labeled 3 (High Risk) or 2 (Medium Risk), these have been pre-sorted for you so that there is only one primary issue open per unique finding. All duplicates have been labeled duplicate, linked to a primary issue, and closed.

Weigh in on severity

Judges have the ultimate discretion in determining severity of issues, as well as whether/how issues are considered duplicates. However, sponsor input is a significant criteria.

For a detailed breakdown of severity criteria and how to estimate risk, please refer to the judging criteria in our documentation.

If you disagree with a finding's severity, leave the severity label intact and add the label disagree with severity, along with a comment indicating your opinion for the judges to review. It is possible for issues to be considered 0 (Non-critical).

Feel free to use the question label for anything you would like additional C4 input on.

Please don't change the severity labels; that's up to the judge's discretion.

Respond to issues

Label each open/primary High or Medium risk finding as one of these:

  • sponsor confirmed, meaning: "Yes, this is a problem and we intend to fix it."
  • sponsor disputed, meaning either: "We cannot duplicate this issue" or "We disagree that this is an issue at all."
  • sponsor acknowledged, meaning: "Yes, technically the issue is correct, but we are not going to resolve it for xyz reasons."

(Note: please don't use sponsor disputed for a finding if you think it should be considered of lower or higher severity. Instead, use the label disagree with severity and add comments to recommend a different severity level -- and include your reasoning.)

Add any necessary comments explaining your rationale for your evaluation of the issue. Note that when the repo is public, after all issues are mitigated, wardens will read these comments.

QA and Gas Reports

For low and non-critical findings (AKA QA), as well as gas optimizations: all warden submissions in these three categories should now be submitted as bulk listings of issues and recommendations:

  • QA reports should include all low severity and non-critical findings, along with a summary statement.
  • Gas reports should include all gas optimization recommendations, along with a summary statement.

For QA and Gas reports, we ask that you:

  • Leave a comment for the judge on any reports you consider to be particularly high quality. (These reports will be awarded on a curve.)
  • Add the sponsor disputed label to any reports that you think should be completely disregarded by the judge, i.e. the report contains no valid findings at all.

Once labelling is complete

When you have finished labelling findings, drop the C4 team a note in your private Discord backroom channel and let us know you've completed the sponsor review process. At this point, we will pass the repo over to the judge to review your feedback while you work on mitigations.

Share your mitigation of findings

Note: this section does not need to be completed in order to finalize judging. You can continue work on mitigations while the judge finalizes their decisions and even beyond that. Ultimately we won't publish the final audit report until you give us the ok.

For each finding you have confirmed, you will want to mitigate the issue before the contest report is made public.

As you undertake that process, we request that you take the following steps:

  1. Within your own GitHub repo, create a pull request for each finding.
  2. Link the PR to the issue that it resolves within your contest findings repo.

This will allow for complete transparency in showing the work of mitigating the issues found in the contest. Do not close the issue; simply label it as resolved. If the issue in question has duplicates, please link to your PR from the open/primary issue.

2022-10-holograph-findings's People

Contributors

code423n4 avatar itsmetechjay avatar c4-staff avatar

Stargazers

vincent avatar

Watchers

angello pozo avatar Alexander avatar Ashok avatar  avatar

2022-10-holograph-findings's Issues

transferFrom() method is used instead of safeTransferFrom()

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/src/HolographOperator.sol#L740
https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/src/HolographOperator.sol#L790

Vulnerability details

Vulnerability details

Details & Impact
The transferFrom() method is used instead of safeTransferFrom(), presumably to save gas. I however argue that this isn’t recommended because:

1)OpenZeppelin’s documentation discourages the use of transferFrom(), use safeTransferFrom() whenever possible
2)Given that any NFT can be used for the call option, there are a few NFTs (here’s an example) that have logic in the onERC721Received() function, which is only triggered in the safeTransferFrom() function and not in transferFrom()

Recommended Mitigation Steps
Call the safeTransferFrom() method instead of transferFrom()

File: 2022-10-holograph\src\HolographOperator.sol
740,29: require(_utilityToken().transferFrom(msg.sender, address(this), amount), "HOLOGRAPH: token transfer failed");
790,31: require(_utilityToken().transferFrom(msg.sender, address(this), amount), "HOLOGRAPH: token transfer failed");

Use call instead of transfer

Lines of code

https://github.com/holographxyz/holograph-protocol/blob/c4_audit/contracts/enforcer/PA1D.sol#L396

Vulnerability details

Impact

The transfer function is not recommended for sending ETH due to its 2300 gas unit limit. Instead call can be used to circumvent the gas limit

Proof of Concept

  1. Observe the _payoutEth function
  function _payoutEth() private {
...

    // uint256 sent;
    for (uint256 i = 0; i < length; i++) {
      sending = ((bps[i] * balance) / 10000);
      addresses[i].transfer(sending);
      // sent = sent + sending;
    }
  }
  1. Observe that contract is using transfer function for sending eth to addresses[i] where addresses[i] might be a contract requiring more than 2300 gas unit in which case this function will fail

Recommended Mitigation Steps

Use call instead of transfer

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

External Call To User-Supplied Address

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/abstract/Admin.sol#L133

Vulnerability details

Impact

A call to a user-supplied address is executed.
An external message call to an address specified by the caller is executed. Note that the callee account might contain arbitrary code and could re-enter any function within this contract. Reentering the contract in an intermediate state may lead to unexpected behaviour. Make sure that no state modifications are executed after this call and/or reentrancy guards are in place.

Proof of Concept

  1. https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/abstract/Admin.sol#L133

  2. Contract: LayerZeroModule
    Function name: adminCall(address,bytes)
    PC address: 3982
    Estimated Gas Usage: 2426 - 38825

  3. In file: Admin.sol:136

call(gas(), target, callvalue(), 0, data.length, 0, 0)

Initial State:

Account: [CREATOR], balance: 0x345c3, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

Transaction Sequence:

Caller: [CREATOR], calldata: , value: 0x0
Caller: [SOMEGUY], function: init(bytes), txdata: 0x4ddf47d4000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, value: 0x0
Caller: [SOMEGUY], function: adminCall(address,bytes), txdata: 0xbf64a82d000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000, value: 0x0

Tools Used

Manual/code reading

Recommended Mitigation Steps

The best practices to avoid Reentrancy weaknesses are:

Make sure all internal state changes are performed before the call is executed. This is known as the Checks-Effects-Interactions pattern
Use a reentrancy lock (ie. OpenZeppelin's ReentrancyGuard.

Incorrect signer is validated

Lines of code

https://github.com/holographxyz/holograph-protocol/blob/c4_audit/contracts/HolographFactory.sol#L333

Vulnerability details

Impact

The ecrecover function will return 0 address in case of incorrect signature. Since the contract is not handling this case, thus if _verifySigner is called with signer as 0 address then signature will be deemed verified

Proof of Concept

  1. Observe the _verifySigner function
function _verifySigner(
    bytes32 r,
    bytes32 s,
    uint8 v,
    bytes32 hash,
    address signer
  ) private pure returns (bool) {
    if (v < 27) {
      v += 27;
    }
    /**
     * @dev signature is checked against EIP-191 first, then directly, to support legacy wallets
     */
    return (ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)), v, r, s) == signer ||
      ecrecover(hash, v, r, s) == signer);
  }
  1. Observe that if ecrecover returns 0 address due to incorrect signature and signer is also set as 0 address then signature will be deemed valid
    return (ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)), v, r, s) == signer ||
      ecrecover(hash, v, r, s) == signer);

Recommended Mitigation Steps

Add a new condition preventing invalid address

require(signer!=address(0), "Invalid signer");
return (ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)), v, r, s) == signer ||
      ecrecover(hash, v, r, s) == signer);

QA Report

See the markdown file with the details of this report here.

No approval required for operators

Lines of code

https://github.com/holographxyz/holograph-protocol/blob/c4_audit/contracts/enforcer/HolographERC20.sol#L572
https://github.com/holographxyz/holograph-protocol/blob/c4_audit/contracts/enforcer/HolographERC20.sol#L520

Vulnerability details

Impact

It seems that operator is given a free hand in safeTransferFrom by skipping the approval check. This could lead to issue Since holograph contract can upgrade operator anytime using setOperator at Holograph.sol#L308. The new malicious operator can wipe all user balance

Proof of Concept

  1. Observe the safeTransferFrom function
 function safeTransferFrom(
    address account,
    address recipient,
    uint256 amount,
    bytes memory data
  ) public returns (bool) {
    if (account != msg.sender) {
      if (msg.sender != _holograph().getBridge() && msg.sender != _holograph().getOperator()) {
        uint256 currentAllowance = _allowances[account][msg.sender];
        require(currentAllowance >= amount, "ERC20: amount exceeds allowance");
        unchecked {
          _allowances[account][msg.sender] = currentAllowance - amount;
        }
      }
    }
...
}
  1. Observe that if msg.sender is _holograph().getOperator() then approval checks are not done

Recommended Mitigation Steps

Let user approve there tokens to the operator

QA Report

See the markdown file with the details of this report here.

Dependence on tx.origin

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographBridge.sol#L342

Vulnerability details

Impact

Use of tx.origin as a part of authorization control.
The tx.origin environment variable has been found to influence a control flow decision. Note that using tx.origin as a security control might cause a situation where a user inadvertently authorizes a smart contract to perform an action on their behalf. It is recommended to use msg.sender instead.

Proof of Concept

  1. https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographBridge.sol#L342

  2. Contract: HolographBridge
    Function name: getBridgeOutRequestPayload(uint32,address,uint256,uint256,bytes)
    PC address: 10458
    Estimated Gas Usage: 5192 - 77380

  3. In file: #utility.yul:864

if gt(offset, 0xffffffffffffffff) { revert_error_c1322bf8034eace5e0b5c7295db60986aa89aae5e0ea0873e4689e076861a5db() }

Initial State:

Account: [CREATOR], balance: 0xa1489, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

Transaction Sequence:

Caller: [CREATOR], calldata: , value: 0x0
Caller: [SOMEGUY], function: getBridgeOutRequestPayload(uint32,address,uint256,uint256,bytes), txdata: 0x636ee68b000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000101010101010101010101010101010200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, value: 0x0

Tools Used

Manual/code reading

Recommended Mitigation Steps

tx.origin should not be used for authorization. Use msg.sender instead.

call() should be used instead of transfer() on an address payable

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/src/HolographOperator.sol#L497

Vulnerability details

Vulnerability details

call() should be used instead of transfer() on an address payable

This is a classic Code4rena issue:

code-423n4/2021-04-meebits-findings#2
code-423n4/2021-10-tally-findings#20
code-423n4/2022-01-openleverage-findings#75

Impact

The use of the deprecated transfer() function for an address will inevitably make the transaction fail when:

The claimer smart contract does not implement a payable function.
The claimer smart contract does implement a payable fallback which uses more than 2300 gas unit.
The claimer smart contract implements a payable fallback function that needs less than 2300 gas units but is called through proxy, raising the call’s gas usage above 2300.

Additionally, using higher than 2300 gas might be mandatory for some multisig wallets.

Impacted lines:

File: 2022-10-holograph\src\HolographOperator.sol
497,5: payable(hToken).transfer(hlgFee);

Recommended Mitigation

I recommend using call() instead of transfer().

ERC20 transactions with no return vaule will fail to transfer

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/token/hToken.sol#L224

Vulnerability details

Impact

Although the ERC20 standard suggests that a transfer should return true on success, many tokens are non-compliant in this regard. In that case, the .transfer() call here will revert even if the transfer is successful, because solidity will check that the RETURNDATASIZE matches the ERC20 interface

Proof of Concept

 ERC20(token).transfer(to, amount);

Tools Used

Manual review

Recommended Mitigation Steps

Consider using OpenZeppelin’s SafeERC20

HolographOperator._getCurrentBondAmount() performs a multiplication on the result of a division which may cause loss of precision

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L1177

Vulnerability details

Impact

Solidity integer division might truncate. As a result, performing multiplication before division can sometimes avoid loss of precision.

Proof of Concept

HolographOperator._getCurrentBondAmount(uint256) (HolographOperator.sol#1167-1180) performs a multiplication on the result of a division:
-current += (current / _operatorThresholdDivisor) * (position / _operatorThresholdStep) (HolographOperator.sol#1177)

If _operatorThresholdDivisor is greater than current or _operatorThresholdStep is greater than position, current will add a value of 0.

Tools Used

Slither

Recommended Mitigation Steps

Consider ordering multiplication before division. In general, it's usually a good idea to re-arrange arithmetic to perform multiplication before division, unless the limit of a smaller type makes this dangerous.

enforcer/Holographer.sol uses delegatecall to a input-controlled function id

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/Holographer.sol#L147-L169

Vulnerability details

Impact

Delegatecall or callcode to an address controlled by the user allows a user to delegate the execution to their malicious contract, as a result a user could withdraw funds of the contract and destruct it.

Proof of Concept

Holographer.init(bytes) (enforcer/Holographer.sol#147-169) uses delegatecall to a input-controlled function id
- (success,returnData) = HolographRegistryInterface(HolographInterface(holograph).getRegistry()).getReservedContractTypeAddress(contractType).delegatecall(abi.encodeWithSignature(init(bytes),initCode)) (enforcer/Holographer.sol#162-164)

Tools Used

Slither

Recommended Mitigation Steps

Avoid using delegatecall. Use only trusted destinations.

QA Report

See the markdown file with the details of this report here.

Contract HolographFactory lacks withdraw functions

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographFactory.sol#L123-L350

Vulnerability details

Impact

Contract HolographFactory (HolographFactory.sol#123-350) has payable functions (receive(), fallback(), etc.), but does not have a function to withdraw, therefore, every Ether sent to HolographFactory will be lost.

Proof of Concept

Contract functions and structure illustrate the concept.

Tools Used

Slither

Recommended Mitigation Steps

Remove the payable attribute or add a withdraw function.

Dependence on tx.origin

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L849

Vulnerability details

Impact

Use of tx.origin as a part of authorization control.
The tx.origin environment variable has been found to influence a control flow decision. Note that using tx.origin as a security control might cause a situation where a user inadvertently authorizes a smart contract to perform an action on their behalf. It is recommended to use msg.sender instead.

Proof of Concept

  1. https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L849

  2. Contract: HolographOperator
    Function name: bondUtilityToken(address,uint256,uint256)
    PC address: 11539
    Estimated Gas Usage: 77171 - 335565

  3. In file: HolographOperator.sol:889

require(_utilityToken().transferFrom(msg.sender, address(this), amount), "HOLOGRAPH: token transfer failed")

Initial State:

Account: [CREATOR], balance: 0x40800000000000045, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

Transaction Sequence:

Caller: [CREATOR], calldata: , value: 0x0
Caller: [SOMEGUY], function: bondUtilityToken(address,uint256,uint256), txdata: 0x8b432e4e000000000000000000000000000000000000000000000000000000000000000001010101010101010101010001010101010101010101010101010101010101010000000000000000000000000000000000000000000000000000000000000001, value: 0x0

Tools Used

Manual/code reading

Recommended Mitigation Steps

tx.origin should not be used for authorization. Use msg.sender instead.

`transfer` is used instead of `call`

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/PA1D.sol#L396

Vulnerability details

Impact

transfer uses a fixed amount of gas, which can result in revert if the recipient is a contract. For instance when:

  • The contract does not implement a payable fallback function.
  • The contract implements a payable fallback function which uses more than 2300 gas units.
  • The contract implements a payable fallback function which needs less than 2300 gas units but is called through a proxy that raises the call’s gas usage above 2300.

The PA1D._payoutEth function use transfer instead of call to distribute royalties. Which could lead to DoS for all royalties recipients since only one misbehaving address is needed for the entire function call to revert.

Proof of Concept

In PA1D._payoutEth royalties are distributed to the recipients addresses set by the owner in addresses using transfer. If only one of these addresses is a contract which for the any reason need more than 2300 gas to execute the transfer or doesn't implement a receive function, the call to PA1D._payoutEth will revert and no one will be able to receive his royalties.

Link : https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/PA1D.sol#L396

            addresses[i].transfer(sending);

Blog post about this from consensys diligence (2019): https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/

Tools Used

Manual review.

Recommended Mitigation Steps

call should be used instead of transfer, while making sure to follow the "Checks-Effects-Interactions" pattern to prevent potential reentrancy. A reentrancy lock could also be used as additionnal safety, but would increase the gas usage.

Change this :

            addresses[i].transfer(sending);

to this :

      (bool success, ) = addresses[i].call{value: sending}("");
      require(success, "Transfer Failed");

Reducing randomness can force open season

Lines of code

https://github.com/holographxyz/holograph-protocol/blob/c4_audit/contracts/HolographOperator.sol#L849

Vulnerability details

Impact

User can call bondUtilityToken function with large pod value which ensure pushing [address(0)] from _operatorPods.length till pod.

This becomes a problem _operatorPods.length increases dramatically causing randomess to reduce significantely in crossChainMessage. Reason being random % _operatorPods.length will mostly result in unused pod with address(0) as only operator, causing open season where anyone can execute the job

Proof of Concept

  1. User A calls the bondUtilityToken function with pod say 100
  function bondUtilityToken(
    address operator,
    uint256 amount,
    uint256 pod
  ) external {
...
for (uint256 i = _operatorPods.length; i <= pod; i++) {
          /**
           * @dev add zero address into pod to mitigate empty pod issues
           */
          _operatorPods.push([address(0)]);
        }
...
}
  1. Lets say _operatorPods initially had 2 elements so after calling this function and pod as 100 this will push address(0) 98 times making the _operatorPods length as 100

  2. This becomes a problem while calling crossChainMessage function

function crossChainMessage(bytes calldata bridgeInRequestPayload) external payable {
...
uint256 pod = random % _operatorPods.length;
uint256 podSize = _operatorPods[pod].length;
 uint256 operatorIndex = random % podSize;
...
}
  1. Assume random is selected as 10, which makes pod as 10 and _operatorPods[pod].length is 1 (while initializing one dummy operator with address 0 is added). This makes operatorIndex as 0 (random%1=0)

  2. Now 0 operatorIndex means open season and anyone can execute the job

/**
       * @dev If operator index is 0, then it's open season! Anyone can execute this job. First come first serve
       *      pop operator to ensure that they cannot be selected for any other job until this one completes
       *      decrease pod size to accomodate popped operator
       */

Recommended Mitigation Steps

Do not allow user to specify a very large value for pods

executeJob will always revert if the original operator is gone offline and there aren't enough bondedAmounts for the original operator

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L341-L403

Vulnerability details

Impact

executeJob will always revert if the original operator is gone offline and there aren't enough bondedAmounts for the original operator. To be continue...

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Tools Used

Recommended Mitigation Steps

Gas price spikes cause the selected operator to be vulnerable to frontrunning and be slashed

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L354

Vulnerability details

Impact

Gas price spikes cause the selected operator to be vulnerable to frontrunning and be slashed.

Proof of Concept

require(gasPrice >= tx.gasprice, "HOLOGRAPH: gas spike detected");
        /**
         * @dev select operator that failed to do the job, is slashed the pod base fee
         */
        _bondedAmounts[job.operator] -= amount;
        /**
         * @dev the slashed amount is sent to current operator
         */
        _bondedAmounts[msg.sender] += amount;

Since you have designed a mechanism to prevent other operators to slash the operator due to "the selected missed the time slot due to a gas spike". It can induce that operators won't perform their job if a gas price spike happens due to negative profit.

But your designed mechanism has a vulnerability. Other operators can submit their transaction to the mempool and queue it using gasPrice in bridgeInRequestPayload. It may get executed before the selected operator as the selected operator is waiting for the gas price to drop but doesn't submit any transaction yet. If it doesn't, these operators lose a little gas fee. But a slashed reward may be greater than the risk of losing a little gas fee.

require(timeDifference > 0, "HOLOGRAPH: operator has time");

Once 1 epoch has passed, selected operator is vulnerable to slashing and frontrunning.

Recommended Mitigation Steps

Modify your operator node software to queue transactions immediately with gasPrice in bridgeInRequestPayload if a gas price spike happened. Or allow gas fee loss tradeoff to prevent being slashed.

executeJob does not validate msg.sender against bonded operators of that pod on timeDifference >= 6

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L301-L439
https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L849-L891

Vulnerability details

Impact

executeJob does not validate msg.sender against bonded operators of that pod on timeDifference >= 6. Unbonded operators can perform executeJob, slash selected operator, and receive the reward (if implemented) as well. This is unexpected since your document clearly indicates that "To become an operator, you must view the pods available to join, select a pod, and bond at least the minimum bond requirement". Unbonded operators shouldn't execute jobs and have fee rewards without bonding.

Proof of Concept

Joining Pods

To become an operator, you must view the pods available to join, select a pod, and bond at least the minimum bond requirement. ... At step 3, you are now able to call the bondUtilityToken function with the pod and amounts you want to use to enter the pod. Please note, there is a minimum bond requirement to join but no maximum.

  function bondUtilityToken(
    address operator,
    uint256 amount,
    uint256 pod
  ) external {
    ...
      _operatorPods[pod - 1].push(operator);
      _operatorPodIndex[operator] = _operatorPods[pod - 1].length - 1;
      _bondedOperators[operator] = pod;
      _bondedAmounts[operator] = amount;
      /**
       * @dev transfer tokens last, to prevent reentrancy attacks
       */
      require(_utilityToken().transferFrom(msg.sender, address(this), amount), "HOLOGRAPH: token transfer failed");
    }
  }

_bondedOperators[operator] will be set to bonded pod

Processing Jobs

You must join a pod to become an Operator

...

At step 5, the wallet sends a transaction to the executeJob method on the HolographOperator.sol contract. In here, further checks are done to validate the job and user's wallet. After this transaction is mined on the blockchain, the NFT will become finalized and available on the new blockchain.

function executeJob(bytes calldata bridgeInRequestPayload) external payable {
...
if (job.operator != msg.sender)
...
        if (timeDifference < 6) {
          uint256 podIndex = uint256(job.fallbackOperators[timeDifference - 1]);
          /**
           * @dev do a quick sanity check to make sure operator did not leave from index or is a zero address
           */
          if (podIndex > 0 && podIndex < _operatorPods[pod].length) {
            address fallbackOperator = _operatorPods[pod][podIndex];
            /**
             * @dev ensure that sender is currently valid backup operator
             */
            require(fallbackOperator == msg.sender, "HOLOGRAPH: invalid fallback");
          }
        }
...
    /**
     * @dev execute the job
     */
    try
      HolographOperatorInterface(address(this)).nonRevertingBridgeCall{value: msg.value}(
        msg.sender,
        bridgeInRequestPayload
      )
    {
      /// @dev do nothing
    } catch {
      _failedJobs[hash] = true;
      emit FailedOperatorJob(hash);
    }
    /**
     * @dev every executed job (even if failed) increments total message counter by one
     */
    ++_inboundMessageCounter;
    /**
     * @dev reward operator (with HLG) for executing the job
     * @dev this is out of scope and is purposefully omitted from code
     */
    ////  _bondedOperators[msg.sender] += reward;
}

I haven't seen anywhere checking msg.sender for operator that executed on timeDifference >= 6 is bonding to job.pod (_bondedOperators[msg.sender] == job.pod)

But the document clearly indicates that You must join a pod to become an Operator and an operator is one that should call executeJob

Tools Used

Manual review

Recommended Mitigation Steps

Check that msg.sender is bonded to job.pod by checking

require(_bondedOperators[msg.sender] == job.pod);

Return value of transfer() function is ignored

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L400

Vulnerability details

Impact

In that function executeJob() it has a feature that transfer leftover utility tokens to another address, but the return value of that token.transfer() is not cheked, there no such validation check of return value.
That may help to receive token multiple times.

Proof of Concept

Tools Used

Manual review

Recommended Mitigation Steps

There should be a require condition check on return value of transfer()

Uncheck return value by use ERC20 transfer

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L400

Vulnerability details

Impact

HolographOperator.executeJob
If leftover amount is greater than 0 so contract will return remaining tokens
When call transfer it can be fail but not being tracking.

Proof of Concept

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L400

Recommended Mitigation Steps

Should use SafeERC20.safetransfer or ensure that the transfer return value is checked.

M01: USE SAFETRANSFERFROM INSTEAD OF TRANSFERFROM

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L400
https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L596
https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L932
https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/enforcer/PA1D.sol#L396
https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/enforcer/PA1D.sol#L416
https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/enforcer/PA1D.sol#L439

Vulnerability details

Medium Risk Findings

M01: USE SAFETRANSFERFROM INSTEAD OF TRANSFERFROM

problem

The transferFrom() method is used instead of safeTransferFrom(), presumably to save gas. I however argue that this isn’t recommended because:

OpenZeppelin’s documentation discourages the use of transferFrom(), use safeTransferFrom() whenever possible.

prof

HolographOperator.sol, 400, b' _utilityToken().transfer(job.operator, leftovers);'
HolographOperator.sol, 596, b' payable(hToken).transfer(hlgFee);'
HolographOperator.sol, 932, b' require(_utilityToken().transfer(recipient, amount), "HOLOGRAPH: token transfer failed");'
enforcer/PA1D.sol, 396, b' addresses[i].transfer(sending);'
enforcer/PA1D.sol, 416, b' require(erc20.transfer(addresses[i], sending), "PA1D: Couldn't transfer token");'
enforcer/PA1D.sol, 439, b' require(erc20.transfer(addresses[i], sending), "PA1D: Couldn't transfer token");'

Agreements & Disclosures

Agreements

If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:

To signal your agreement to these terms, add a 👍 emoji to this issue.

Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.

Disclosures

Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.

To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:

  1. any sponsor staff or sponsor contractors who are also participating as wardens
  2. any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
  3. any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
  4. any other case where someone might reasonably infer a possible conflict of interest.

HolographOperator.crossChainMessage() has a weak PRNG

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L484-L539
https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L1185-L1193

Vulnerability details

Impact

Weak PRNG due to a modulo on block.timestamp, now or blockhash. These can be influenced by miners to some extent so they should be avoided.

Exploit Scenario

Example exploit scenario. Eve is a miner. Eve calls guessing and re-orders the block containing the transaction. As a result, Eve wins the game.

Proof of Concept

HolographOperator.crossChainMessage(bytes) (HolographOperator.sol#484-539) uses a weak PRNG: "operatorIndex = random % podSize (HolographOperator.sol#511)"
HolographOperator.crossChainMessage(bytes) (HolographOperator.sol#484-539) uses a weak PRNG: "pod = random % _operatorPods.length (HolographOperator.sol#503)"
HolographOperator._randomBlockHash(uint256,uint256,uint256) (HolographOperator.sol#1185-1193) uses a weak PRNG: "(random + uint256(blockhash(uint256)(block.number - n))) % podSize (HolographOperator.sol#1191)"
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#weak-PRNG

Tools Used

Slither

Recommended Mitigation Steps

Do not use block.timestamp, now or blockhash as a source of randomness

HolographOperator.executeJob() ignores return value

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L400

Vulnerability details

Impact

The return value of an external transfer/transferFrom call is not checked. Several tokens do not revert in case of failure and return false. If one of these tokens is used in the contract, deposit will not revert if the transfer fails, and an attacker can call deposit for free.

Proof of Concept

HolographOperator.executeJob(bytes) (HolographOperator.sol#301-439) ignores return value by _utilityToken().transfer(job.operator,leftovers) (HolographOperator.sol#400)

Tools Used

Slither

Recommended Mitigation Steps

Use SafeERC20, or ensure that the transfer/transferFrom return value is checked.

Dependence on tx.origin-

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographBridge.sol#L245

Vulnerability details

Impact

Use of tx.origin as a part of authorization control.
The tx.origin environment variable has been found to influence a control flow decision. Note that using tx.origin as a security control might cause a situation where a user inadvertently authorizes a smart contract to perform an action on their behalf. It is recommended to use msg.sender instead.

Proof of Concept

  1. https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographBridge.sol#L245

  2. Contract: HolographBridge
    Function name: bridgeOutRequest(uint32,address,uint256,uint256,bytes)
    PC address: 9954
    Estimated Gas Usage: 5128 - 77314

  3. In file: #utility.yul:772

if gt(offset, 0xffffffffffffffff) { revert_error_c1322bf8034eace5e0b5c7295db60986aa89aae5e0ea0873e4689e076861a5db() }

Initial State:

Account: [CREATOR], balance: 0x1000000, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

Transaction Sequence:

Caller: [CREATOR], calldata: , value: 0x0
Caller: [CREATOR], function: bridgeOutRequest(uint32,address,uint256,uint256,bytes), txdata: 0xe558566600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050, value: 0x0

Tools Used

Manual/code reading

Recommended Mitigation Steps

tx.origin should not be used for authorization. Use msg.sender instead.

QA Report

See the markdown file with the details of this report here.

Incorrect condition could increase free season chances

Lines of code

https://github.com/holographxyz/holograph-protocol/blob/c4_audit/contracts/HolographOperator.sol#L871

Vulnerability details

Impact

Due to an incorrect condition, the randomness in choosing operator for job execution could be reduced a little bit as shown in poc

Proof of Concept

  1. Assume currently _operatorPods.length is 1

  2. User A calls the bondUtilityToken function with pod value as 2

function bondUtilityToken(
    address operator,
    uint256 amount,
    uint256 pod
  ) external {

...
if (_operatorPods.length < pod) {
        /**
         * @dev activate pod(s) up until the selected pod
         */
        for (uint256 i = _operatorPods.length; i <= pod; i++) {
          /**
           * @dev add zero address into pod to mitigate empty pod issues
           */
          _operatorPods.push([address(0)]);
        }
      }
...

}
  1. This starts a loop from _operatorPods.length which is 1 till pod which is 2 so the loop run twice. This means _operatorPods.push([address(0)]); will be run twice, thus increasing the _operatorPods length to 1+2=3

  2. After this _operatorPods is filled at index pod-1=2-1=1

_operatorPods[pod - 1].push(operator);
      _operatorPodIndex[operator] = _operatorPods[pod - 1].length - 1;
  1. Now index 2 is untouched and this becomes a problem while selecting the operator index for job execution at crossChainMessage function

  2. Since length of _operatorPods becomes 3 where last index does not contain anything (execute default 0 address operator) and random pod is selected from _operatorPods.length, thus there is a high chance of selecting the last index 2. If last index 2 is selected (having only 0 address operator) then its free season ie no operator restriction

uint256 pod = random % _operatorPods.length;

Recommended Mitigation Steps

Revise the loop as shown below

for (uint256 i = _operatorPods.length; i < pod; i++)

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

For Sending ETH HolographOperator.sol contract's using transfer() which may cause ETH to get stuck

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L596

Vulnerability details

Impact

In file HolographOperator.so, contract uses transfer() to send eth from contract to EOA/address due which eth can get stuck.

Proof of Concept

The Istanbul hardfork increases the gas cost of the SLOAD operation and therefore breaks some existing smart contracts.
The reason behind this is that, after the Istanbul hardfork, any smart contract that uses transfer() or send() is taking a hard dependency on gas costs by forwarding a fixed amount of gas (2300). This forwards 2300 gas, which may not be enough if the recipient is a contract and the cost of gas changes.

Tools Used

Manual Review

Recommended Mitigation Steps

Recommend using call() to send eth.

User can lose funds

Lines of code

https://github.com/holographxyz/holograph-protocol/blob/c4_audit/contracts/HolographOperator.sol#L382

Vulnerability details

Impact

In case if a non operator calls the executeJob, then the slashed amount added to this non operator cannot be redeemed as shown in poc causing loss of funds

Proof of Concept

  1. Operator A forgets to execute job

  2. User A sees an opportunity and immediately calls the executeJob function

  3. This slashes the _getBaseBondAmount from Operator A and add this to User A

uint256 amount = _getBaseBondAmount(pod);
        /**
         * @dev select operator that failed to do the job, is slashed the pod base fee
         */
        _bondedAmounts[job.operator] -= amount;
        /**
         * @dev the slashed amount is sent to current operator
         */
        _bondedAmounts[msg.sender] += amount;
  1. Now User A has _bondedAmounts[User A]=amount but _bondedOperators[User A] remain 0 since User A is not an operator

  2. This becomes a problem since now User cannot redeem the funds using unbondUtilityToken function since below condition will fail

require(_bondedOperators[operator] != 0, "HOLOGRAPH: operator not bonded");

Recommended Mitigation Steps

Add a check to verify that caller is a valid operator

require(_bondedOperators[msg.sender] != 0, "HOLOGRAPH: operator not bonded");

External Call To User-Supplied Address

Lines of code

https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographBridge.sol#L296

Vulnerability details

Impact

A call to a user-supplied address is executed.
An external message call to an address specified by the caller is executed. Note that the callee account might contain arbitrary code and could re-enter any function within this contract. Reentering the contract in an intermediate state may lead to unexpected behaviour. Make sure that no state modifications are executed after this call and/or reentrancy guards are in place.

Proof of Concept

  1. https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographBridge.sol#L296

  2. Function name: revertedBridgeOutRequest(address,uint32,address,bytes)
    PC address: 2698
    Estimated Gas Usage: 3355 - 41733

  3. In file: HolographBridge.sol:305

Holographable(holographableContract).bridgeOut(toChain, sender, bridgeOutPayload)

Initial State:

Account: [CREATOR], balance: 0x21733, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

Transaction Sequence:

Caller: [CREATOR], calldata: , value: 0x0
Caller: [ATTACKER], function: revertedBridgeOutRequest(address,uint32,address,bytes), txdata: 0x565ff49e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef0000000000000000000000000000000000000000000000000000000000000021, value: 0x0

Tools Used

Manual/code reading

Recommended Mitigation Steps

The best practices to avoid Reentrancy weaknesses are:

Make sure all internal state changes are performed before the call is executed. This is known as the Checks-Effects-Interactions pattern
Use a reentrancy lock (ie. OpenZeppelin's ReentrancyGuard.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.