Platform: Code4rena
Start Date: 11/11/2021
Pot Size: $50,000 USDC
Total HM: 9
Participants: 27
Period: 7 days
Judge: alcueca
Total Solo HM: 5
Id: 53
League: ETH
Rank: 8/27
Findings: 2
Award: $1,354.15
🌟 Selected for report: 4
🚀 Solo Findings: 0
🌟 Selected for report: PierrickGT
Also found by: WatchPug, gzeon, harleythedog, pants
pants
You use safeApprove of openZeppelin although it's deprecated. (see https://github.com/OpenZeppelin/openzeppelin-contracts/blob/566a774222707e424896c0c390a84dc3c13bdcb2/contracts/token/ERC20/utils/SafeERC20.sol#L38) IERC20(DAI).approve(address(uniswap), amount);
(For example line 37 ExchangeHelpers.sol)
#0 - maximebrugel
2021-11-18T08:49:23Z
Duplicated : #50
🌟 Selected for report: pants
866.6085 USDC - $866.61
pants
DummyRouter.sol .transfer isn't safe - with USDT can steal tokens (dummyswapToken).
We know this is a dummy contract but this should also be safe. Therefore we ask for low-risk and not high-risk.
#0 - adrien-supizet
2021-11-19T10:15:30Z
Safety here is pointless, this kind of dummy contract is only used for local tests to mock 0x behavior as shown in the project.
#1 - alcueca
2021-12-03T15:45:48Z
I assume that DummyRouter.sol should have been out of scope. Not stated, so the issue is valid.
pants
There are many for loops that follows this for-each pattern: (for example line 227 in FeeSplitter.sol or line 32 in MixinOperatorResolver.sol)
for (uint256 i = 0; i < array.length; i++) {
// do something with array[i]
}
In such for loops, the `array.length` is read on every iteration, instead of caching it once in a local variable and read it again using the local variable. ## Impact Storage reads are much more expensive than reading local variables. Memory and calldata reads are a bit more expensive than reading local variables. ## Tool Used Manual code review. ## Recommended Mitigation Steps Read these values from storage / memory / calldata once, cache them in local variables and then read them again using the local variables. For example:
uint256 length = array.length;
for (uint256 i = 0; i < length; i++) {
// do something with array[i]
}
#0 - adrien-supizet
2021-11-19T10:34:26Z
duplicate #7
🌟 Selected for report: GiveMeTestEther
Also found by: pants
47.7068 USDC - $47.71
pants
This impacts every for loop of the code and therefore can save a lot of gas!!! For example, FeeSplitter.sol lines 108, 125, 210, 227
There is no risk of overflow caused by increamenting the iteration index in for loops (the i++
in for (uint256 i = 0; i < numIterations; i++)
).
Increaments perform overflow checks that are not necessary in this case.
Manual code review.
Surround the increament expressions with an unchecked { ... }
block to avoid the default overflow checks. For example, change the loop
for (uint256 i = 0; i < numIterations; i++) { // ... }
to
for (uint256 i = 0; i < numIterations;) { // ... unchecked { ++i; } }
It is a little less readable but it saves a significant amount of gas.
#0 - adrien-supizet
2021-11-19T10:38:35Z
duplicate #25
🌟 Selected for report: pants
106.015 USDC - $106.02
pants
NestedRecords line 22 - you can save storage by reordering Holding struct fields in the following way:
original: struct Holding { address token; uint256 amount; bool isActive; }
change to:
struct Holding { uint256 amount; address token; bool isActive; }
#0 - maximebrugel
2021-11-19T10:27:02Z
Before :
After :
🌟 Selected for report: pants
106.015 USDC - $106.02
pants
MixinOperatorResolver.rebuildCache (addressCache[name]), isResolverCached (addressCache[name])
You can cache the value after the first read into a local variable to save the other SLOAD and also the "out of bounds" check.
#0 - maximebrugel
2021-11-19T10:47:31Z
The issue is not clear...
So, we confirm for isResolverCached
:
function isResolverCached() external view returns (bool) { bytes32[] memory requiredAddresses = resolverAddressesRequired(); for (uint256 i = 0; i < requiredAddresses.length; i++) { bytes32 name = requiredAddresses[i]; address cache_tmp = addressCache[name]; // false if our cache is invalid or if the resolver doesn't have the required address if (resolver.getAddress(name) != cache_tmp || address(0) == cache_tmp) { return false; } } return true; }
But for rebuildCache
what are the expected mitigation steps ?
🌟 Selected for report: pants
106.015 USDC - $106.02
pants
Since you are using solidity version >= 0.8.0 you have additional underflow / overflow checks that takes gas cost. Here I talk about the WETHMock.withdraw where you first require(balanceOf[msg.sender] >= wad) and then compute balanceOf[msg.sender] - wad without using unchecked{...} keyword to save gas. Using this unchecked{...} keyword can help to save gas (SLOAD) of this important function.
This issue also appears at WETHMock.transferFrom, lines 75 and 78.
#0 - adrien-supizet
2021-11-19T10:14:23Z
Gas optimizations for mock contracts are pointless, this kind of contract is only used for local tests as shown in the project.
#1 - alcueca
2021-12-03T15:47:18Z
The wardens are expected to audit everything that has been submitted by the sponsor, and not make assumptions of what is in and out of scope, even if seemingly obvious.