Platform: Code4rena
Start Date: 16/09/2021
Pot Size: $50,000 USDC
Total HM: 26
Participants: 30
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 17
Id: 36
League: ETH
Rank: 13/30
Findings: 3
Award: $1,265.37
🌟 Selected for report: 6
🚀 Solo Findings: 0
450.8693 USDC - $450.87
hrkrshnn
The createBasket function does not account for tokens with fee on transfer.
function createBasket(uint256 idNumber) external override returns (IBasket) { // ... for (uint256 i = 0; i < bProposal.weights.length; i++) { IERC20 token = IERC20(bProposal.tokens[i]); token.safeTransferFrom(msg.sender, address(this), bProposal.weights[i]); token.safeApprove(address(newBasket), bProposal.weights[i]); } // ... }
The function safeTransferFrom
may not transfer exactly
bProposal.weights[i]
amount of tokens, for tokens with a fee on
transfer. This means that the safeApprove
call in the next line would
be approving more tokens than what was received, leading to accounting
issues.
It is recommended to find the balance of the current contract before and
after the transferFrom
to see how much tokens were received, and
approve only what was received.
#0 - frank-beard
2021-10-19T16:49:07Z
the protocol for now is only expected to work with defi safe, standard erc-20 tokens.
#1 - GalloDaSballo
2021-12-02T00:50:25Z
This finding is similar to #206 , but in contrast to it, it shows a specific way to brick / grief the protocol, as per the docs:
2 — Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.
This is an hypothetical attack with stated assumptions
Will not mark as duplicate and will consider this as a valid finding as it shows a specific vulnerability when paired with feeOnTransfer
tokens
Mitigation can be as simple as never using feeOnTransfer
tokens
🌟 Selected for report: hrkrshnn
333.9773 USDC - $333.98
hrkrshnn
block.gaslimit
The function
proposeBasketLicense
allows initializing proposals of with arbitrary amount of tokens.
However, createBasket
stage involves the actual transfers. Since each
unique token in the list undergoes a safeApprove
, which would cost at
least 22,100
gas (for zero to non-zero sstore update). Taking this
alone would mean that having a token list of size 1300
would exceed
the current block gas limit. This number would in practice be even lower
when including other calls.
proposeBasketLicense
for n
tokens and
try to estimate n
that exceeds the current block gas limit.proposeBasketLicense
with a
require(tokens.length < n)
.This would more or less guarantee that each proposed basket can be created.
#0 - GalloDaSballo
2021-12-02T01:28:29Z
I will keep this finding as valid because it is true
That said in practice this won't happen, so I would recommend the sponsor to know the theoretical limit and not to sweat about it
🌟 Selected for report: hrkrshnn
333.9773 USDC - $333.98
hrkrshnn
The protocol defines several parameters, for example, minLicenseFee which can be updated by using a corresponding setter function that the owner specifies.
However, there are no sanity checks for these parameters in the setter functions. Without sanity checks, the owner can set the parameters in a way to sabotage the protocol.
For example, setting
auctionDecrement
to 0
would mean that
settleAuction
function will always revert, making some operations in the protocol to
halt. Therefore, adding a check that the value is non-zero in the setter
avoids potential problems of this sort.
For each parameter, add a require
check for both upper and
lower-bounds.
#0 - frank-beard
2021-10-19T17:18:51Z
it is assumed the owner is trusted in this version
#1 - GalloDaSballo
2021-12-02T01:33:15Z
Personally I believe that adding checks to limit admin privileges is a way to gain community trust, as well as properly avoid potential rug vectors This typically helps users of the protocols (as the protocol doesn't require trust in the owner / dev) as well as avoid potential mistakes
I think the finding is valid, and while superficially trivial (why would I set it to the wrong value), I highly recommend the sponsor to add checks as they are security guarantees to the users of the protocol
15.5766 USDC - $15.58
hrkrshnn
tokenList.length
by existing variable length
modified contracts/contracts/Basket.sol @@ -61,7 +61,7 @@ contract Basket is IBasket, ERC20Upgradeable { require(_tokens[i] != address(0)); require(_weights[i] > 0); - for (uint256 x = 0; x < tokenList.length; x++) { + for (uint256 x = 0; x < length; x++) { require(_tokens[i] != tokenList[x]); }
Context: https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L64
The value tokenList.length
is read from memory and therefore requires
a mload(...)
(6 gas for push memory_offset
+ mload
). On the other
hand, this value is already available in the stack as length
and could
just be dup-ed
(3 gas). Saves 3 gas for each loop iteration of the
interior loop.
#0 - GalloDaSballo
2021-11-26T17:39:01Z
Thank you for actually explaining the math behind it!!
15.5766 USDC - $15.58
hrkrshnn
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.
A non-exhaustive list of examples:
#0 - GalloDaSballo
2021-11-26T17:33:48Z
Because factory is immutable, I agree with the finding
#1 - GalloDaSballo
2021-11-29T13:56:20Z
Duplicate of #15
🌟 Selected for report: hrkrshnn
57.691 USDC - $57.69
hrkrshnn
(This is only relevant if you are using the default solidity checked arithmetic.)
Consider the following generic for loop:
for (uint i = 0; i < length; i++) { // do something that doesn't change the value of i }
In this example, the for loop post condition, i.e., i++
involves
checked arithmetic, which is not required. This is because the value of
i
is always strictly less than length <= 2**256 - 1
. Therefore, the
theoretical maximum value of i
to enter the for-loop body is `2**256
. This means that the
i++` in the for loop can never overflow.
Regardless, the overflow checks are performed by the compiler.Unfortunately, the Solidity optimizer is not smart enough to detect this and remove the checks. One can manually do this by:
for (uint i = 0; i < length; i = unchecked_inc(i)) { // do something that doesn't change the value of i } function unchecked_inc(uint i) returns (uint) { unchecked { return i + 1; } }
Note that it's important that the call to unchecked_inc
is inlined.
This is only possible for solidity versions starting from 0.8.2
.
Gas savings: roughly speaking this can save 30-40 gas per loop iteration. For lengthy loops, this can be significant!
./contracts/contracts/Auction.sol:81: for (uint256 i = 0; i < inputTokens.length; i++) { ./contracts/contracts/Auction.sol:85: for (uint256 i = 0; i < outputTokens.length; i++) { ./contracts/contracts/Auction.sol:96: for (uint256 i = 0; i < pendingWeights.length; i++) { ./contracts/contracts/Auction.sol:142: for (uint256 i = 0; i < bountyIds.length; i++) { ./contracts/contracts/Factory.sol:103: for (uint256 i = 0; i < bProposal.weights.length; i++) { ./contracts/contracts/Basket.sol:60: for (uint i = 0; i < length; i++) { ./contracts/contracts/Basket.sol:225: for (uint256 i = 0; i < weights.length; i++) { ./contracts/contracts/Basket.sol:231: for (uint256 i = 0; i < weights.length; i++) { ./contracts/contracts/Basket.sol:238: for (uint256 i = 0; i < weights.length; i++) {
#0 - GalloDaSballo
2021-11-26T17:34:38Z
It does save gas, at what cost!?
🌟 Selected for report: hrkrshnn
57.691 USDC - $57.69
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.
#0 - GalloDaSballo
2021-11-26T17:40:21Z
Agree with the finding, great explanation!