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: 19/54
Findings: 1
Award: $85.67
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
I'll try to give overall analysis about the project and codebase in scope.
To understand contract or codebase in scope we need to know about the general view of the protocol
Here it was the general intro about what ENS is.
I'll try to give a short introduction about ERC20MultiDelegate
(Current codebase in scope).
ERC20MultiDelegate
(Codebase in scope)The current Codebase in scope contains ERC20MultiDelegate
contract:
ERC20MultiDelegate contract manages the delegation process efficiently and securely. Together, they enable token holders to delegate tokens to multiple delegates, handle transfers, create proxy contracts, and ensure that unused tokens are returned to the delegators.
The current codebase contains four contracts but there is only one contract in scope for this audit so I'll try to explain its functionality and process flow of this contract.
Current Codebase in scope contains contract that introduces two main components:
ERC20MultiDelegate: This contract extends ERC1155 and Ownable contracts. It allows delegators to split their tokens among multiple delegates. Delegators can specify various source delegates, target delegates, and corresponding amounts to transfer. The contract ensures secure token transfers and emits events (ProxyDeployed
and DelegationProcessed
) for tracking these transactions. Delegation transfers can be initiated by calling the delegateMulti
function.
ERC20ProxyDelegator: This contract is a child contract deployed by ERC20MultiDelegate. It acts as a proxy delegator to vote on behalf of the original delegator. It is constructed with an ERC20Votes token and the target delegate address, allowing efficient delegation of voting power.
I find contract structure very interesting and will try to break it down in minimalist words to make this codebase understandable.
Certainly! Let's break down the workflow of the delegateMulti
and _delegateMulti
functions and explain their purpose step by step:
delegateMulti
Function:Function Signature and Parameters:
function delegateMulti( uint256[] calldata sources, uint256[] calldata targets, uint256[] calldata amounts ) external {
The delegateMulti
function takes three arrays as input parameters: sources
, targets
, and amounts
. These arrays represent the source delegates, target delegates, and corresponding amounts of tokens to be transferred between them, respectively. The function is declared as external
, allowing it to be called from outside the contract.
Delegation Validation:
require( sources.length > 0 || targets.length > 0, "Delegate: You should provide at least one source or one target delegate" ); require( Math.max(sources.length, targets.length) == amounts.length, "Delegate: The number of amounts must be equal to the greater of the number of sources or targets" );
The function performs validation checks to ensure that valid delegation data is provided. It requires at least one source or one target delegate to be provided, and it checks that the number of amounts corresponds to the maximum of the number of sources or targets.
Delegation Process Loop:
for (uint transferIndex = 0; transferIndex < Math.max(sources.length, targets.length); transferIndex++) {
The function enters a loop that iterates through the provided source and target delegates.
Processing Delegation Transfers:
if (transferIndex < Math.min(sources.length, targets.length)) { _processDelegation(source, target, amount); } else if (transferIndex < sources.length) { _reimburse(source, amount); } else if (transferIndex < targets.length) { createProxyDelegatorAndTransfer(target, amount); }
transferIndex
is within the range of valid source and target delegates, _processDelegation
function is called to transfer tokens between the current source and target delegate pair.transferIndex
is within the range of only sources, any remaining source amounts after the transfer process are handled by calling the _reimburse
function.transferIndex
is within the range of only targets, any remaining target amounts after the transfer process are handled by creating a proxy delegator contract and transferring tokens using createProxyDelegatorAndTransfer
function.Finalizing the Delegation Process:
if (sources.length > 0) { _burnBatch(msg.sender, sources, amounts[:sources.length]); } if (targets.length > 0) { _mintBatch(msg.sender, targets, amounts[:targets.length], ""); }
After processing all delegation transfers, the function finalizes the process by burning the token amounts from the sender for the source delegates and minting the token amounts to the sender for the target delegates. This ensures that the correct token balances are adjusted based on the delegation transfers.
_delegateMulti
Function (internal):The _delegateMulti
function is an internal helper function that performs similar actions to the delegateMulti
function. It is used internally within the contract to handle the logic of delegation transfers. The primary purpose of splitting the logic into an external and an internal function is to maintain a clean and modular code structure, allowing for easier testing and readability.
delegatemulti
functions enable the contract to efficiently handle multiple delegation transfers, ensuring that tokens are transferred correctly between source and target delegates while maintaining proper accounting of token balances.
_processDelegation
Function:This internal function is responsible for processing the delegation transfer between a source delegate and a target delegate. Here's a step-by-step explanation of its workflow:
Input Parameters:
function _processDelegation( address source, address target, uint256 amount ) internal {
The function takes three parameters: source
(the source delegate from which tokens are being withdrawn), target
(the target delegate to which tokens are being transferred), and amount
(the amount of tokens to be transferred between the source and target delegates).
Check Delegate Balance:
uint256 balance = getBalanceForDelegate(source); assert(amount <= balance);
The function first checks the balance of the source delegate to ensure it has sufficient tokens (balance >= amount
) for the delegation transfer. The getBalanceForDelegate
function is called to retrieve the balance of the source delegate. If the source delegate does not have enough balance, the assert
statement will revert the transaction, ensuring the function fails gracefully if the condition is not met.
Deploy Proxy Delegator (if needed):
deployProxyDelegatorIfNeeded(target);
The function calls the deployProxyDelegatorIfNeeded
internal function, which ensures that a proxy delegator contract is deployed for the target delegate if it hasn't been deployed already. Proxy delegators are used to facilitate voting or other actions on behalf of the delegators in a more gas-efficient manner.
Transfer Tokens Between Delegators:
transferBetweenDelegators(source, target, amount);
The function then calls the transferBetweenDelegators
internal function, which transfers the specified amount
of tokens from the source delegate to the target delegate. This function handles the actual token transfer between the delegators.
Emit Event:
emit DelegationProcessed(source, target, amount);
Function emits a DelegationProcessed
event. Events are useful for tracking and logging significant state changes on the blockchain. In this case, the event indicates that a delegation transfer has been successfully processed between the specified source and target delegates with the specified amount of tokens.
The _processDelegation
function ensures the validity of the delegation transfer by checking the source delegate's balance, deploys a proxy delegator for the target delegate if necessary, transfers tokens between the source and target delegates, and emits an event to log the processed delegation transfer.
_reimburse
Function:This internal function is responsible for reimbursing any remaining token amounts back to the delegator after the delegation transfer process. Here's a step-by-step explanation of its workflow:
Input Parameters:
function _reimburse(address source, uint256 amount) internal {
The function takes two parameters: source
(the source delegate from which tokens are being withdrawn) and amount
(the amount of tokens to be withdrawn from the source delegate).
Retrieve Proxy Contract Address:
address proxyAddressFrom = retrieveProxyContractAddress(token, source);
The function calls the retrieveProxyContractAddress
internal function to obtain the proxy contract address associated with the specified source
delegate. Proxy contracts are used to efficiently manage token transfers and voting processes. The retrieved proxyAddressFrom
represents the source delegate's proxy contract address.
Transfer Tokens to Delegator:
token.transferFrom(proxyAddressFrom, msg.sender, amount);
Using the transferFrom
function of the token
(an ERC20Votes contract), the function transfers the specified amount
of tokens from the proxyAddressFrom
(source delegate's proxy contract) to the msg.sender
. Here, msg.sender
represents the delegator who initiated the reimbursement.
This step ensures that any remaining tokens from the source delegate are returned to the delegator after the delegation transfer process. If there are no remaining tokens (amount
equals the remaining balance), the full source amount is transferred back to the delegator.
The _reimburse
function allows the contract to handle the return of remaining tokens from a source delegate back to the delegator. It ensures that the delegator receives any remaining tokens that were not transferred during the delegation process, maintaining accurate token balances for both the delegator and the source delegate.
setUri
Function:function setUri(string memory uri) external onlyOwner { _setURI(uri); }
Purpose:
The setUri
function allows the owner of the contract to set the metadata URI associated with the ERC1155 tokens. Metadata URI provides a way to fetch additional off-chain information about the tokens, such as their images, descriptions, or other attributes.
Workflow:
Access Control:
external
, meaning it can be called from outside the contract.onlyOwner
modifier ensures that only the owner of the contract (the one who deployed it) can invoke this function.Setting Metadata URI:
_setURI
internal function, passing the provided uri
parameter. _setURI
is likely a function defined elsewhere in the contract, responsible for setting the metadata URI for the ERC1155 tokens.createProxyDelegatorAndTransfer
Function:function createProxyDelegatorAndTransfer( address target, uint256 amount ) internal { address proxyAddress = deployProxyDelegatorIfNeeded(target); token.transferFrom(msg.sender, proxyAddress, amount); }
Purpose:
The createProxyDelegatorAndTransfer
function is responsible for creating a proxy delegator contract for a target delegate (if necessary) and transferring a specified amount of tokens to that proxy delegator.
Workflow:
Deploy Proxy Delegator:
deployProxyDelegatorIfNeeded(target)
to check if a proxy delegator contract already exists for the specified target
delegate. If not, it deploys a new proxy delegator contract for the target
delegate.Token Transfer:
amount
tokens from the sender (msg.sender
) to the proxyAddress
, which represents the proxy delegator contract created for the target
delegate.transferBetweenDelegators
Function:function transferBetweenDelegators( address from, address to, uint256 amount ) internal { address proxyAddressFrom = retrieveProxyContractAddress(token, from); address proxyAddressTo = retrieveProxyContractAddress(token, to); token.transferFrom(proxyAddressFrom, proxyAddressTo, amount); }
Purpose:
The transferBetweenDelegators
function facilitates the transfer of tokens between two proxy delegator contracts. It ensures that tokens are moved from the from
proxy to the to
proxy.
Workflow:
Proxy Address Retrieval:
from
and to
delegates using the retrieveProxyContractAddress
internal function.Token Transfer:
amount
tokens from the proxyAddressFrom
(proxy of the from
delegate) to the proxyAddressTo
(proxy of the to
delegate).These functions work together to manage the delegation process efficiently. setUri
allows the owner to set metadata URI, createProxyDelegatorAndTransfer
creates proxy delegators and transfers tokens to them, and transferBetweenDelegators
moves tokens between proxy delegator contracts, ensuring smooth token delegation and management.
deployProxyDelegatorIfNeeded
Function: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; }
Purpose:
The deployProxyDelegatorIfNeeded
function is responsible for deploying a proxy delegator contract for a specified delegate if it hasn't been deployed already. Proxy delegators are used to handle token delegation efficiently.
Workflow:
Proxy Contract Retrieval:
delegate
by calling retrieveProxyContractAddress(token, delegate)
.Check Proxy Contract Existence:
proxyAddress
contract already has code deployed (extcodesize(proxyAddress)
). If the bytecodeSize
is 0, it means the contract does not exist.Deploy Proxy Contract:
ERC20ProxyDelegator
contract is deployed with the specified token
and delegate
parameters.salt: 0
parameter ensures that each deployment creates a unique contract instance.emit
statement logs the deployment event with the delegate
and proxyAddress
details.Return Proxy Address:
proxyAddress
, whether it was newly deployed or already existed. This address can be used for subsequent token transfers and delegations.getBalanceForDelegate
Function:function getBalanceForDelegate( address delegate ) internal view returns (uint256) { return ERC1155(this).balanceOf(msg.sender, uint256(uint160(delegate))); }
Purpose:
The getBalanceForDelegate
function is used to retrieve the balance of a specific delegate for the ERC1155 tokens.
Workflow:
Balance Retrieval:
balanceOf
function of the ERC1155 contract (this contract) to retrieve the balance of the msg.sender
(caller) for the specified delegate
.uint256(uint160(delegate))
conversion is likely a way to convert the delegate
address into a format suitable for ERC1155 token IDs.Return Balance:
delegate
for the caller. This balance represents the number of tokens the caller has for the specified delegate.retrieveProxyContractAddress
Function: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))); }
Purpose:
The retrieveProxyContractAddress
function generates the address of a proxy contract for a specified _delegate
and _token
. It follows a specific calculation pattern to ensure each proxy contract has a unique address.
Workflow:
Creation Code Encoding:
ERC20ProxyDelegator
contract along with the provided _token
and _delegate
parameters._token
, and _delegate
information.Address Calculation:
address(this)
), and a salt value of 0
. The salt value is added to ensure unique contract addresses for each deployment.bytes32
hash.Conversion and Return:
bytes32
hash to uint256
and then to address
using the uint160(uint256(hash))
method._token
and _delegate
.These functions work together to manage the deployment and retrieval of proxy delegator contracts, ensuring each delegate has a distinct proxy for efficient token delegation and management.
Hardhat Testing
Codebase Quality Analysis Report
Overall Rating: 50%
**1. Documentation Analysis:
**2. Fuzzing Analysis:
**3. Invariant Testing Analysis:
The setUri
function in the provided code is exposed to centralization risk due to its reliance on the onlyOwner
modifier. Let's break down why this function poses a centralization risk:
function setUri(string memory uri) external onlyOwner { _setURI(uri); }
Explanation:
Access Restriction: The setUri
function is marked as external
, meaning it can be called from outside the contract by anyone. However, the function is protected by the onlyOwner
modifier, restricting its use to the owner of the contract.
Ownership Control: The onlyOwner
modifier restricts the function to be executed only by the owner of the contract, as defined in the Ownable
contract (presumably used in the contract's code, although it's not explicitly shown in the provided snippet).
Centralization Risk:
The centralization risk arises from the fact that the ability to change the URI (Uniform Resource Identifier) of the contract's metadata is solely controlled by the owner.
1-3 Hours : Overview of Codebase 3-5 Hours : Understanding concepts of Core codebase 5-7 Hours : Reading Test and Finding Weak Spots 7-10 Hours : Writing Analysis
Total Time Spent : 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-22T16:24:52Z
hansfriese marked the issue as grade-a