Platform: Code4rena
Start Date: 26/08/2021
Pot Size: $100,000 USDC
Total HM: 8
Participants: 13
Period: 14 days
Judge: Albert Chon
Total Solo HM: 7
Id: 27
League: COSMOS
Rank: 5/13
Findings: 3
Award: $3,410.36
π Selected for report: 6
π Solo Findings: 0
1268.3578 USDC - $1,268.36
hrkrshnn
updateValset
does not have enough sanity checksIn updateValset function, the current set of validators adds a new set.
It is missing the check that the combined power of all new validators is
above the state_powerThreshold
. If this is false, then the contract is
effectively stuck. Consider adding an on-chain check for this.
It is also worth adding a that the size of the new validator check is less than a certain number.
Here is a rough calculation explaining how 10000 validators (an extreme example) is too much:
N
validators are needed to get the total threshold
above state_powerThreshold
.ecrecover
,
costing at least 3000
gas, the minimum gas needed for getting a
proposal over state_powerThreshold
would be N * 3000
N * 3000
cannot be more than the block.gaslimit
Currently, this
puts N
to be less than 10000
Another approach to solve the above potential problems is to do the updating as a two step process:
This guarantees that the new set of validators has enough power to pass threshold and doesn't have gas limit issues in doing so.
#0 - jkilpatr
2021-09-09T23:20:19Z
Semi duplicate of #63, #37 which describes the power sum issue
Also a semi duplicate of #9 which describes the block size issue
these are both valid and should be assigned congruent severity with the duplicates.
#1 - loudoguno
2021-10-01T03:45:42Z
reopening as per judges assessment as "primary issue" on findings sheet
π Selected for report: hrkrshnn
524.714 USDC - $524.71
hrkrshnn
There are currently several strings that are larger than 32 bytes.
Revert strings above 32 characters would need an additional mstore
.
This would incur cost for an additional mstore
, along with cost for
memory expansion, as well as cost for additional stack operations. This
cost is only relevant when the revert condition is met.
Shortening would also reduce the deploy cost for the contract in all cases.
Consider using Custom errors from solidity 0.8.4, which is more gas efficient than revert strings.
It is a good practice to make public
functions that are not referenced
inside the code to external
. In very old Solidity versions, in some
cases, this lead to a decrease in gas. However, this is unlikely to be
the case now.
#0 - jkilpatr
2021-09-09T23:26:05Z
Partial duplicate of #50 from the same submitter.
#1 - albertchon
2021-09-23T13:46:58Z
Agreed, although marking this as a unique report
#2 - loudoguno
2021-10-01T03:54:03Z
reopening as per judges assessment as "primary issue" on findings sheet
236.1213 USDC - $236.12
hrkrshnn
Using newer compiler versions and the optimizer gives gas optimizations and additional safety checks for free!
The advantages of versions 0.8.*
over <0.8.0
are:
0.8.0
(can be more gas efficient than
library based safemath.)0.8.2
, leads to cheaper runtime gas. Especially relevant when
the contract has small functions. For example, OpenZeppelin
libraries typically have a lot of small helper functions and if they
are not inlined, they cost an additional 20 to 40 gas because of 2
extra jump
instructions and additional stack operations needed for
function calls.0.8.3
, storing packed structs, in some cases used an
additional storage read operation. After
EIP-2929, if the slot was
already cold, this means unnecessary stack operations and extra
deploy time costs. However, if the slot was already warm, this means
additional cost of 100
gas alongside the same unnecessary stack
operations and extra deploy time costs.0.8.4
, leads to cheaper deploy time cost and run time cost.
Note: the run time cost is only relevant when the revert condition
is met. In short, replace revert strings by custom errors.#0 - jkilpatr
2021-09-10T14:27:35Z
Especially since we're backporting abi encoding v2 a complete upgrade may be advisable. Will attempt
Duplicate of #41
#1 - loudoguno
2021-10-01T03:54:08Z
reopening as per judges assessment as "primary issue" on findings sheet
π Selected for report: hrkrshnn
524.714 USDC - $524.71
hrkrshnn
Consider a generic example of an array arr
and the following loop:
for (uint i = 0; i < arr.length; i++) { // do something that doesn't change arr.length }
In the above case, the solidity compiler will always read the length of
the array during each iteration. That is, if it is a storage array, this
is an extra sload
operation (100 additional extra gas for each
iteration except for the first) and if it is a memory array, this is an
extra mload
operation (3 additional gas for each iteration except for
the first).
This extra costs can be avoided by caching the array length (in stack):
uint length = arr.length; for (uint i = 0; i < length; i++) { // do something that doesn't change arr.length }
In the above example, the sload
or mload
operation is only done once
and subsequently replaced by a cheap dupN
instruction.
This optimization is especially important if it is a storage array or if it is a lengthy for loop.
Note that the Yul based optimizer (not enabled by default; only relevant
if you are using --experimental-via-ir
or the equivalent in standard
JSON) can sometimes do this caching automatically. However, this is
likely not the case in your project.
#0 - jkilpatr
2021-09-10T14:26:31Z
Nice flag here, we may not be able to use this one as we're already very close to our stack limit (you can see we do significant scoping to get around it). We'll see if this can be applied.
Edit: Did not end up running into stack issues implementing this. It did save us some gas, but less than .1% so I don't think it's worth the extra lines. Thanks for the suggestion!
#1 - loudoguno
2021-10-01T03:54:17Z
reopening as per judges assessment as "primary issue" on findings sheet
95.6291 USDC - $95.63
hrkrshnn
immutable
Solidity 0.6.5
introduced immutable
as a major feature. It allows setting
contract-level variables at construction time which gets stored in code
rather than storage.
Consider the following generic example:
contract C { /// The owner is set during contruction time, and never changed afterwards. address public owner = msg.sender; }
In the above example, each call to the function owner()
reads from
storage, using a sload
. After
EIP-2929, this costs 2100 gas
cold or 100 gas warm. However, the following snippet is more gas
efficient:
contract C { /// The owner is set during contruction time, and never changed afterwards. address public immutable owner = msg.sender; }
In the above example, each storage read of the owner
state variable is
replaced by the instruction push32 value
, where value
is set during
contract construction time. Unlike the last example, this costs only 3
gas.
#0 - jkilpatr
2021-09-10T13:17:35Z
This is a good improvement that will have some nice gas savings considering how often we access these variables. Which should probably be hardcoded rather than constructor arguments anyways.
also mentioned (not directly though) in #44
#1 - loudoguno
2021-10-01T03:54:32Z
reopening as per judges assessment as "primary issue" on findings sheet
π Selected for report: hrkrshnn
524.714 USDC - $524.71
hrkrshnn
calldata
instead of memory
for function parametersIn some cases, having function arguments in calldata
instead of
memory
is more optimal.
Consider the following generic example:
contract C { function add(uint[] memory arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } }
In the above example, the dynamic array arr
has the storage location
memory
. When the function gets called externally, the array values are
kept in calldata
and copied to memory
during ABI decoding (using the
opcode calldataload
and mstore
). And during the for loop, arr[i]
accesses the value in memory using a mload
. However, for the above
example this is inefficient. Consider the following snippet instead:
contract C { function add(uint[] calldata arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } }
In the above snippet, instead of going via memory, the value is directly
read from calldata
using calldataload
. That is, there are no
intermediate memory operations that carries this value.
Gas savings: In the former example, the ABI decoding begins with
copying value from calldata
to memory
in a for loop. Each iteration
would cost at least 60 gas. In the latter example, this can be
completely avoided. This will also reduce the number of instructions and
therefore reduces the deploy time cost of the contract.
In short, use calldata
instead of memory
if the function argument
is only read.
Note that in older Solidity versions, changing some function arguments
from memory
to calldata
may cause "unimplemented feature error".
This can be avoided by using a newer (0.8.*
) Solidity compiler.
Some parameters in examples given above are later hashed. It may be
beneficial for those parameters to be in memory
rather than
calldata
.
#0 - jkilpatr
2021-09-10T14:25:13Z
Will attempt this modification and place results on reduced costs here
Edit: After experimenting some, it doesn't seem there's an easy spot to use this as it pushes us into stack issues in update valset and elsewhere I've tried it.
#1 - albertchon
2021-09-23T13:58:35Z
Yeah we tried this as well and it didn't work, oh well.
#2 - loudoguno
2021-10-01T03:54:24Z
reopening as per judges assessment as "primary issue" on findings sheet
π Selected for report: ElliotFriedman
Also found by: hrkrshnn
236.1213 USDC - $236.12
hrkrshnn
It is a good practice to make public
functions that are not referenced
inside the code to external
. In very old Solidity versions, in some
cases, this lead to a decrease in gas. However, this is unlikely to be
the case now.
#0 - jkilpatr
2021-09-09T23:26:22Z
partial duplicate of #49 from the same submitter.