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: 52/54
Findings: 1
Award: $5.43
🌟 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
Issue | |
---|---|
QA-01 | setUri() Should Emit An Event |
QA-02 | Clearly Add Comment to Instances of Magic Numbers |
QA-03 | Consider Making ERC20ProxyDelegator Upgradeable |
QA-04 | Descriptive Naming Conventions Should be Adopted |
QA-05 | _delegateMulti() Should be Made More Effective |
QA-06 | Functions Should Always be Documented |
QA-07 | Fix Missing Input Validations |
QA-08 | Static Salt Value for Proxy Deployment |
QA-09 | _processDelegation() , _reimburse() & createProxyDelegatorAndTransfer() Should Emit Events |
QA-10 | Using Multiple Loops |
QA-11 | Consider Adding Circuit Breakers |
QA-12 | Assembly Codes Should Always be Accompanied With Comments |
setUri()
Should Emit An EventDifficult to track changes, and users wouldn't know what uri
this points to without checking it out which might be dangerous cause if a malicious link is set, users must click this to find out.
Take a look at setUri()
function setUri(string memory uri) external onlyOwner { _setURI(uri); } //which internally calls thia: function _setURI(string memory newuri) internal virtual { _uri = newuri; }
As seen when the URI is changed using setUri
, no event is emitted.
Emit an event indicating the change in URI in the setUri
function.
Low/info, difficulty in understanding the significance of certain values.
Take a look at retrieveProxyContractAddress()
function retrieveProxyContractAddress( ERC20Votes _token, address _delegate ) private view returns (address) { bytes memory bytecode = abi.encodePacked( type(ERC20ProxyDelegator).creationCode, abi.encode(_token, _delegate) ); bytes32 hash = keccak256( abi.encodePacked( bytes1(0xff), address(this), uint256(0), // salt keccak256(bytecode) ) ); return address(uint160(uint256(hash))); }
As seen 0xff
is used without context.
Provide clear comments or use named constants for magic numbers.
ERC20ProxyDelegator
UpgradeableLow/info, most proxies as the name suggest are upgradeable, so as to add in new logics or address issues after deployment but that's currently not posssible.
Once the ERC20ProxyDelegator
is deployed, there's no way to upgrade or modify it.
Consider using a proxy pattern that allows for upgrades.
Low, info Can lead to developer confusion.
As an example take a look at _reimburse()
function _reimburse(address source, uint256 amount) internal { address proxyAddressFrom = retrieveProxyContractAddress(token, source); token.transferFrom(proxyAddressFrom, msg.sender, amount); }
As seen, function names _reimburse
like might be misinterpreted. A more explicit name like _reimburseToSender
could be clearer.
Rename functions to more descriptive variants.
_delegateMulti()
Should be Made More EffectiveLow, idea for refactoring code for better structure.
The loop condition inside the _delegateMulti
function is potentially inefficient due to a redundant check. This can result in suboptimal gas usage during execution, which can lead to increased transaction costs.
In the loop iteration:
for ( uint transferIndex = 0; transferIndex < Math.max(sourcesLength, targetsLength); transferIndex++ )
The call to Math.max(sourcesLength, targetsLength)
is unnecessary because, due to a preceding validation check:
require( Math.max(sourcesLength, targetsLength) == amountsLength, "Delegate: The number of amounts must be equal to the greater of the number of sources or targets" );
it is guaranteed that Math.max(sourcesLength, targetsLength)
is always equal to amountsLength
.
So the use of Math.max(sourcesLength, targetsLength)
in a loop condition can be replaced by amountsLength
due to the check Math.max(sourcesLength, targetsLength) == amountsLength
.
Replace the loop condition:
transferIndex < Math.max(sourcesLength, targetsLength);
with:
transferIndex < amountsLength;
This change will optimize the loop condition, potentially saving gas during execution.
Low.. info on how to better structure of code to help stop potential misunderstanding of functions.
Unlike _reimburse
, functions like createProxyDelegatorAndTransfer()
, setUri()
, retrieveProxyContractAddress()
getBalanceForDelegate()
, deployProxyDelegatorIfNeeded()
, transferBetweenDelegators()
don't have associated natspec comments.
Add detailed natspec comments for every function.
Potential unexpected behavior.
Inputs for the functions are not thoroughly validated. For instance, the _delegateMulti
does not prevent an address from appearing in both sources
and targets
, where as the inherited contract's functions would revert the transaction's execution, users would not know what's wrong and lead them to have a bad UI/UX experience and frustrations
Introduce strict validation of inputs to functions, ensuring no overlaps or inconsistencies.
Low.. In general having a static salt causes a transaction to revert when it's been called in a tx before another call to it, which is why it could be a painless thing for an attacker to always front run a call to use a static salt and cause a bad UI/UX experience for users, this also limits flexibility and can cause issues.
In deployProxyDelegatorIfNeeded
:
new ERC20ProxyDelegator{salt: 0}(token, delegate);
Looking at retrieveProxyContractAddress()
function retrieveProxyContractAddress( ERC20Votes _token, address _delegate ) private view returns (address) { bytes memory bytecode = abi.encodePacked( type(ERC20ProxyDelegator).creationCode, abi.encode(_token, _delegate) ); bytes32 hash = keccak256( abi.encodePacked( bytes1(0xff), address(this), uint256(0), // salt keccak256(bytecode) ) ); return address(uint160(uint256(hash))); }
Both instances have the salt hardcoded and being statically set to 0
.
Allow dynamic salt values or use unique values derived from delegate addresses, i.e the idea should be to use an ever increasing value for the nonce
_processDelegation()
, _reimburse()
& createProxyDelegatorAndTransfer()
Should Emit EventsLow, current implementation makes direct tracking token movements harder.
No event is emitted in all three functions: _processDelegation()
, _reimburse()
& createProxyDelegatorAndTransfer()
after tokens are transferred.
Do note that whereas these functions are called from the delegateMulti()
function delegateMulti() external { _delegateMulti(sources, targets, amounts); } function _delegateMulti() 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], ""); } }
Add a suitable event and emit it after the token transfer in these functions
Low
Take a look at _delegateMulti()
function _delegateMulti() 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], ""); } }
As seen, multiple loops are used in _delegateMulti
to handle different conditions. This can be gas inefficient.
Consider a more streamlined approach to reduce the number of loops and checks.
Info, currently if an issue arises, there's no way to pause contract interactions.
Going through the ERC20MultiDelegate.sol
contract, we can see that circuit breakers & pause functionalities are absent.
Consider adding a Pausable
functionality from OpenZeppelin to allow for emergency stops, though if this would be implemented could lead to more centralization risks, but could come in handy.
Difficulty in understanding and verifying assembly code.
Take a look at `
function deployProxyDelegatorIfNeeded( address delegate ) internal returns (address) { address proxyAddress = retrieveProxyContractAddress(token, delegate); // check if the proxy contract has already been deployed uint bytecodeSize; assembly { bytecodeSize := extcodesize(proxyAddress) } // if the proxy contract has not been deployed, deploy it if (bytecodeSize == 0) { new ERC20ProxyDelegator{salt: 0}(token, delegate); emit ProxyDeployed(delegate, proxyAddress); } return proxyAddress; }
Assembly is used in deployProxyDelegatorIfNeeded
without comprehensive comments.
Always accompany inline assembly with comments explaining the logic and intent.
#0 - c4-pre-sort
2023-10-13T12:29:24Z
141345 marked the issue as sufficient quality report
#1 - c4-judge
2023-10-24T15:49:37Z
hansfriese marked the issue as grade-b