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: 30/144
Findings: 2
Award: $277.92
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
55.6726 USDC - $55.67
.send()
is not recommended to use due to the limits of 2300 gas per callinit()
could be front-runnedapprove()
could be front-runned_isContract()
is not safe to use and could be bypassed in some casesERC721.transferFrom()
doesn't check for the case, where the receiver is a smart-contract without ERC721.onCheckedReceived()
method.send()
is not recommended to use due to the limits of 2300 gas per call<a name="L-01"></a>Contracts:
file: src/HolographOperator.sol ..................................... // Lines: [541-548] messagingModule.send{value: msg.value - hlgFee}( gasLimit, gasPrice, toChain, msgSender, msg.value - hlgFee, encodedData );
.call{}()
.init()
could be front-runned<a name="L-02"></a>init()
, but it could be front-run by mev bots or anyone to initialize the bridge. This will lead to redeploying contracts.Contracts:
file: src/HolographBridge.sol ..................................... // Lines: [63-78] function init(bytes memory initPayload) external override returns (bytes4) { require(!_isInitialized(), "HOLOGRAPH: already initialized"); (address factory, address holograph, address operator, address registry) = abi.decode( initPayload, (address, address, address, address) ); assembly { sstore(_adminSlot, origin()) sstore(_factorySlot, factory) sstore(_holographSlot, holograph) sstore(_operatorSlot, operator) sstore(_registrySlot, registry) } _setInitialized(); return InitializableInterface.init.selector; }
approve()
could be front-runned<a name="L-03"></a>Contracts:
file: src/enforcer/HolographERC20.sol ..................................... // Lines: [516-525] function _approve( address account, address spender, uint256 amount ) private { require(account != address(0), "ERC20: account is zero address"); require(spender != address(0), "ERC20: spender is zero address"); _allowances[account][spender] = amount; emit Approval(account, spender, amount); }
_isContract()
is not safe to use and could be bypassed in some cases<a name="L-04"></a>Contracts:
file: src/HolographFactory.sol ..................................... // Lines: [210-216] function _isContract(address contractAddress) private view returns (bool) { bytes32 codehash; assembly { codehash := extcodehash(contractAddress) } return (codehash != 0x0 && codehash != precomputekeccak256("")); }
ERC721.transferFrom()
doesn't check for the case, where the receiver is a smart-contract without ERC721.onCheckedReceived()
method **<a name="L-05"></a>Contracts:
file: src/enforcer/HolographERC20.sol ..................................... // Lines: [517-531] function transferFrom( address from, address to, uint256 tokenId, bytes memory data ) public payable { require(_isApproved(msg.sender, tokenId), "ERC721: not approved sender"); if (_isEventRegistered(HolographERC721Event.beforeTransfer)) { require(SourceERC721().beforeTransfer(from, to, tokenId, data)); } _transferFrom(from, to, tokenId); if (_isEventRegistered(HolographERC721Event.afterTransfer)) { require(SourceERC721().afterTransfer(from, to, tokenId, data)); } }
#0 - alexanderattar
2022-11-09T20:33:42Z
We will consider some of these
🌟 Selected for report: oyc_109
Also found by: 0x040, 0x1f8b, 0x5rings, 0xNazgul, 0xSmartContract, 0xZaharina, 0xsam, 0xzh, 2997ms, Amithuddar, Aymen0909, B2, Bnke0x0, Deivitto, Diana, Dinesh11G, Franfran, JC, JrNet, Jujic, KingNFT, KoKo, Mathieu, Metatron, Mukund, Olivierdem, PaludoX0, Pheonix, Picodes, RaymondFam, RedOneN, ReyAdmirado, Rolezn, Saintcode_, Satyam_Sharma, Shinchan, Tagir2003, Tomio, Waze, Yiko, __141345__, adriro, ajtra, aysha, ballx, beardofginger, bobirichman, brgltd, bulej93, catchup, catwhiskeys, cdahlheimer, ch0bu, chaduke, chrisdior4, cryptostellar5, cylzxje, d3e4, delfin454000, dharma09, djxploit, durianSausage, emrekocak, erictee, exolorkistis, fatherOfBlocks, gianganhnguyen, gogo, halden, hxzy, i_got_hacked, iepathos, karanctf, leosathya, lucacez, lukris02, lyncurion, m_Rassska, martin, mcwildy, mics, nicobevi, peanuts, peiw, rbserver, ret2basic, rotcivegaf, ryshaw, sakman, sakshamguruji, saneryee, sikorico, skyle, svskaushik, tnevler, vv7, w0Lfrum, zishansami
222.2506 USDC - $222.25
[GQ-xx]
, buf for general optimization patterns [G-xx]
- only the random one. This is done, cuz I wanna focus more on the quality side. Although, some [GQ-xx]s
might be invalid, sorry about it! Enjoy!calldataload
for read-only arguments in functionsa = a + b
instead of a += b
, in a case of a
being a state variablerequire/if
statements according to the gas usagerequire()/reverts
before some computationssload
++i/--i
instead of i++/i--
unchecked{++i}
instead of i++
in loopsif/require
statementstype(uint256).max
every timecalldataload
for read-only arguments in functions<a name="GQ-01"></a>calldataload
, or replace memory
with calldata
, if args are not supposed to be modified.Contracts:
file: contracts/HolographBridge.sol .................................... // Lines: [63-78] // Comment: use calldata for `initPayload` function init(bytes memory initPayload) external override returns (bytes4) { require(!_isInitialized(), "HOLOGRAPH: already initialized"); (address factory, address holograph, address operator, address registry) = abi.decode( initPayload, (address, address, address, address) ); assembly { sstore(_adminSlot, origin()) sstore(_factorySlot, factory) sstore(_holographSlot, holograph) sstore(_operatorSlot, operator) sstore(_registrySlot, registry) } _setInitialized(); return InitializableInterface.init.selector; } // Lines: [163-167] // Comment: use calldata instead of memory for `returnedPayload`, since it has not been modified. (bytes4 selector, bytes memory returnedPayload) = Holographable(holographableContract).bridgeOut( toChain, msg.sender, bridgeOutPayload ); // Lines: [197-230] function revertedBridgeOutRequest( address sender, uint32 toChain, address holographableContract, bytes calldata bridgeOutPayload ) external returns (string memory revertReason) { /** * @dev make a bridgeOut function call to the holographable contract inside of a try/catch */ try Holographable(holographableContract).bridgeOut(toChain, sender, bridgeOutPayload) returns ( bytes4 selector, bytes memory payload ) { /** * @dev ensure returned selector is bridgeOut function signature, to guarantee that the function was called and succeeded */ if (selector != Holographable.bridgeOut.selector) { /** * @dev if selector does not match, then it means the request failed */ return "HOLOGRAPH: bridge out failed"; } assembly { /** * @dev the entire payload is sent back in a revert */ revert(add(payload, 0x20), mload(payload)) } } catch Error(string memory reason) { return reason; } catch { return "HOLOGRAPH: unknown error"; } }
Contracts:
file: src/HolographBridge.sol ............................... // Lines: [197-202] // Comment: `revertReason` has not been used. function revertedBridgeOutRequest( address sender, uint32 toChain, address holographableContract, bytes calldata bridgeOutPayload ) external returns (string memory revertReason) {}
a = a + b
instead of a += b
, in case of a
being state variable<a name="GQ-03"></a>a += b
some extra copies of a
are performed.Contracts:
file: src/HolographOperator.sol ............................... // Lines: [279-283] _bondedAmounts[job.operator] -= amount; /** * @dev the slashed amount is sent to current operator */ _bondedAmounts[msg.sender] += amount; // Lines: [735-735] _bondedAmounts[operator] += amount;
file: src/enforcer/HolographERC20.sol ..................................... // Lines: [586-587] _totalSupply += amount; _balances[to] += amount; // Lines: [603-603] _balances[recipient] += amount; // Lines: [584-584] _totalSupply -= amount;
require/if
statements according to the gas usage<a name="GQ-04"></a>The tx should revert in any other cases: exceptif (!creationUnlocked && msg.sender != _owner) revert LBFactory__FunctionIsLockedForUsers(msg.sender);
true && true
, but checking msg.sender != _owner
first and then checking other part costs less gas in a cases, where the second part fails. Therefore, it's reasonable to put lightest conditions first.Contracts:
file: src/HolographBridge.sol ............................... // Lines: [156-159] // Comments: put the second condition first, since it cheaper require( _registry().isHolographedContract(holographableContract) || address(_factory()) == holographableContract, "HOLOGRAPH: not holographed" );
sload
<a name="GQ-10"></a>mload
costs only 3 units of gas, sload
(warm access) is about 100 units. Therefore, cache, when it's possible.Contracts:
file: src/HolographOperator.sol ......................................... // Lines: [202-312] // Comment: it's possible to cache `_operatorPods[pod].length` here function executeJob(bytes calldata bridgeInRequestPayload) external payable { /** * @dev derive the payload hash for use in mappings */ bytes32 hash = keccak256(bridgeInRequestPayload); /** * @dev check that job exists */ require(_operatorJobs[hash] > 0, "HOLOGRAPH: invalid job"); uint256 gasLimit = 0; uint256 gasPrice = 0; assembly { /** * @dev extract gasLimit */ gasLimit := calldataload(sub(add(bridgeInRequestPayload.offset, bridgeInRequestPayload.length), 0x40)) /** * @dev extract gasPrice */ gasPrice := calldataload(sub(add(bridgeInRequestPayload.offset, bridgeInRequestPayload.length), 0x20)) } /** * @dev unpack bitwise packed operator job details */ OperatorJob memory job = getJobDetails(hash); /** * @dev to prevent replay attacks, remove job from mapping */ delete _operatorJobs[hash]; /** * @dev check that a specific operator was selected for the job */ if (job.operator != address(0)) { /** * @dev switch pod to index based value */ uint256 pod = job.pod - 1; /** * @dev check if sender is not the selected primary operator */ if (job.operator != msg.sender) { /** * @dev sender is not selected operator, need to check if allowed to do job */ uint256 elapsedTime = block.timestamp - uint256(job.startTimestamp); uint256 timeDifference = elapsedTime / job.blockTimes; /** * @dev validate that initial selected operator time slot is still active */ require(timeDifference > 0, "HOLOGRAPH: operator has time"); /** * @dev check that the selected missed the time slot due to a gas spike */ require(gasPrice >= tx.gasprice, "HOLOGRAPH: gas spike detected"); /** * @dev check if time is within fallback operator slots */ 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 time to reward the current operator */ 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; /** * @dev check if slashed operator has enough tokens bonded to stay */ if (_bondedAmounts[job.operator] >= amount) { /** * @dev enough bond amount leftover, put operator back in */ _operatorPods[pod].push(job.operator); _operatorPodIndex[job.operator] = _operatorPods[pod].length - 1; _bondedOperators[job.operator] = job.pod; } else { /** * @dev slashed operator does not have enough tokens bonded, return remaining tokens only */ uint256 leftovers = _bondedAmounts[job.operator]; if (leftovers > 0) { _bondedAmounts[job.operator] = 0; _utilityToken().transfer(job.operator, leftovers); } } } else { /** * @dev the selected operator is executing the job */ _operatorPods[pod].push(msg.sender); _operatorPodIndex[job.operator] = _operatorPods[pod].length - 1; _bondedOperators[msg.sender] = job.pod; } }
++i/--i
instead of i++/i--
<a name="G-12"></a>i++
, the compiler has to to create a temp variable to return i
(if there's a need) and then i
gets incremented.++i
, the compiler just simply returns already incremented value.Contracts:
file: src/HolographOperator.sol ................................ // Lines: [682-684] for (uint256 i = 0; i < length; i++) { operators[i] = _operatorPods[pod][index + i]; }
unchecked{++i};
instead of i++;
/++i;
in loops<a name="G-13"></a>Since Solidity 0.8.0, all arithmetic operations revert on over- and underflow by default Therefore, unchecked
box can be used to prevent all unnecessary checks, if it's no a way to get a reversion.
There are ~1e80 atoms in the universe, so 2^256 is closed to that number, therefore it's no a way to be overflowed, because of the gas limit as well.
Contracts:
file: src/HolographOperator.sol ................................ // Lines: [682-684] for (uint256 i = 0; i < length; i++) { operators[i] = _operatorPods[pod][index + i]; }
onlyOwner
modifier will be reverted, if the user invoke following methods.Contracts:
file: src/enforcer/PA1D.sol ............................... // Lines: [372-381] function configurePayouts(address payable[] memory addresses, uint256[] memory bps) public onlyOwner { require(addresses.length == bps.length, "PA1D: missmatched array lenghts"); uint256 totalBp; for (uint256 i = 0; i < addresses.length; i++) { totalBp = totalBp + bps[i]; } require(totalBp == 10000, "PA1D: bps down't equal 10000"); _setPayoutAddresses(addresses); _setPayoutBps(bps); } ```
function execTransaction
-> 0x6a761202
- function execTransaction32785586
-> 0x00000081
which will the first in a view-table after sorting, thus saves some gas avoiding extra jums each time.if/require
statements<a name="G-17"></a>AND
OR
opcodes.Contracts:
file: src/HolographBridge.sol .............................. // Lines: [282-285] if (gasPrice < type(uint256).max && gasLimit < type(uint256).max) { (uint256 hlgFee, ) = _operator().getMessageFee(toChain, gasLimit, gasPrice, bridgeOutPayload); fee = hlgFee; }
type(uint256).max
every time<a name="G-18"></a>Contracts:
file: src/HolographBridge.sol .............................. // Lines: [282-285] if (gasPrice < type(uint256).max && gasLimit < type(uint256).max) { (uint256 hlgFee, ) = _operator().getMessageFee(toChain, gasLimit, gasPrice, bridgeOutPayload); fee = hlgFee; }
Contracts:
file: src/HolographFactory.sol ............................... // Lines: [125-125] // Comments: if the bytecode of created contract is huge, that would lead in significant amount of gas needed to push into the memory. bytes memory holographerBytecode = type(Holographer).creationCode;
Holograph protocol
for taking that much effort on that.