PoolTogether - LeoS's results

A protocol for no-loss prize savings

General Information

Platform: Code4rena

Start Date: 07/07/2023

Pot Size: $121,650 USDC

Total HM: 36

Participants: 111

Period: 7 days

Judge: Picodes

Total Solo HM: 13

Id: 258

League: ETH

PoolTogether

Findings Distribution

Researcher Performance

Rank: 71/111

Findings: 1

Award: $24.30

Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Awards

24.2984 USDC - $24.30

Labels

bug
G (Gas Optimization)
grade-b
G-18

External Links

Summary

IssueInstancesGas Saved
[G-01]Variables inside struct should be optimised18490
[G-02]Use calldata instead of memory1524
[G-03]Use named return1575-195
[G-04]Use switch from assembly for direct number constants if-else198
[G-05]Use assembly to write address storage values145
[G-06]Functions guaranteed to revert when called by normal users can be marked payable242
[G-07Ternary over if ... else339

Gas Optimizations

[G-01] Variables inside struct should be optimised

Packing struct allow for multiple variables to be stored in the same slot, as the Solidity EVM works with 32-byte units. This results in save for all related operations.

1 instances 1 slot can be saved

struct Observation {
  // track the total amount available as of this Observation
  uint96 available;
  // track the total accumulated previously
- uint168 disbursed;
+ uint160 distursed;
}

96+160=256 -> 32 bytes Modifications obviously followed by the change line 92-93 The size of the variable is reduced but this does not add any problem given the orders of magnitude.

With this change, these evolutions in gas average report can be observed:

DrawAccumulatorLibWrapper: add: 88086 -> 79729 (-8357) DrawAccumulatorLibWrapper: getDisbursedBetween: 15114 -> 15029 (-85) DrawAccumulatorLibWrapper: getTotalRemaining: 2541 -> 2511 (-30)

[G-02] Use calldata instead of memory

Using calldata instead of memory for function parameters can save gas if the argument is only read in the function

1 instances

With this change, this evolution in gas average report can be observed:

VaultFactory: deployVault: 3737309 -> 3736785 (-524)

[G-03] Use named return

Using the named return whenever possible saves gas by avoiding a return and the double declaration of a variable.

15 instances Save up to 5-13 gas per call (depending on the complexity of the function)

For instance, the code block below may be refactored as follows:

- function prizeCount(uint8 _tier) internal pure returns (uint256) {
+ function prizeCount(uint8 _tier) internal pure returns (uint256 _numberOfPrizes) {
-    uint256 _numberOfPrizes = 4 ** _tier;
+    _numberOfPrizes = 4 ** _tier;

-    return _numberOfPrizes;
 }

The NatSpec format will obviously also have to be redesigned.

[G-04] Use switch from assembly for direct number constants if-else

When there's a large series of if else assigning constant numbers, it's interesting to use the assembly switch. However, this may affect the readability of the code. Two alternatives are therefore proposed.

1 instance

Small optimisation (-23 gas each call)

function _estimatedPrizeCount(uint8 numTiers) internal pure returns (uint32) { uint32 estimatedPrizes; assembly { switch numTiers case 3 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_2_TIERS } case 4 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_3_TIERS } case 5 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_4_TIERS } case 6 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS } case 7 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS } case 8 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS } case 9 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS } case 10 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS } case 11 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS } case 12 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS } case 13 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_12_TIERS } case 14 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_13_TIERS } case 15 { estimatedPrizes := ESTIMATED_PRIZES_PER_DRAW_FOR_14_TIERS } default { estimatedPrizes := 0 } } return estimatedPrizes; }

Great optimisation (-98 gas each call)

function _estimatedPrizeCount(uint8 numTiers) internal pure returns (uint32) { assembly { let slot := mload(0x40) // Get a pointer to some free memory. mstore(0x40, add(slot, 0x20)) //Since the allocated memory will be return, the free memory pointer must be incremented switch numTiers case 3 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_2_TIERS) return(slot, 32) } case 4 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_3_TIERS) return(slot, 32) } case 5 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_4_TIERS) return(slot, 32) } case 6 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS) return(slot, 32) } case 7 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS) return(slot, 32) } case 8 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS) return(slot, 32) } case 9 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS) return(slot, 32) } case 10 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS) return(slot, 32) } case 11 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS) return(slot, 32) } case 12 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS) return(slot, 32) } case 13 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_12_TIERS) return(slot, 32) } case 14 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_13_TIERS) return(slot, 32) } case 15 { mstore(slot, ESTIMATED_PRIZES_PER_DRAW_FOR_14_TIERS) return(slot, 32) } default { mstore(slot, 0) return(slot, 32) } } }

With this last change, this evolution in gas average report can be observed:

TieredLiquidityDistributorWrapper: estimatedPrizeCount: 740 -> 642 (-98)

[G-05] Use assembly to write address storage values

This change has little effect on code reading.

1 instance Save up to 45 gas per call

- drawManager = _drawManager;
+ assembly {sstore(drawManager.slot, _drawManager)}

[G-06] Functions guaranteed to revert when called by normal users can be marked payable

Marking a function as payable can lower gas costs for legitimate callers by avoiding opcodes such as CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2).

2 instances Save up to 21 gas per call

[G-07] Ternary over if ... else

Replacing an if-else statement with the ternary operator can save gas.

For instance, the code block below may be refactored as follows:

- if (_tier != _canaryTier) {
- return _computeMaxFee(prizePool.getTierPrizeSize(_canaryTier - 1));
- } else {
- return _computeMaxFee(prizePool.getTierPrizeSize(_canaryTier));
+ return  (_tier != _canaryTier)
+ ? _computeMaxFee(prizePool.getTierPrizeSize(_canaryTier -  1))
+ : _computeMaxFee(prizePool.getTierPrizeSize(_canaryTier));

3 instances Save up to 13 gas per call

#0 - c4-judge

2023-07-18T19:05:17Z

Picodes marked the issue as grade-b

#1 - PierrickGT

2023-09-08T23:28:45Z

G-01: good one! Fixed in this PR: https://github.com/GenerationSoftware/pt-v5-prize-pool/pull/63 G-02, 03, 07: has been fixed G-04, 05: we would lose in code legibility G-06: functions shouldn't be marked as payable if they are not

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter