Platform: Code4rena
Start Date: 11/11/2022
Pot Size: $90,500 USDC
Total HM: 52
Participants: 92
Period: 7 days
Judge: LSDan
Total Solo HM: 20
Id: 182
League: ETH
Rank: 46/92
Findings: 2
Award: $120.17
š Selected for report: 0
š Solo Findings: 0
š Selected for report: 0xSmartContract
Also found by: 0x4non, 0xNazgul, 0xRoxas, 0xdeadbeef0x, 0xmuxyz, 9svR6w, Awesome, Aymen0909, B2, Bnke0x0, CloudX, Deivitto, Diana, Franfran, IllIllI, Josiah, RaymondFam, ReyAdmirado, Rolezn, Sathish9098, Secureverse, SmartSek, Trust, Udsen, a12jmx, aphak5010, brgltd, bulej93, c3phas, ch0bu, chaduke, chrisdior4, clems4ever, cryptostellar5, datapunk, delfin454000, fs0c, gogo, gz627, hl_, immeas, joestakey, lukris02, martin, nogo, oyc_109, pashov, pavankv, peanuts, pedr02b2, rbserver, rotcivegaf, sahar, sakman, shark, tnevler, trustindistrust, zaskoh, zgo
52.0338 USDC - $52.03
Solidity pragma versioning should be exactly same in all contracts. Currently some contracts use ^0.8.13 but some are fixed to 0.8.13
Examples: Syndicate.sol
, LiquidStakingManager.sol
indexed
fieldsIndex event fields make the field more quickly accessible to off-chain tools that parse events. However, note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields). Each event
should use three indexed
fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three fields, all of the fields should be indexed.
event ETHWithdrawnByDepositor(address depositor, uint256 amount);
event LPTokenBurnt(bytes blsPublicKeyOfKnot, address token, address depositor, uint256 amount);
event NewLPTokenIssued(bytes blsPublicKeyOfKnot, address token, address firstDepositor, uint256 amount);
event LPTokenMinted(bytes blsPublicKeyOfKnot, address token, address depositor, uint256 amount);
event ETHDeposited(address sender, uint256 amount);
event ETHWithdrawn(address receiver, address admin, uint256 amount);
event ERC20Recovered(address admin, address recipient, uint256 amount);
event WETHUnwrapped(address admin, uint256 amount);
To prevent any issues in the future (e.g. using solely hardhat to compile and deploy the contracts), upgrade the used OZ packages within the package.json to the latest versions. Currently the project is using 4.5.0 flowtable.
Upgrade it to stable 4.8.0 (which is the newest)
File: LiquidStakingManager.sol
uint256 public MODULO = 100_00000;
Should be:
uint256 public MODULO = 1e7
Solidity documents mention that properly functioning code should never reach a failing assert statement and if this happens there is a bug in the contract which should be fixed. Reference: https://docs.soliditylang.org/en/v0.8.15/control-structures.html#panic-via-assert-and-error-via-require
assert(syndicateFactoryMock.slot() == slot);
Replace assert by require.
#0 - c4-judge
2022-11-30T14:16:57Z
dmvt marked the issue as grade-b
š Selected for report: IllIllI
Also found by: 0xSmartContract, Awesome, Aymen0909, CloudX, Deivitto, ReyAdmirado, Saintcode_, bharg4v, brgltd, btk, c3phas, chrisdior4, ignacio, imare, lukris02, skyle, tnevler
68.1383 USDC - $68.14
Using the addition operator instead of plus-equals saves 113 gas
File: GiantPoolBase
idleETH += msg.value; -
idleETH = idleETH + msg.value; +
===========================
idleETH -= _amount; -
idleETH00 = idleETH - _amount; +
===========================
File: GiantSavETHVaultPool.sol
idleETH -= transactionAmount; -
idleETH = idleETH - transactionAmount; +
==========================
File: LiquidStakingManager.sol
numberOfKnots += 1; -
numberOfKnots = numberOfKnots + 1; +
========================
numberOfKnots += 1; -
numberOfKnots = numberOfKnots + 1; +
===========================
The unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas per loop:
for(uint256 i; i < _blsPubKeys.length; ++i) {
===========================
for(uint256 i; i < len; ++i)
Comparing to a constant (true or false) is a bit more expensive than directly checking the returned boolean value. I suggest using:
if(directValue) instead of if(directValue == true) and if(!directValue) instead of if(directValue == false) here:
if (isKnotRegistered[_blsPublicKey] == false) revert KnotIsNotRegisteredWithSyndicate();
if (isNoLongerPartOfSyndicate[_blsPublicKey] == true) revert KnotHasAlreadyBeenDeRegistered();
<x> / 2 is the same as <x> >> 1. The DIV opcode costs 5 gas, whereas SHR only costs 3 gas
return ethPerKnot / 2;
Consider changing it to:
return ethPerKnot >> 1;
Combining mappings of address into a single mapping of address to a struct can save a Gssset (20000 gas) operation per mapping combined. This also makes it cheaper for functions reading and writing several of these mappings by saving a Gkeccak256 operation- 30 gas.
File: LiquidStakingManager
mapping(address => bool) public isNodeRunnerWhitelisted; mapping(address => address) public smartWalletRepresentative; mapping(address => address) public smartWalletOfNodeRunner; mapping(address => address) public nodeRunnerOfSmartWallet; mapping(address => uint256) public stakedKnotsOfSmartWallet; mapping(address => address) public smartWalletDormantRepresentative; mapping(address => bool) public bannedNodeRunners;
Change it to:
struct Hold { bool isNodeRunnerWhitelisted; address smartWalletRepresentative; address smartWalletOfNodeRunner; address nodeRunnerOfSmartWallet; uint256 stakedKnotsOfSmartWallet; address smartWalletDormantRepresentative; bool bannedNodeRunners; } mapping (address => Hold) public holds;
If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2),DUP1(3),ISZERO(3),PUSH2(3),JUMPI(10),PUSH1(3),DUP1(3),REVERT(0),JUMPDEST(1),POP(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost:
function registerKnotsToSyndicate( bytes[] calldata _newBLSPublicKeyBeingRegistered ) external onlyOwner {
function deRegisterKnots(bytes[] calldata _blsPublicKeys) external onlyOwner { updateAccruedETHPerShares(); _deRegisterKnots(_blsPublicKeys); }
function addPriorityStakers(address[] calldata _priorityStakers) external onlyOwner { updateAccruedETHPerShares(); _addPriorityStakers(_priorityStakers); }
function updatePriorityStakingBlock(uint256 _endBlock) external onlyOwner { updateAccruedETHPerShares(); priorityStakingEndBlock = _endBlock; }
function setApproval(address to, bool status) external onlyOwner override { require( to != address(0), "OwnableSmartWallet: Approval cannot be set for zero address" ); // F: [OSW-2A] _setApproval(msg.sender, to, status); }
// Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27 Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas) for the extra SLOAD, and to avoid Gsset (20000 gas) when changing from false to true, after having been true in the past:
bool _deployOptionalGatekeeper,
bool status );
bool withdrawn;
mapping(address => bool) public isLiquidStakingManager;
bool _deployOptionalHouseGatekeeper,
#0 - c4-judge
2022-11-30T14:15:50Z
dmvt marked the issue as grade-b