Platform: Code4rena
Start Date: 05/10/2023
Pot Size: $33,050 USDC
Total HM: 1
Participants: 54
Period: 6 days
Judge: hansfriese
Id: 294
League: ETH
Rank: 15/54
Findings: 2
Award: $91.10
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: thekmj
Also found by: 0x3b, 33BYTEZZZ, Bauchibred, Chom, Dravee, J4X, Limbooo, Maroutis, MiloTruck, MrPotatoMagic, SBSecurity, Sathish9098, Tadev, ZanyBonzy, adam-idarrha, adriro, btk, hyh, lukejohn, nmirchev8, peakbolt, radev_sw, rvierdiiev
5.4311 USDC - $5.43
address
es may result in collisionsIf a number is downcast to an address
the upper bytes are truncated, which may mean that more than one value will map to the address
.
FILE: 2023-10-ens/contracts/ERC20MultiDelegate.sol 91: ? address(uint160(sources[transferIndex])) 94: ? address(uint160(targets[transferIndex])) 214: return address(uint160(uint256(hash)));
require()
should be used instead of assert()
assert()
is used to check for conditions that should never, ever be false. It's meant for detecting and protecting against invariants in your code. When an assert() statement evaluates to false, it indicates a bug in the code, and the entire transaction will be reverted. It does not return any gas
.
FILE: 2023-10-ens/contracts/ERC20MultiDelegate.sol 131: assert(amount <= balance);
+ require(amount <= balance , "");
_delegateMulti()
functionA default case (often represented as an else or default statement) is used to handle situations where none of the preceding conditions in a series of if or else if statements are met. It acts as a catch-all for unexpected or unhandled cases.
The absence of a default case means that if transferIndex does not match any of the conditions specified in the if and else if blocks, then no code will be executed. This could potentially lead to unexpected behavior or errors if transferIndex takes on an unexpected value.
FILE: 2023-10-ens/contracts/ERC20MultiDelegate.sol if (transferIndex < Math.min(sourcesLength, targetsLength)) { // Process the delegation transfer between the current source and target delegate pair. _processDelegation(source, target, amount); } else if (transferIndex < sourcesLength) { // Handle any remaining source amounts after the transfer process. _reimburse(source, amount); } else if (transferIndex < targetsLength) { // Handle any remaining target amounts after the transfer process. createProxyDelegatorAndTransfer(target, amount); }
Add the default case
if (transferIndex < Math.min(sourcesLength, targetsLength)) { // Process the delegation transfer between the current source and target delegate pair. _processDelegation(source, target, amount); } else if (transferIndex < sourcesLength) { // Handle any remaining source amounts after the transfer process. _reimburse(source, amount); } else if (transferIndex < targetsLength) { // Handle any remaining target amounts after the transfer process. createProxyDelegatorAndTransfer(target, amount); } + else + {// Add unsatisfactory case }
safeIncreaseAllowance
``safeDecreaseAllowance`` for approving tokens instead of unsafe approve() functionIt is generally recommended to use the safeIncreaseAllowance() and safeDecreaseAllowance() functions instead of the approve() function when approving tokens. The safeIncreaseAllowance() and safeDecreaseAllowance() functions are defined in the OpenZeppelin SafeERC20 library. This library provides a number of safe functions for interacting with ERC20 tokens, including the safeIncreaseAllowance() and safeDecreaseAllowance() functions.
FILE: 2023-10-ens/contracts/ERC20MultiDelegate.sol 17: _token.approve(msg.sender, type(uint256).max);
Even if the function follows the best practice of check-effects-interaction, not using a reentrancy guard when there may be transfer hooks will open the users of this protocol up to read-only reentrancies with no way to protect against it, except by block-listing the whole protocol.
FILE: 2023-10-ens/contracts/ERC20MultiDelegate.sol 148: token.transferFrom(proxyAddressFrom, msg.sender, amount); 160: token.transferFrom(msg.sender, proxyAddress, amount); 170: token.transferFrom(proxyAddressFrom, proxyAddressTo, amount);
deployProxyDelegatorIfNeeded(target)
function returns address not checked and not cachedThe deployProxyDelegatorIfNeeded
function doesn't need to return an address because the created proxy delegator's address is already known
.
function _processDelegation( address source, address target, uint256 amount ) internal { uint256 balance = getBalanceForDelegate(source); assert(amount <= balance); deployProxyDelegatorIfNeeded(target); transferBetweenDelegators(source, target, amount); emit DelegationProcessed(source, target, amount); } function deployProxyDelegatorIfNeeded(address delegate) internal returns (address)
#0 - 141345
2023-10-13T12:23:10Z
#1 - c4-pre-sort
2023-10-13T12:23:14Z
141345 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-24T16:15:31Z
hansfriese marked the issue as grade-b
🌟 Selected for report: polarzero
Also found by: 0x3b, 0xSmartContract, 0xnev, ABA, Bulletprime, J4X, Limbooo, MrPotatoMagic, SBSecurity, Sathish9098, asui, d3e4, hyh, ihtishamsudo, inzinko, marchev, pfapostol, radev_sw, rahul, ro1sharkm
85.6695 USDC - $85.67
This system is designed to be used with an ERC20 token
that supports delegation of votes
(such as the OpenZeppelin ERC20Votes
extension) and ERC1155 metadata
ERC1155
and Ownable
.delegate
their tokens to multiple target
delegates simultaneously.ERC20 token
(via the token variable) with which delegations
are made.proxy contracts
to facilitate delegation
.ERC20MultiDelegate
.delegate votes
on behalf of an original delegator
.ERC20 token
and the address of the delegate to whom votes are delegated
.High-level overview
: I analyzed the overall codebase
in one iteration to get a high-level understanding of the code structure
and functionality
.
Documentation review
: I studied the documentation
to understand the purpose of each contract
, its functionality
, and how it is connected
with other contracts.
Literature review
: I read old audits
and known findings
, as well as the bot races findings
.
Testing setup
: I set up my testing environment
and ran the tests to ensure that all tests passed
. Used yarn and hardhat to test this protocol.
Detailed analysis
: I started with the detailed analysis
of the code base, line by line. I took the necessary notes
to ask some questions to the sponsors
.
The _processDelegation()
function in the ERC20MultiDelegate
contract does not check if the target
delegate is a valid address
. This means that an attacker could send delegations to invalid addresses
, which could result in lost funds
impersonates
a valid target delegate
.attacker
sends a delegation
to the malicious contract from their own account
.ERC20MultiDelegate
contract transfers the delegation
to the malicious contract
.The _processDelegation()
function should be modified to check if the target delegate
is a valid
address before sending the delegation.
The _delegateMulti()
function in the ERC20MultiDelegate
contract iterates through all source and target delegates
, even if there is no delegation to be processed for some of them. This can be inefficient and waste gas, especially for large numbers of delegates.
function delegateMulti( uint256[] calldata sources, uint256[] calldata targets, uint256[] calldata amounts ) external { _delegateMulti(sources, targets, amounts); } function _delegateMulti( uint256[] calldata sources, uint256[] calldata targets, uint256[] calldata amounts ) internal { uint256 sourcesLength = sources.length; uint256 targetsLength = targets.length; uint256 amountsLength = amounts.length; require( sourcesLength > 0 || targetsLength > 0, "Delegate: You should provide at least one source or one target delegate" ); require( Math.max(sourcesLength, targetsLength) == amountsLength, "Delegate: The number of amounts must be equal to the greater of the number of sources or targets" ); // Iterate until all source and target delegates have been processed. for ( uint transferIndex = 0; transferIndex < Math.max(sourcesLength, targetsLength); transferIndex++ ) { address source = transferIndex < sourcesLength ? address(uint160(sources[transferIndex])) : address(0); address target = transferIndex < targetsLength ? address(uint160(targets[transferIndex])) : address(0); uint256 amount = amounts[transferIndex]; if (transferIndex < Math.min(sourcesLength, targetsLength)) { // Process the delegation transfer between the current source and target delegate pair. _processDelegation(source, target, amount); } else if (transferIndex < sourcesLength) { // Handle any remaining source amounts after the transfer process. _reimburse(source, amount); } else if (transferIndex < targetsLength) { // Handle any remaining target amounts after the transfer process. createProxyDelegatorAndTransfer(target, amount); } } if (sourcesLength > 0) { _burnBatch(msg.sender, sources, amounts[:sourcesLength]); } if (targetsLength > 0) { _mintBatch(msg.sender, targets, amounts[:targetsLength], ""); }
Step 1
: The _delegateMulti()
function iterates through all source and target delegates, even if there is no delegation to be processed for some of them. This is because the loop condition is transferIndex < Math.max(sourcesLength, targetsLength)
. This means that the loop will continue to iterate until all of the delegates have been processed, even if there are no delegations
to be processed for some of them.
Step 2
: For each delegate, the _delegateMulti()
function checks if there is a delegation to be processed. This is done by checking if the amount for the delegate is greater than zero. If the amount is greater than zero, then the delegation is processed.
Step 3
: If the delegation is processed, then the _delegateMulti()
function transfers the delegation from the source delegate to the target delegate. This is done by calling the transfer()
function on the ERC20 token contract.
Step 4
: The transfer()
function is an expensive operation, especially for large delegations
. This is because the transfer()
function needs to update the state of the ERC20 token contract.
Step 5
: If there is no delegation to be processed for a given delegate, then the _delegateMulti()
function skips the delegate and moves on to the next delegate.
To improve the efficiency of the _delegateMulti()
function is to use a more efficient data structure to store the delegation information
. For example, a tree structure
could be used to store the delegates
and their delegations. This would make it more efficient to search
for and update
delegation information, and it would also avoid the need to iterate
through all of the delegates in the _delegateMulti() function.
By making these changes, the _delegateMulti()
function can be made more efficient and less wasteful of gas
. This is especially important for large numbers
of delegates
and large delegations
.
The code could be better documented. This would make it easier to understand how the code works and how to use it
The code could be improved by adding better error handling. This would help to prevent errors from causing unexpected behavior or crashes. The code could check for errors when calling the transfer()
function and handle any errors that occur
The code should use a consistent coding style. This will make the code more readable and easier to maintain. The code should use consistent indentation
, spacing
, and line breaks
The code should use meaningful
variable names
. This will make the code more readable and easier to understand.Instead of using variable names like a
, b
, and c
, the code should use variable names meaningfull
The code should be broken down into smaller functions. This will make the code more modular and easier to maintain. For example, the _delegateMulti()
function could be broken down into smaller functions for each step of the delegation process
The code currently does not use any caching mechanisms
. This can lead to unnecessary performance overhead, especially for frequently accessed delegations. A caching mechanism could be used to cache the delegation information. This would improve the performance of the code, especially for frequently accessed delegations.
The setUri()
function can only be called by the owner
of the contract. This gives the owner a lot of power, as they can change the URI
of the contract at will. This could be used to maliciously redirect users
to a fake URI or to change the branding of the contract.
function setUri(string memory uri) external onlyOwner {
The fact that the setUri()
function can only be called by the owner introduces a single point of failure
. If the owner's account is compromised
, then the attacker could use the setUri()
function to perform malicious actions.
Add a timelock to the setUri()
function. This would require the owner
to wait a certain amount of time before they can change the URI
of the contract. This would give the community time
to review any proposed changes
and to object if necessary.
The ERC20MultiDelegate
contract allows delegators to delegate their tokens to multiple target
delegates simultaneously. This can be useful for delegators
who want to spread their risk or who want to vote for multiple delegates
. However, it also introduces the possibility of delegation attacks
. An attacker could create a malicious proxy delegator contract that impersonates a legitimate delegate. If delegators delegate their tokens to this malicious contract, the attacker could steal their funds.
The ERC20MultiDelegate
and ERC20ProxyDelegator
contracts are complex smart contracts. As such, they are susceptible to smart contract vulnerabilities
, such as reentrancy attacks
and integer overflows
. If a vulnerability is discovered in one of these contracts, it could be exploited to steal funds from the contract or to disrupt its operation.
The contract inherits from ERC1155 and expects a metadata URI for token metadata. Ensure that the provided URI is accessible and that metadata is set correctly.
The method used to verify whether a proxy contract has already been deployed is not foolproof and could result in false positives or negatives.
There are a number of other potential risks associated with using the ERC20MultiDelegate and ERC20ProxyDelegator contracts. For example, the contracts are still under development and may contain bugs. Additionally, the contracts rely on a number of external dependencies, such as the ERC20 token contract. If there is a problem with one of these external dependencies, it could affect the operation of the ERC20MultiDelegate and ERC20ProxyDelegator contracts.
Simplify the logic for retrieving proxy contract addresses. Using assembly might make the code harder to read and maintain.
Keep an eye on updates to the OpenZeppelin libraries and ensure compatibility with the chosen versions.
If you are using a floating version of Solidity, such as ^0.8.2, this means that your codebase will be compatible with all versions of Solidity from ^0.8.0
to the latest version. This can be useful if you are not sure which version of Solidity you want to use, or if you need to support multiple versions of Solidity.
Overall, I would recommend using the latest version
like 0.8.20
or 0.8.21
of Solidity if possible.
Comments can be very helpful for both auditors and developers, especially in complex codebases. I also agree that the codebase you are describing is generally clean and strategically straightforward, but there are a few sections that could benefit from additional comments.
ERC20MultiDelegate
, serves as a utility for token delegation in a decentralized governance system. It allows delegators to delegate their tokens to multiple target delegates efficiently using proxy contracts.
10 hours
10 hours
#0 - c4-pre-sort
2023-10-14T08:44:04Z
141345 marked the issue as sufficient quality report
#1 - c4-judge
2023-10-24T16:43:07Z
hansfriese marked the issue as grade-a