Platform: Code4rena
Start Date: 25/01/2023
Pot Size: $90,500 USDC
Total HM: 3
Participants: 26
Period: 9 days
Judge: GalloDaSballo
Id: 209
League: ETH
Rank: 7/26
Findings: 1
Award: $1,448.21
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: descharre
Also found by: 0xA5DF, 0xSmartContract, Aymen0909, Deivitto, NoamYakov, ReyAdmirado, Rolezn, chaduke, cryptostellar5, matrix_0wl
1448.2102 USDC - $1,448.21
ID | Finding | Gas saved | Instances |
---|---|---|---|
G-01 | Make for loop unchecked | 644 | 13 |
G-02 | Use an unchecked block when operands can't underflow/overflow | 688 | 7 |
G-03 | Write element of storage struct to memory when used more than once | 10 | 1 |
G-04 | Call block.timestamp direclty instead of function | 22 | 1 |
G-05 | Make 3 event parameters indexed when possible | 1059 | 2 |
G-06 | Transfer erc20 immediately to Dripshub | 57824 | 1 |
G-07 | Transfer ERC20 immediately to the user | 22112 | 1 |
G-08 | Use double if statements instead of && | 4 | 40 |
G-09 | Miscellaneous | 100 | 3 |
The risk of for loops getting overflowed is extremely low. Because it always increments by 1 and is limited to the arrays length. Even if the arrays are extremely long, it will take a massive amount of time and gas to let the for loop overflow.
callBatched()
gas saved: 76createSplits()
gas saved: 92DripsHub: receiveDrips()
gas saved: 58DripsHub: receiveDripsResult()
gas saved: 54DripsHub: squeezeDrips()
gas saved: 32DripsHub: squeezeDripsResult()
gas saved: 71DripsHub: squeezeDripsResult()
gas saved: 29DripsHub: balanceAt()
gas saved: 59DripsHub: setDrips()
gas saved: 31emitUserMetadata()
gas saved: 59DripsHub: splitResult()
gas saved: 10DripsHub: split()
gas saved: 10DripsHub: setSplits()
gas saved: 63There are 2 ways to make a for loop unchecked in a safe way
- for (uint256 i = 0; i < calls.length; i++) { - for (uint256 i = 0; i < calls.length;) { Call memory call = calls[i]; returnData[i] = _call(sender, call.to, call.data, call.value); + unchecked{ + i++ + } }
+ function unchecked_inc(uint256 x) private pure returns (uint256) { + unchecked { + return x + 1; + } + } - for (uint256 i = 0; i < calls.length; i++) { + for (uint256 i = 0; i < calls.length; i = unchecked_inc(i)) { Call memory call = calls[i]; returnData[i] = _call(sender, call.to, call.data, call.value); }
Division can't overflow or underflow unless the divisor is -1. Which is not the case here. DripsHub: squeezeDripsResult()
gas saved: 60
uint256 idxMid = (idx + idxCap) / 2;
There is no possibility of overflowing when incrementing by one. currCycleConfigs is a uint32 but even for that will take a massive amount of time to make it overflow. DripsHub: setDrips()
gas saved: 215
state.currCycleConfigs++;
For ++, the same applies to:
DripsHub: squeezeDrips()
gas saved: 28DripsHub: setDrips()
gas saved: 35DripsHub: registerDriver()
gas saved: 200DripsHub.sol#L632: because of the require statement above, it can't overflow. give()
gas saved: 54
DripsHub.sol#L636: amount will never be larger than the total balance: collect()
gas saved: 96
When a struct contains a nested mapping, it's not possible to save it in memory. But it's possible to save one element of the struct to memory when it's used more than once. DripsHub: balanceAt()
gas saved: 10
DripsState storage state = _dripsStorage().states[assetId][userId]; + uint32 updateTime = state.updateTime; - require(timestamp >= state.updateTime, "Timestamp before last drips update"); + require(timestamp >= updateTime, "Timestamp before last drips update"); require(_hashDrips(receivers) == state.dripsHash, "Invalid current drips list"); - return _balanceAt(state.balance, state.updateTime, state.maxEnd, receivers, timestamp); + return _balanceAt(state.balance, updateTime, state.maxEnd, receivers, timestamp);
The _currTimestamp()
function casts the block.timestamp
to a uint32. However it's not always necessary to have a uint32.
In the example below you are assigning the timestamp to a uint256. Which makes it unnecessary to cast the timestamp to uint32. So it's better to call block.timestamp
directly.
Drips.sol#L689: DripsHub: setDrips()
gas saved: 22
- uint256 enoughEnd = _currTimestamp(); + uint256 enoughEnd = block.timestamp;
It's the most gas efficient to make up to 3 event parameters indexed. If there are less than 3 parameters, you need to make all parameters indexed.
- event UserMetadataEmitted(uint256 indexed userId, bytes32 indexed key, bytes value); + event UserMetadataEmitted(uint256 indexed userId, bytes32 indexed key, bytes indexed value);
The event is used in the function emitUserMetaData, this function is used multiple times. The gas saved for making all the parameters indexed is:
emitUserMetadata()
gas saved: 364emitUserMetadata()
gas saved: 208createSplits()
gas saved: 139emitUserMetadata()
gas saved: 139safeMint()
gas saved: 209The same extra indexed parameter can be applied to:
DripsHub: setDrips()
Dripshub: give()
gas saved: 9449AddressDriver: give()
gas saved: 29439NFTDriver: give()
gas saved: 18936When you call the give()
function in the Address or NFTDriver. The erc20 token are first getting send to those contracts and afterwards to the DripsHub contract. It's also possible to send the tokens directly to the dripsHub contract.
AddressDriver.sol#L170-L176 NFTDriver.sol#L285-L291
function _transferFromCaller(IERC20 erc20, uint128 amt) internal { - erc20.safeTransferFrom(_msgSender(), address(this), amt); + erc20.safeTransferFrom(_msgSender(), address(dripsHub), amt); // Approval is done only on the first usage of the ERC-20 token in DripsHub by the driver - if (erc20.allowance(address(this), address(dripsHub)) == 0) { - erc20.safeApprove(address(dripsHub), type(uint256).max); } }
function give(uint256 userId, uint256 receiver, IERC20 erc20, uint128 amt) public whenNotPaused onlyDriver(userId) { _increaseTotalBalance(erc20, amt); Splits._give(userId, receiver, _assetId(erc20), amt); - erc20.safeTransferFrom(msg.sender, address(this), amt); }
setDrips need to be adjusted or you can create a seperate function for that.
AddressDriver: collect()
gas saved: 12954NFTDriver: collect()
gas saved: 9158The same thing can be done for the collect()
function. Instead of transferring first to the address or NFTdriver. You can instantly transfer to the user.
DripsHub.sol#L386-L395)
- function collect(uint256 userId, IERC20 erc20) + function collect(uint256 userId, IERC20 erc20, address transferTo) public whenNotPaused onlyDriver(userId) returns (uint128 amt) { amt = Splits._collect(userId, _assetId(erc20)); _decreaseTotalBalance(erc20, amt); - erc20.safeTransfer(msg.sender, amt); + erc20.safeTransfer(transferTo, amt); }
function collect(IERC20 erc20, address transferTo) public whenNotPaused returns (uint128 amt) { - amt = dripsHub.collect(callerUserId(), erc20); + amt = dripsHub.collect(callerUserId(), erc20, transferTo); - erc20.safeTransfer(transferTo, amt); }
If the if statement has a logical AND and is not followed by an else statement, it can be replaced with 2 if statements.
Saves a little bit of deployment gas
- bytes32 private immutable _counterSlot = _erc1967Slot("eip1967.immutableSplitsDriver.storage"); + bytes32 private immutable _counterSlot = bytes32(uint256(keccak256(bytes("eip1967.immutableSplitsDriver.storage"))) - 1);
- mapping(uint32 => AmtDelta) storage amtDeltas = state.amtDeltas; for (uint32 cycle = fromCycle; cycle < toCycle; cycle++) { - delete amtDeltas[cycle]; + delete state.amtDeltas[cycle]; } // The next cycle delta must be relative to the last received cycle, which got zeroed. // In other words the next cycle delta must be an absolute value. if (finalAmtPerCycle != 0) { - amtDeltas[toCycle].thisCycle += finalAmtPerCycle; + state.amtDeltas[toCycle].thisCycle += finalAmtPerCycle; }
In the _receiveDrips()
function you can change the check to receivedAmt != 0
. Because when fromCycle and toCycle are the same. ReceivedAmt will be 0.
Drips.sol#L243 receiveDrips()
gas saved: 75
(receivedAmt, receivableCycles, fromCycle, toCycle, finalAmtPerCycle) = _receiveDripsResult(userId, assetId, maxCycles); - if (fromCycle != toCycle) { + if (receivedAmt != 0) {
#0 - GalloDaSballo
2023-02-15T15:39:02Z
G-01 Make for loop unchecked 644 13 Let's say 260 gas (13 * 20
G-02 Use an unchecked block when operands can't underflow/overflow 688 7 20 * 7 = 140
G-03 Write element of storage struct to memory when used more than once 10 1 100 gas
G-04 Call block.timestamp direclty instead of function 22 1 16
G-06 Transfer erc20 immediately to Dripshub 57824 1 5k
G-07 Transfer ERC20 immediately to the user 22112 1 5k
Rest is marginal
10k+
#1 - c4-judge
2023-02-24T10:58:53Z
GalloDaSballo marked the issue as grade-a
#2 - GalloDaSballo
2023-02-24T10:58:59Z
Best because the savings are high impact and tangible
#3 - c4-judge
2023-02-24T10:59:03Z
GalloDaSballo marked the issue as selected for report