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: 1/27
Findings: 7
Award: $11,095.85
🌟 Selected for report: 14
🚀 Solo Findings: 2
GreyArt
It is possible for duplicate shareholders to be added. These shareholders will get more than intended when _sendFee()
is called.
Ensure that the _accounts
array is sorted in setShareholders()
.
for (uint256 i = 0; i < _accounts.length; i++) { if (i > 0) { require(_accounts[i - 1] < _accounts[i], "FeeSplitter: ACCOUNTS_NOT_SORTED"); } _addShareholder(_accounts[i], _weights[i]); }
#0 - adrien-supizet
2021-11-22T10:01:05Z
Duplicate #231
#1 - adrien-supizet
2021-11-22T10:02:08Z
Indeed there is a fix to do here, we'll prevent adding the same shareholders instead as suggested in #231
#2 - alcueca
2021-12-03T09:35:38Z
Taking this issue as the principal, and raising #231 to medium severity.
🌟 Selected for report: GreyArt
2599.8256 USDC - $2,599.83
GreyArt
A user that mistakenly calls either create()
or addToken()
with WETH (or another ERC20) as the input token, but includes native ETH with the function call will have his native ETH permanently locked in the contract.
It is best to ensure that msg.value = 0
in _transferInputTokens()
for the scenario mentioned above.
} else if (address(_inputToken) == ETH) { ... } else { require(msg.value == 0, "NestedFactory::_transferInputTokens: ETH sent for non-ETH transfer"); _inputToken.safeTransferFrom(_msgSender(), address(this), _inputTokenAmount); }
🌟 Selected for report: GreyArt
2599.8256 USDC - $2,599.83
GreyArt
There is no limit to the number of shareholders. It is therefore possible to set a large number of shareholders such that _sendFees()
will run out of gas when adding shares to each shareholder.
This will cause denial of service to all NestedFactory functions, especially the ones that will remove funds like withdraw()
and destroy()
.
It would be best to set a sanity maximum number of shareholders that can be added.
GreyArt
While there is no loss of funds, removing an operator will cause the cache functionality to be permanently broken. If there was a function that had a modifier which requires the cache to be synced before the function can be called, it would not be callable as well.
The underlying issue is how the bytes32
operator is removed from the array when removeOperator()
is called. Its value gets deleted (set to 0x0), but isn't taken out of the array.
For ease of reading, we use abbreviated strings like 0xA
, 0xB
for bytes32
and address
types.
OperatorResolver.importOperators([0xA, 0xB, 0xC], [0x1, 0x2, 0x3)
.NestedFactory.addOperator()
3 times to push these 3 operators into the operators
state variable.NestedFactory.rebuildCache()
to build the cache.0xB
is to be removed. Taking reference from the removeOperator.ts
script, OperatorResolver.importOperators([0xA, 0xB, 0xC], [0x1, 0x2, 0x3)
is called first. This works because OperatorResolver uses a mapping(bytes32 ⇒ address) to represent the operators. Hence, by setting 0xB
's destination address to be the null address, it is like as if he was never an operator.NestedFactory.rebuildCache()
to rebuild the cache. resolverAddressesRequired()
will return [0xA, 0xB, 0xC]
. 0xB
will be removed from addressCache
because resolver.getAddress(0xB)
returns 0x000 since it has been deleted from the OperatorResolver.NestedFactory.removeOperator(0xB)
. The operators
array now looks like this: [0xA, 0x0, 0xC]
.NestedFactory.isResolverCached
, it will always return false because of the null bytes32
value, where addressCache[0x0]
will always return the null address.Instead of doing an element deletion, it should be replaced with the last element, then have the last element popped in removeOperator()
.
function removeOperator(bytes32 operator) external override onlyOwner { for (uint256 i = 0; i < operators.length; i++) { if (operators[i] == operator) { operators[i] = operators[operators.length - 1]; operators.pop(); break; } } }
#0 - maximebrugel
2021-11-22T10:29:16Z
Duplicated : #58
#1 - alcueca
2021-12-03T11:10:10Z
Taking this issue apart as a non-duplicate, for finding the most severe consequence of the incorrect implementation.
GreyArt
Currently, many core operations (like NestedFactory.create(), NestedFactory.swapTokenForTokens()) are dependent on the assumption that the cache is synced before these functions are executed however this may not necessarily be the case.
This flow is not aware that the cache is not in synced.
Add a modifier to require that the cache is synced to all functions that interact with the operators.
#0 - maximebrugel
2021-11-22T21:45:03Z
Duplicated : #157
#1 - alcueca
2021-12-03T10:22:31Z
Taking this as the main.
🌟 Selected for report: GreyArt
866.6085 USDC - $866.61
GreyArt
The view functions have the comment use ETH_ADDR for ETH
. However, native ETH isn't supported by the FeeSplitter except for releaseETH()
. Even so, releaseETH()
uses WETH as the token address, not the ETH constant, as it will unwrap WETH to ETH.
This gives the wrong impression to readers of the contract that ETH is supported when it is in fact, not.
Remove the comment use ETH_ADDR for ETH
wherever it is mentioned.
#0 - maximebrugel
2021-11-22T09:44:39Z
This is a wrong comment, should be "Non-critical".
#1 - alcueca
2021-12-03T16:04:17Z
Issues with comments are severity 1.
GreyArt
You might have double entries of the same operator. This doesn't break any logic at the moment.
Use a mapping to check if an operator already exists in the array before pushing onto the array. This mapping should be bytes32⇒uint where uint represents the index in the array.
The index of the operator should be incremented by 1 before updating the mapping and decremented by 1 before trying to query the element in the array. This offset will help to handle the case where uint defaults to 0.
#0 - maximebrugel
2021-11-19T16:51:56Z
Duplicated : #180
🌟 Selected for report: GreyArt
106.015 USDC - $106.02
GreyArt
The importOperators()
declares the name
and destination
variables multiple times. It'll be cheaper to declare them once outside the loop and reuse the variables inside.
bytes32 name; address destination; for (uint256 i = 0; i < names.length; i++) { name = names[i]; destination = destinations[i]; operators[name] = destination; emit OperatorImported(name, destination); }
#0 - maximebrugel
2021-11-19T15:03:18Z
🌟 Selected for report: GreyArt
106.015 USDC - $106.02
GreyArt
It is unnecessary to store the token
variable in the Holding
struct because the token is used as the key to access the Holding
struct.
Remove the token
variable in the Holding
struct.
/// @dev Info about assets stored in reserves struct Holding { uint256 amount; bool isActive; }
GreyArt
The initial factory address can be set to null in the constructor method. Since updateFactory()
checks that the new factory address to be set isn't null, this same check should also exist in the constructor method.
constructor(address _factory) { require(_factory != address(0), "NestedReserve: INVALID_ADDRESS"); factory = _factory; }
#0 - maximebrugel
2021-11-19T17:20:15Z
Duplicated : #178
🌟 Selected for report: GreyArt
106.015 USDC - $106.02
GreyArt
The name
and destination
local variables in the rebuildCache
function are declared multiple times within the loop. It'll be cheaper to declare them once outside the loop and reuse the variables inside.
bytes32 name; address destination; // The resolver must call this function whenever it updates its state for (uint256 i = 0; i < requiredAddresses.length; i++) { name = requiredAddresses[i]; // Note: can only be invoked once the resolver has all the targets needed added destination = resolver.getAddress(name); ... }
🌟 Selected for report: GreyArt
106.015 USDC - $106.02
GreyArt
createRecord()
is only invoked by store()
. Its visibility can therefore be made internal / private.
function createRecord( uint256 _nftId, address _token, uint256 _amount, address _reserve ) internal onlyFactory {...}
#0 - maximebrugel
2021-11-19T16:28:47Z
Will make createRecord
internal (or move inside store
) and remove redundant checks
🌟 Selected for report: GreyArt
GreyArt
createRecord()
is only invoked by store()
, whose logic guarantees that holding.isActive
is false when it is called. Hence, the check
require(!holding.isActive, "NestedRecords: HOLDING_EXISTS");
is redundant in createRecord()
.
The following lines can be removed
Holding memory holding = records[_nftId].holdings[_token]; require(!holding.isActive, "NestedRecords: HOLDING_EXISTS");
#0 - maximebrugel
2021-11-19T16:29:37Z
Duplicated : #124
#1 - alcueca
2021-12-03T10:24:43Z
Not a duplicate
🌟 Selected for report: GreyArt
106.015 USDC - $106.02
GreyArt
_submitOutOrders()
is invoked by 2 functions sellTokensToNft()
and sellTokensToWallet()
, both of which specify the _fromReserve
parameter to be true
. This parameter is therefore unneeded.
function _submitOutOrders( uint256 _nftId, IERC20 _outputToken, uint256[] memory _inputTokenAmounts, Order[] calldata _orders, bool _reserved ) private returns (uint256 feesAmount, uint256 amountBought) { ... IERC20 _inputToken = _transferInputTokens( _nftId, IERC20(_orders[i].token), _inputTokenAmounts[i], true ); }
#0 - maximebrugel
2021-11-23T16:17:26Z
The parameter was added in the case of _submitOutOrders
from a wallet (not the portfolio/reserve) to set the value to false
. Since we are not allowing to do this, we can remove it.
However, it is not a bad idea to include a deposit
function to add multiple tokens.
#1 - maximebrugel
2021-12-14T14:26:09Z
Same in _submitInOrders
with _reserved
47.7068 USDC - $47.71
GreyArt
require(_accountIndex + 1 <= shareholders.length, "FeeSplitter: INVALID_ACCOUNT_INDEX");
can be simplified to
require(_accountIndex < shareholders.length, "FeeSplitter: INVALID_ACCOUNT_INDEX");
#0 - maximebrugel
2021-11-19T16:33:24Z
Duplicated : #207
🌟 Selected for report: GreyArt
106.015 USDC - $106.02
GreyArt
The transferFromFactory()
function is missing the valid(address(_token))
modifier that is present in the transfer()
and withdraw()
functions.
It is in our opinion that these sanity checks on the token address are redundant, because the transaction will revert anyway in the SafeERC20 library.
Either add in the modifier check for the transferFromFactory()
function. Alternatively, remove them from all the functions as a gas optimization.
#0 - maximebrugel
2021-11-19T16:06:33Z
Remove valid
modifier but keep zero address check on _recipient
in transfer()
(we don't want to burn).
🌟 Selected for report: GreyArt
106.015 USDC - $106.02
GreyArt
createRecord()
is only invoked by store()
. The onlyFactory
check already exists in store()
, so this modifier is redundant in createRecord()
.
Remove the onlyFactory
modifier.
#0 - maximebrugel
2021-11-19T16:29:55Z
Duplicated : #124
#1 - alcueca
2021-12-03T10:25:11Z
Not a duplicate