Platform: Code4rena
Start Date: 18/10/2022
Pot Size: $75,000 USDC
Total HM: 27
Participants: 144
Period: 7 days
Judge: gzeon
Total Solo HM: 13
Id: 170
League: ETH
Rank: 96/144
Findings: 3
Award: $11.44
π Selected for report: 2
π Solo Findings: 0
π Selected for report: minhtrng
Also found by: Deivitto, V_B, __141345__, adriro, cdahlheimer, d3e4, ladboy233, nadin, teawaterwire
2.5585 USDC - $2.56
Using block.number and block.timestamp as a source of randomness is commonly advised against, as the outcome can be manipulated by calling contracts. In this case a compromised layer-zero-endpoint would be able to retry the selection of the primary operator until the result is favorable to the malicious actor.
An attack path for rerolling the result of bad randomness might look roughly like this:
function attack(uint256 currentNonce, uint256 wantedPodIndex, uint256 numPods, uint256 wantedOperatorIndex, uint256 numOperators, bytes calldata bridgeInRequestPayload) external{ bytes32 jobHash = keccak256(bridgeInRequestPayload); //same calculation as in HolographOperator.crossChainMessage uint256 random = uint256(keccak256(abi.encodePacked(jobHash, currentNonce, block.number, block.timestamp))); require(wantedPodIndex == random % numPods) require(wantedOperatorIndex == random % numOperators); operator.crossChainMessage(bridgeInRequestPayload); }
The attack basically consists of repeatedly calling the attack
function with data that is known and output that is wished for until the results match and only then continuing to calling the operator.
Manual Review
Consider using a decentralized oracle for the generation of random numbers, such as Chainlinks VRF.
It should be noted, that in this case there is a prerequirement of the layer-zero endpoint being compromised, which confines the risk quite a bit, so using a normally unrecommended source of randomness could be acceptable here, considering the tradeoffs of integrating a decentralized oracle.
#0 - gzeoneth
2022-10-30T16:59:32Z
Duplicate of #27
#1 - ACC01ADE
2022-11-09T15:01:10Z
Very valid issue
#2 - gzeoneth
2022-11-19T18:21:25Z
While sponsor noted this is a design choice to use pseudorandomness, as pointed out by the warden a compromised layer-zero-endpoint can exploit this for profit, judging this as Med risk.
8.8837 USDC - $8.88
https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L374-L382 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L849-L857
Bond tokens (HLG) equal to the slash amount will get permanently stuck in the HolographOperator each time a job gets executed by someone who is not an (fallback-)operator.
The HolographOperator.executeJob
function can be executed by anyone after a certain passage of time:
... if (job.operator != address(0)) { ... if (job.operator != msg.sender) { //perform time and gas price check if (timeDifference < 6) { // check msg.sender == correct fallback operator } // slash primary operator uint256 amount = _getBaseBondAmount(pod); _bondedAmounts[job.operator] -= amount; _bondedAmounts[msg.sender] += amount; //determine if primary operator retains his job if (_bondedAmounts[job.operator] >= amount) { ... } else { ... } } } // execute the job
In case if (timeDifference < 6) {
gets skipped, the slashed amount will be assigned to the msg.sender
regardless if that sender is currently an operator or not. The problem lies within the fact that if msg.sender
is not already an operator at the time of executing the job, he cannot become one after, to retrieve the reward he got for slashing the primary operator. This is because the function HolographOperator.bondUtilityToken
requires _bondedAmounts
to be 0 prior to bonding and hence becoming an operator:
require(_bondedOperators[operator] == 0 && _bondedAmounts[operator] == 0, "HOLOGRAPH: operator is bonded");
Manual Review
Assuming that it is intentional that non-operators can execute jobs (which could make sense, so that a user could finish a bridging process on his own, if none of the operators are doing it): remove the requirement that _bondedAmounts
need to be 0 prior to bonding and becoming an operator so that non-operators can get access to the slashing reward by unbonding after.
Alternatively (possibly preferrable), just add a method to withdraw any _bondedAmounts
of non-operators.
#0 - alexanderattar
2022-11-08T06:07:29Z
Known issue that already has been fixed for the next update
π Selected for report: Rolezn
Also found by: 0x1f8b, 0x52, 0x5rings, 0xNazgul, 0xSmartContract, 0xZaharina, 0xhunter, 0xzh, 8olidity, Amithuddar, Aymen0909, B2, Bnke0x0, Chom, Deivitto, Diana, Diraco, Dravee, Franfran, JC, Jeiwan, Josiah, JrNet, Jujic, KingNFT, KoKo, Lambda, Margaret, Migue, Ocean_Sky, PaludoX0, Picodes, Rahoz, RaoulSchaffranek, RaymondFam, RedOneN, ReyAdmirado, Shinchan, Tagir2003, Trust, Waze, Yiko, __141345__, a12jmx, adriro, ajtra, arcoun, aysha, ballx, bin2chen, bobirichman, brgltd, bulej93, catchup, catwhiskeys, caventa, cccz, cdahlheimer, ch0bu, chaduke, chrisdior4, cloudjunky, cryptostellar5, cryptphi, csanuragjain, cylzxje, d3e4, delfin454000, djxploit, durianSausage, erictee, fatherOfBlocks, francoHacker, gianganhnguyen, gogo, hansfriese, i_got_hacked, ignacio, imare, karanctf, kv, leosathya, louhk, lukris02, lyncurion, m_Rassska, malinariy, martin, mcwildy, mics, minhtrng, nicobevi, oyc_109, pashov, peanuts, pedr02b2, peiw, rbserver, ret2basic, rotcivegaf, rvierdiiev, ryshaw, sakman, sakshamguruji, saneryee, securerodd, seyni, sikorico, svskaushik, teawaterwire, tnevler, w0Lfrum
0 USDC - $0.00
The documents which contain the specification and were provided as part of the contest specify on multiple places that the likelihood to being selected as the primary operator for a job should be dependent on the number of tokens bonded:
https://docs.holograph.xyz/holograph-protocol/operator-network-specification#operator-job-selection :
https://github.com/code-423n4/2022-10-holograph/blob/main/README_DETAILS.md#operating
This is currently not the case, as the amount of bonded tokens does not affect the random selection mechanism.
The random selection mechanism can be found in HolographOperator:
uint256 random = uint256(keccak256(abi.encodePacked(jobHash, _jobNonce(), block.number, block.timestamp))); /** * @dev divide by total number of pods, use modulus/remainder */ uint256 pod = random % _operatorPods.length; /** * @dev identify the total number of available operators in pod */ uint256 podSize = _operatorPods[pod].length; /** * @dev select a primary operator */ uint256 operatorIndex = random % podSize;
Pods are essentially just selected by an equal distribution (and so are the operators within the pods). For a simple example of how this does not check out according to the specification, look at the following example:
A higher pod number requires a higher bonding amount (details are in the docs and HolographOperator.getPotBondAmounts
), so the operators within pod #3 should have a higher chance of being selected than the ones in the other pods. With this implementation they actually have a lower chance (~16.67%) of being selected than the operators in pod#1 and pod#2 (both ~33.33%)
It is presumed that the project has chosen this design to rely on rational market forces to adjust the distribution of operators over the pods.
If all operators were technically savvy, they would read the code, understand the selection mechanism and would adjust the pod they are in accordingly. This however can not be assumed and regular users would just try to get into the highest pod possible with the amount of bonding tokens they own, which can lead to operators in pods with lower bonding requirements actually having a higher chance of being selected due to higher pods being more "crowded" (as in the example above).
Manual Review
Change the random selection mechanism to weight pods differently, depending on how many tokens are bonded for each particular pod, for example weight = minimum bonding requirement * amount of operators in the pod
.
#0 - Minh-Trng
2022-10-25T21:02:38Z
i would like to add that the report looks a bit weird on the example due to the way how github links combination of hash+number to a github issue so pod #1
turns to what you see above
#1 - gzeoneth
2022-10-28T08:32:46Z
i would like to add that the report looks a bit weird on the example due to the way how github links combination of hash+number to a github issue so
pod #1
turns to what you see above
edited to better format
#2 - alexanderattar
2022-11-08T05:49:37Z
This is a valid point, but also part of the game. The suggestion will be considered, but we might stick with this design.
#3 - gzeoneth
2022-11-19T19:42:02Z
duplicate of #434
#4 - gzeoneth
2022-11-21T07:25:36Z
As QA report