GithubHelp home page GithubHelp logo

2023-10-ens-findings's Introduction

ENS Audit

Unless otherwise discussed, this repo will be made public after audit completion, sponsor review, judging, and issue mitigation window.

Contributors to this repo: prior to report publication, please review the Agreements & Disclosures issue.


Audit findings are submitted to this repo

Sponsors have three critical tasks in the audit process:

  1. Respond to issues.
  2. Weigh in on severity.
  3. Share your mitigation of findings.

Let's walk through each of these.

High and Medium Risk Issues

Wardens submit issues without seeing each other's submissions, so keep in mind that there will always be findings that are duplicates. For all issues labeled 3 (High Risk) or 2 (Medium Risk), these have been pre-sorted for you so that there is only one primary issue open per unique finding. All duplicates have been labeled duplicate, linked to a primary issue, and closed.

Judges have the ultimate discretion in determining validity and severity of issues, as well as whether/how issues are considered duplicates. However, sponsor input is a significant criterion.

Respond to issues

For each High or Medium risk finding that appears in the dropdown at the top of the chrome extension, please label as one of these:

  • sponsor confirmed, meaning: "Yes, this is a problem and we intend to fix it."
  • sponsor disputed, meaning either: "We cannot duplicate this issue" or "We disagree that this is an issue at all."
  • sponsor acknowledged, meaning: "Yes, technically the issue is correct, but we are not going to resolve it for xyz reasons."

Add any necessary comments explaining your rationale for your evaluation of the issue.

Note that when the repo is public, after all issues are mitigated, wardens will read these comments; they may also be included in your C4 audit report.

Weigh in on severity

If you believe a finding is technically correct but disagree with the listed severity, select the disagree with severity option, along with a comment indicating your reasoning for the judge to review. You may also add questions for the judge in the comments. (Note: even if you disagree with severity, please still choose one of the sponsor confirmed or sponsor acknowledged options as well.)

For a detailed breakdown of severity criteria and how to estimate risk, please refer to the judging criteria in our documentation.

QA reports, Gas reports, and Analyses

All warden submissions in these three categories are submitted as bulk listings of issues and recommendations:

  • QA reports include all low severity and non-critical findings from an individual warden.
  • Gas reports include all gas optimization recommendations from an individual warden.
  • Analyses contain high-level advice and review of the code: the "forest" to individual findings' "trees.”

For QA reports, Gas reports, and Analyses, sponsors are not required to weigh in on severity or risk level. We ask that sponsors:

  • Leave a comment for the judge on any reports you consider to be particularly high quality. (These reports will be awarded on a curve.)
  • For QA and Gas reports only: add the sponsor disputed label to any reports that you think should be completely disregarded by the judge, i.e. the report contains no valid findings at all.

Once labelling is complete

When you have finished labelling findings, drop the C4 team a note in your private Discord backroom channel and let us know you've completed the sponsor review process. At this point, we will pass the repo over to the judge to review your feedback while you work on mitigations.

Share your mitigation of findings

Note: this section does not need to be completed in order to finalize judging. You can continue work on mitigations while the judge finalizes their decisions and even beyond that. Ultimately we won't publish the final audit report until you give us the OK.

For each finding you have confirmed, you will want to mitigate the issue before the contest report is made public.

If you are planning a Code4rena mitigation review:

  1. In your own Github repo, create a branch based off of the commit you used for your Code4rena audit, then
  2. Create a separate Pull Request for each High or Medium risk C4 audit finding (e.g. one PR for finding H-01, another for H-02, etc.)
  3. Link the PR to the issue that it resolves within your contest findings repo.

Most C4 mitigation reviews focus exclusively on reviewing mitigations of High and Medium risk findings. Therefore, QA and Gas mitigations should be done in a separate branch. If you want your mitigation review to include QA or Gas-related PRs, please reach out to C4 staff and let’s chat!

If several findings are inextricably related (e.g. two potential exploits of the same underlying issue, etc.), you may create a single PR for the related findings.

If you aren’t planning a mitigation review

  1. Within a repo in your own GitHub organization, create a pull request for each finding.
  2. Link the PR to the issue that it resolves within your contest findings repo.

This will allow for complete transparency in showing the work of mitigating the issues found in the contest. If the issue in question has duplicates, please link to your PR from the open/primary issue.

2023-10-ens-findings's People

Contributors

c4-submissions avatar c4-bot-3 avatar c4-bot-6 avatar c4-bot-7 avatar c4-bot-2 avatar c4-bot-4 avatar c4-bot-5 avatar c4-bot-8 avatar c4-bot-1 avatar c4-bot-9 avatar c4-bot-10 avatar c4-judge avatar code423n4 avatar itsmetechjay avatar

Stargazers

krgko avatar  avatar  avatar 0xconstructor avatar  avatar  avatar  avatar Scooby avatar

Watchers

Ashok avatar  avatar

2023-10-ens-findings's Issues

.max Token Approval in ERC20ProxyDelegator

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L17

Vulnerability details

Impact

The ERC20ProxyDelegator contract approves the maximum possible amount of the ERC20 token to be spent by the contract creator. This could potentially be exploited if the contract creator is malicious.

Proof of Concept

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L17

contract ERC20ProxyDelegator {
constructor(ERC20Votes _token, address _delegate) {
    _token.approve(msg.sender, type(uint256).max);  // Contract aprove maximum posible ammount 
    _token.delegate(_delegate);
       }
 }

Tools Used

Manually

Recommended Mitigation Steps

contract ERC20ProxyDelegator {
constructor(ERC20Votes _token, address _delegate) {
    // Remove the approval of tokens within the constructor
    _token.delegate(_delegate);
    }
   }

Assessed type

ERC20

[H-02] Approving maximum value in the ERC20MultiDelegate contract

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L16-L19

Vulnerability details

Impact

In the constructor the approve function is using the maximum value available for uint256.
This will create a vulnerability within this contract.

Proof of Concept

The constructor is vulnerable to approving maximum value approve function

// Line 16-19
    constructor(ERC20Votes _token, address _delegate) {
        _token.approve(msg.sender, type(uint256).max);
        _token.delegate(_delegate);
    }

Tools Used

VS Code.

Recommended Mitigation Steps

Change the amount to be approved to a smaller amount.
And utilise safeIncreaseAllowance or safeDecreaseAllowance.

Assessed type

ERC20

Agreements & Disclosures

Agreements

If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:

To signal your agreement to these terms, add a 👍 emoji to this issue.

Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.

Disclosures

Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.

To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:

  1. any sponsor staff or sponsor contractors who are also participating as wardens
  2. any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
  3. any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
  4. any other case where someone might reasonably infer a possible conflict of interest.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Analysis

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Wrong address will be used in delegation process cause delegateMulti function failed

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L198-L214

Vulnerability details

Impact

Incorrect address maybe used in _processDelegation, _reimburse and createProxyDelegatorAndTransfer functions.

Function ERC20MultiDelegate#retrieveProxyContractAddress is called by processDelegation, reimburse and createProxyDelegatorAndTransfer. But ERC20MultiDelegate#retrieveProxyContractAddress
return address is wrong due the truncated issue from uint256 to uint160 type convert:

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)));

If hash is greater than uint160 but upper bits is not zero, it will return a wrong address, cause token transfer to wrong address, failed or deploy to wrong delegate address.

Proof of Concept

Here is an example show such a truncated issue below, you can see the same address is different when convert to uint256 and uint160 type:

function testExp() public view {
    bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), uint256(0), keccak256("0x")));

    console.log("uint256 hash: ", uint256(hash));
    console.log("uint160 hash: ", uint160(uint256(hash)));
}

Result:
Logs:
uint256 hash: 480595119331166662771322338630425961949984860465823482516167119000543655588
uint160 hash: 66872880121789976335680252518238911347936949924

Tools Used

vscode

Recommended Mitigation Steps

Add a check for the upper 96 bits when convert hash value from uint256 to uint160.

Assessed type

Error

Contracts Does not check if the deployment was successful of each other

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L15 https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L25

Vulnerability details

Impact

The ERC20MultiDelegate contract does not check if the ERC20ProxyDelegator contract deployment was successful. If the deployment fails for any reason, the contract will continue to execute as if it was successful.

Proof of Concept

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L15
https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L25
Both contract does not check each other to check if deployed correctly or not

Tools Used

Mannualy

Recommended Mitigation Steps

"Factory Pattern" or "Factory Contract," is mitigation to address deployment dependencies. it ensures that contract instances are created and managed in a way that minimizes the risk of deployment failures and guarantees consistency in contract interactions.

Assessed type

Error

Risk of Stuck Fund Due to Lack of Approval Management in ERC20ProxyDelegator

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L15-L20

Vulnerability details

Impact

If the _delegate address specified in ERC20ProxyDelegator.constructor belongs to a protocol contract with a large number of members and there is substantial activity involving the ERC20Votes token, the ERC20ProxyDelegator contract could drain its token transfer allowance due to this high processing demands. This situation could potentially lock up users' Votes tokens inside the ERC20ProxyDelegator indefinitely.

Proof of Concept

The root cause of the vulnerability is the absence of an endpoint to further manage approvals in the ERC20ProxyDelegator contract. Let's walk through the issue with the following scenario:

  1. Alice is a user of the ENS protocol and holds Votes tokens. She has delegated her Votes to the Foo protocol via ERC20MultiDelegate. Currently, her Votes tokens are locked inside the ERC20ProxyDelegator contract, equivalent to her Votes token balance and the Foo protocol address.

  2. Over time, the ENS protocol experiences substantial growth in delegate, transfer and undelegate transactions to/from the Foo _delegatee address. This address represents a protocol contract with numerous members and frequent activity. These activities would likely invoke calls to ERC20Votes transfers away from the ERC20ProxyDelegator contract ie. transferBetweenDelegators and _reimburse. Everytime a transfer is executed, ERC20ProxyDelegator's allowance drops accordingly.

  3. As the volume of these transactions increases over time, the approval allowance for the ERC20MultiDelegate contract will be consumed, eventually reaching the maximum value of type(uint256).max.

  4. Once the approval allowance is exhausted, Alice and other users are unable to undelegate and reimburse their Votes tokens. Effectively, their Votes tokens become locked inside the ERC20ProxyDelegator contract, as illustrated in the code snippet below:

  function _reimburse(address source, uint256 amount) internal {
      address proxyAddressFrom = retrieveProxyContractAddress(token, source);
      token.transferFrom(proxyAddressFrom, msg.sender, amount); <= FOUND: This operation would fail due to the drained allowance from ERC20ProxyDelegator
  }
  1. The protocol's functionality are at risk of disruption due to the inability to efficiently process undelegate transactions. Furthermore, the loss of voting power to the Foo protocol as perpetual delegatee occurs, as the entire Foo delegator are unable to reclaim their Votes tokens, resulting in a loss of funds.

Tools Used

Manual Review

Recommended Mitigation Steps

It is recommended to implement a dedicated increaseApproveAllowance() endpoint within the ERC20ProxyDelegator contract to increase approval allowance exclusively for the ERC20MultiDelegate contract.

Assessed type

Other

ERC20MultiDelegate._processDelegation() mistakingly calls getBalanceForDelegate(source) to get the token balance of ``source``.

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L124-L137

Vulnerability details

Impact

Detailed description of the impact of this finding.
ERC20MultiDelegate._processDelegation() mistakenly calls getBalanceForDelegate(source) to get the token balance of source. The main issue is that getBalanceForDelegate(source) will return the balance of tokenID source for msg.sender, not the token balance for source.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

ERC20MultiDelegate._processDelegation() processes the delegation transfer between a source delegate and a target delegate:

https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L124-L137

However, it mistakenly calls getBalanceForDelegate(source) to get the token balance of source. The function will only get the balance of msg.sender for token source.

https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L192-L196

We show the correct way to get the token balance of source below.

Tools Used

VSCode

Recommended Mitigation Steps

 function _processDelegation(
        address source,
        address target,
        uint256 amount
    ) internal {
-        uint256 balance = getBalanceForDelegate(source);
+        uint256 balance = token.balanceOf(source);

        assert(amount <= balance);

        deployProxyDelegatorIfNeeded(target);
        transferBetweenDelegators(source, target, amount);

        emit DelegationProcessed(source, target, amount);
    }

Assessed type

ETH-Transfer

Add delegation status checks whether source and target have already delegated

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L124-L137

Vulnerability details

Impact

Without delegation status checks, users may inadvertently perform duplicate delegations. This can lead to confusion and unexpected behavior,not checking delegation status can open the door to misuse. Users might repeatedly delegate their votes to the same delegate to amplify their influence.

Tools Used

Manual Review

Recommended Mitigation Steps

Create a mapping that associates each address (delegate) with a boolean value indicating whether they have delegated or not. mapping(address => bool) public hasDelegated;

function _processDelegation(
address source,
address target,
uint256 amount
) internal {

  //Check Delegation Status
  require(!hasDelegated[source] && !hasDelegated[target], "Source && target has already delegated");

    uint256 balance = getBalanceForDelegate(source);

    assert(amount <= balance);

    deployProxyDelegatorIfNeeded(target);
    transferBetweenDelegators(source, target, amount);

     //Update Delegation Status
      hasDelegated[source] = true;
      hasDelegated[target] = true;

    emit DelegationProcessed(source, target, amount);
}

Assessed type

Other

QA Report

See the markdown file with the details of this report here.

TransferFrom return value ignored

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L148
https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L160
https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L170

Vulnerability details

Impact

The transferFrom function in the ERC20 standard returns a boolean value indicating whether the transfer was successful or not. In the _reimburse function, createProxyDelegatorAndTransfer function and transferBetweenDelegators function there it isn't checked this return value.

If the transferFrom function fails for any reason (for example, if the proxyAddressFrom doesn't have enough tokens), it will return false. However, because the functions doesn't check the return value, they will continue executing as if the transfer was successful. This could lead to unexpected behaviour in the contract (missing reimbursement for users, wrong delegation, etc).

Proof of Concept

contracts/ERC20MultiDelegate.sol#141-146:
ERC20MultiDelegate._reimburse(address,uint256) function token.transferFrom(proxyAddressFrom,msg.sender,amount) - line #148

contracts/ERC20MultiDelegate.sol#152-155:
ERC20MultiDelegate.createProxyDelegatorAndTransfer(address,uint256) function token.transferFrom(msg.sender,proxyAddress,amount) - line #160

contracts/ERC20MultiDelegate.sol#157-161:
ERC20MultiDelegate.transferBetweenDelegators(address,address,uint256) function token.transferFrom(proxyAddressFrom,proxyAddressTo,amount) - line #170

Tools Used

Slither

Recommended Mitigation Steps

Check the return value of the transferFrom function and handle the case where it returns false or evaluate if it can be used the safeTransferFrom() function.

Assessed type

Token-Transfer

The delegateMulti function allows for the delegation of tokens to proxy contracts, which can introduce additional risks or vulnerabilities

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L57

Vulnerability details

Impact

*the delegateMulti function at line #L57: https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L57 may lead the potential for malicious actions by attackers who gain control of the proxy contracts, exploitable vulnerabilities in the proxy contracts themselves, and the risk of loss of funds for users who delegate tokens to the proxy contracts.

Proof of Concept

const ERC20MultiDelegate = artifacts.require("ERC20MultiDelegate");

contract("ERC20MultiDelegate", (accounts) => {
  let token;
  let delegate;

  before(async () => {
    token = await ERC20Votes.new("Token", "TKN");
    delegate = await ERC20MultiDelegate.new(token.address, "https://example.com/{id}.json");
  });

  it("should delegate tokens to multiple delegates", async () => {
    const sources = [accounts[0], accounts[1]];
    const targets = [accounts[2], accounts[3]];
    const amounts = [100, 200];

    await token.approve(delegate.address, 300, { from: accounts[0] });
    await delegate.delegateMulti(sources, targets, amounts, { from: accounts[0] });

    const balance1 = await token.balanceOf(accounts[2]);
    const balance2 = await token.balanceOf(accounts[3]);

    assert.equal(balance1, 100, "Incorrect balance for delegate 1");
    assert.equal(balance2, 200, "Incorrect balance for delegate 2");
  });
});

The code uses the delegateMulti function to delegate tokens from two source delegates (accounts[0] and accounts[1]) to two target delegates (accounts[2] and accounts[3]) in amounts of 100 and 200 tokens, respectively. The code also checks that the balances of the target delegates are correct after the delegation process.

Tools Used

Manual review

Recommended Mitigation Steps

*Implement access control mechanisms to ensure that only authorized users can delegate tokens. This can be done by adding a modifier to the delegateMulti function that checks if the caller is an authorized user.

// Define a mapping to store authorized users
mapping(address => bool) public authorizedUsers;

// Define a modifier to check if the caller is an authorized user
modifier onlyAuthorized() {
    require(authorizedUsers[msg.sender], "Delegate: Caller is not authorized");
    _;
}

// Add the onlyAuthorized modifier to the delegateMulti function
function delegateMulti(
    uint256[] calldata sources,
    uint256[] calldata targets,
    uint256[] calldata amounts
) external onlyAuthorized {
    _delegateMulti(sources, targets, amounts);
}

Define a mapping authorizedUsers to store authorized users.

// Define a function to add an authorized user
function addAuthorizedUser(address user) external onlyOwner {
    authorizedUsers[user] = true;
}

// Define a function to remove an authorized user
function removeAuthorizedUser(address user) external onlyOwner {
    authorizedUsers[user] = false;
}

Assessed type

DoS

QA Report

See the markdown file with the details of this report here.

Potential DOS via Unlimited Proxy Contract Deployment

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L173-L190

Vulnerability details

Impact

The function deployProxyDelegatorIfNeeded allows users to deploy an arbitrary number of proxy contracts for each delegate address they use. This can be exploited by an attacker to launch a Denial of Service (DOS) attack by deploying a large number of contracts, consuming excessive gas, and potentially slowing down the network.

Proof of Concept

ERC20MultiDelegate.sol

    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;
    }

Recommended Mitigation Steps

  • Introduce a mechanism to limit the number of proxy contracts a user can deploy.
  • Consider charging users a fee for deploying proxy contracts to disincentivize spamming.

Assessed type

DoS

Caller can withdrawn balance tokens to his account

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L57-L63
https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L65-L116
https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L144-L149

Vulnerability details

Impact

The caller of ERC20MultiDelegate::delegateMulti function can force the flow into _reimburse() function. The reimburse function is transferring the tokens of the source to the msg.sender account. Hence, the caller can invoke the delegateMulti call to withdrawn tokens from a user account.

Proof of Concept

lets assume the below are parameters of the delegateMulti call.
uint256[] calldata sources = [s1,s2,s3];
uint256[] calldata targets= [];
uint256[] calldata amounts= [amt1,amt2,amt3]; // amt1 is the balance of s1.

in the delegateMulti call flow

   if (transferIndex < Math.min(sourcesLength, targetsLength)) {
      // will not enter this flow as targets.length is less than transferIndex 
   } 
   else if (transferIndex < sourcesLength) {
     will enter since the above condition will satisfy.
      _reimburse(source, amount);
   }

using the _reimburse functions, the funds from S1 will be transferred to the caller account.

 function _reimburse(address source, uint256 amount) internal {
        // Transfer the remaining source amount or the full source amount
        // (if no remaining amount) to the delegator
        address proxyAddressFrom = retrieveProxyContractAddress(token, source);
        token.transferFrom(proxyAddressFrom, msg.sender, amount);
    }

Tools Used

Manual Review

Recommended Mitigation Steps

Assumption that the remaining funds are transferred back to the caller is valid only if the transfer was executed.
So, flag if transfer was executed and only for such cases, call refund to send back the balance. For other cases, the flag will remain false preventing this abuse.

Assessed type

Access Control

Analysis

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Privileged functions in the ERC20MultiDelegate contract may lead to centralization and a single point of failure

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L151
https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L173
https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L155

Vulnerability details

I have identified that, there are three privileged functions in the ERC20MultiDelegate contract:

  1. The setUri function on line 151: https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L151 is a privileged function that can only be called by the contract owner.

  2. The createProxyDelegatorAndTransfer function on line 156:https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L156 is a privileged function that creates a new proxy contract for a delegate and transfers tokens to it from the sender's account.

  3. The deployProxyDelegatorIfNeeded function on line 173: https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L173 is a privileged function that deploys a new proxy contract for a delegate if one does not already exist.

These functions are privileged because they can only be called by the contract itself, and not by external accounts. This means that the it may lead Loss of funds: If the contract owner loses control of their account or is compromised in some way, then an attacker could potentially gain control of these functions and deploy new proxy contracts or transfer tokens to them. This could lead to a loss of funds for the contract and its users.

POC shot

I have tried some abstraction to POC it:

  1. Deploy the ERC20MultiDelegate contract and set the URI to a malicious website:
const { ethers } = require("hardhat");

async function main() {
  const ERC20MultiDelegate = await ethers.getContractFactory("ERC20MultiDelegate");
  const contract = await ERC20MultiDelegate.deploy();
  await contract.deployed();

  // Set the URI to a malicious website
  await contract.setUri("https://malicious-website.com");
}

main();
  1. Call the tokenURI function on the ERC20MultiDelegate contract to retrieve the URI for the token metadata:
const { ethers } = require("hardhat");

async function main() {
  const ERC20MultiDelegate = await ethers.getContractFactory("ERC20MultiDelegate");
  const contract = await ERC20MultiDelegate.deploy();
  await contract.deployed();

  // Set the URI to a malicious website
  await contract.setUri("https://malicious-website.com");

  // Retrieve the URI for the token metadata
  const uri = await contract.tokenURI(0);
  console.log(uri); // Output: "https://malicious-website.com"
}

main();

if deploy the ERC20MultiDelegate contract and set the URI to a malicious website using the setUri function on line 150. We then call the tokenURI function on the contract to retrieve the URI for the token metadata, which returns the malicious website URL.

Mitigations

To mitigate the risks associated with the privileged functions I beleive that some measures can be taken:
*Limit access to the contract owner and any other trusted entities that need access to the privileged functions. This can help reduce the risk of malicious actions and centralization

*Consider implementing a timelock mechanism that requires a certain amount of time to pass before any changes to the privileged functions can take effect. This can help prevent malicious actions and provide a window of time for any issues to be identified and addressed.

Hope it help you guys. Thank you!

Assessed type

Access Control

Lack of Fallback Mechanism for Proxy Contract Deployment Failures

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L173-L190

Vulnerability details

Impact

If the ERC20ProxyDelegator deployment fails for any reason like token authorization failure, the entire transaction will be reverted. This could result in a poor user experience and potentially leave the user's funds in an undesirable state or locked position.

Proof of Concept

deployProxyDelegatorIfNeeded function in ERC20MultiDelegate.sol.

173:    function deployProxyDelegatorIfNeeded(
174:        address delegate
175:    ) internal returns (address) {
176:        address proxyAddress = retrieveProxyContractAddress(token, delegate);
177:
178:        // check if the proxy contract has already been deployed
179:        uint bytecodeSize;
180:        assembly {
181:            bytecodeSize := extcodesize(proxyAddress)
182:        }
183:
184:        // if the proxy contract has not been deployed, deploy it
185:        if (bytecodeSize == 0) {
186:            new ERC20ProxyDelegator{salt: 0}(token, delegate);
187:            emit ProxyDeployed(delegate, proxyAddress);
188:        }
189:        return proxyAddress;
190:    }

Recommended Mitigation Steps

  • Implement a validation check after deploying the ERC20ProxyDelegator to ensure it has been correctly initialized and set up.

  • Introduce a fallback mechanism that can either return the user's funds or provide an alternative remediation path in case the proxy contract deployment fails.

Assessed type

Other

QA Report

See the markdown file with the details of this report here.

contract uses the assert function cause a denial of service.

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L131

Vulnerability details

Impact

The ERC20MultiDelegate contract uses the assert function to check if the amount to be transferred is less than or equal to the balance of the source delegate. If this condition is not met, the contract will throw an exception and revert all changes. This could potentially be exploited to cause a denial of service.

assert function for balance checks. If the balance check fails (i.e., the source delegate does not have enough tokens to transfer), the entire transaction is reverted, including any gas used and state changes made during the transaction.

Proof of Concept

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L131

  assert(amount <= balance);

Tools Used

Manual review

Recommended Mitigation Steps

require(amount <= balance, "Not enough balance for the transfer");

Assessed type

Invalid Validation

The number of amounts is strictly equal between number of targets, and sourses, instead is SHOULD equal of greater

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L79-L81

Vulnerability details

Impact

The current implementation ensures that the number of amounts is strictly equal to the number of targets and sources. This can lead to potential issues if there are more amounts specified than targets or sources.

Proof of Concept

In contract
on lines 79-81 we have this require 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"
        );

The description said the number of amounts should be equal or greater. But the require statement misses this.

Tools Used

Manual review

Recommended Mitigation Steps

To ensure flexibility and avoid potential mismatches, it's recommended to change the condition to check if the number of amounts is equal to or greater than the number of targets and sources. This can be achieved using the Math.max function.

require(Math.max(sourcesLength, targetsLength) <= amountsLength, "ERC20MultiDelegate: LENGTH_MISMATCH");

By implementing the above change, the contract ensures that there are enough amounts specified for the given targets and sources, providing a more robust validation mechanism.

Or it can be problem in incorrect description of require statement

Assessed type

Context

[M-01] Approve front-running attack in the ERC20MultiDelegate contract

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L16-L19

Vulnerability details

Impact

There is no way to safely decrease or increase the amount of the token as the owner can only be the contract.
The recipient can exploit this by pretending to be the sender and sending token as the sender to themselves, the attacker or malicious recipient.
Also, if the sender updates the amount they are trying to send then the malicious recipient can withdraw both amounts from the sender's account.
Hencefotrh a front-running attack on the ERC20 token.
Hence the function can front-run by manipulating the approve function.

Proof of Concept

The constructor is vulnerable to front-running attack because of the ERC20 approve function

// Line 16-19
    constructor(ERC20Votes _token, address _delegate) {
        _token.approve(msg.sender, type(uint256).max);
        _token.delegate(_delegate);
    }

Tools Used

VS Code.

Recommended Mitigation Steps

Just use the approve token to set the amount from zero or to zero and wait for the transaction to be committed.
Use safeDecreaseAllowance or safeIncreaseAllowance functions.

Assessed type

Token-Transfer

Missing access control on transfering tokens

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L65-L116
https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L124-L137
https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L144-L149
https://github.com/code-423n4/2023-10-ens/blob/1adbe2cce191140657b8bccffab85103953bdccb/contracts/ERC20MultiDelegate.sol#L155-L161

Vulnerability details

Impact

An attacker can steal any remaining source amounts from any proxy address, by calling the function delegateMulti such, that it will in turn call _delegateMulti and it will in turn call _reimburse. And then this will transfer from any proxyAddressFrom to msg.sender.

In case the _delegateMulti will call _processDelegation or createProxyDelegatorAndTransfer, there will be a transfer from proxyAddressFrom to proxyAddressTo, or from msg.sender to proxyAddress respectively.

Proof of Concept

For example:
Attacker calling delegateMulti with sources (3 addresses), targets (0 address) and amounts (100, 100, 100).

The function will call _reimburse function here:

            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);

And it in turn will do:

    function _reimburse(address source, uint256 amount) internal {
        // Transfer the remaining source amount or the full source amount
        // (if no remaining amount) to the delegator
        address proxyAddressFrom = retrieveProxyContractAddress(token, source);
        token.transferFrom(proxyAddressFrom, msg.sender, amount);
    }

token.transferFrom from those 3 addresses 100 amount to an attacker.

Tools Used

Manual review.

Recommended Mitigation Steps

Add access controls on every transfer, to ensure that nobody can own others amounts.

Assessed type

Access Control

QA Report

See the markdown file with the details of this report here.

Uncontrolled Metadata URI Update

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L151-L53

Vulnerability details

Impact

The setUri function in the contract allows the owner to change the metadata URI of the ERC1155 tokens arbitrarily. If an attacker gains control over the owner's address, they can redirect the URI to a malicious metadata server. This can lead to phishing attacks by providing misleading token information to users.

Proof of Concept

contracts/ERC20MultiDelegate.sol

    function setUri(string memory uri) external onlyOwner {
        _setURI(uri);
    }

Recommended Mitigation Steps

Set the URI during contract initialization and make it immutable.

Assessed type

Access Control

Steal voting power of any user

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L57-L63

Vulnerability details

Impact

When userA delegates his voting power to userB, through calling delegateMulti, the value of _balances[userA][userB] for ERC1155 will be > 0, which is a result of the _mintBatch function, and is the only "flag" showing that is showing the passed voting power from userA to userB. So in theory, to increase the value of _balances[userA][userB] userA needs to call delegateMulti and pass userB as a target. However, this is not the case here, assume we have 3 users, userA calls delegateMulti and passes userB as a target, he passed his voting power to userB, and now _balances[userA][userB] is > 0. On the other hand, we have userC who calls delegateMulti and passed himself as a target, now we also have _balances[userC][userC] > 0, now userC calls safeTransferFrom from userC to userA, which will cause _balances[userA][userC] to be > 0, resulting in taking userA's voting power without his allowance, the result of that's said is the same as if userA calls delegateMulti and passed userC as a target (both userC and userB will have _balances[userA][userB/C] > 0.

Proof of Concept

it("bug poc", async () => {
  const delegatorTokenAmount = await token.balanceOf(deployer);
  await token.approve(multiDelegate.address, delegatorTokenAmount);

  const [, , deployer2] = await ethers.getSigners();
  const deployer2Address = deployer2.address;
  await increaseTime(365 * 24 * 60 * 60);
  const deployer2Amount = delegatorTokenAmount.div(1000000000)
  await token.mint(deployer2Address, deployer2Amount);
  await token.connect(deployer2).approve(multiDelegate.address, deployer2Amount);

  const delegates = [alice];
  const amounts = delegates.map(() =>
    delegatorTokenAmount.div(delegates.length)
  );

  await multiDelegate.delegateMulti([], delegates, amounts);

  const delegatorTokenAmountAfter = await token.balanceOf(deployer);
  expect(delegatorTokenAmountAfter.toString()).to.equal('0');

  for (let delegateTokenId of delegates) {
    let balance = await multiDelegate.balanceOf(deployer, delegateTokenId);
    expect(balance.toString()).to.equal(
      delegatorTokenAmount.div(delegates.length).toString()
    );
  }

  expect((await multiDelegate.balanceOf(deployer, deployer2Address)).toString()).to.equal('0');
  expect((await token.getVotes(deployer2Address)).toString()).to.equal('0');

  await multiDelegate.connect(deployer2).delegateMulti([], [deployer2Address], [deployer2Amount]);

  const alice2AddressInt = ethers.BigNumber.from(deployer2Address);

  await multiDelegate.connect(deployer2).safeTransferFrom(deployer2.address, deployer, alice2AddressInt, deployer2Amount, []);

  expect((await multiDelegate.balanceOf(deployer, deployer2Address)).toString()).to.equal(deployer2Amount.toString());
  expect((await token.getVotes(deployer2Address)).toString()).to.equal(deployer2Amount.toString());
});

Tools Used

Manual review + vscode

Recommended Mitigation Steps

Override safeTransferFrom and just throw a revert in there.

Assessed type

Access Control

[H-01] Incorrect access control in the ERC20MultiDelegate contract

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L57-L63

Vulnerability details

Impact

Access controls need to be used and not just imported.

The ERC20MultiDelegate contract imports the access control library for: @openzeppelin/contracts/access/Ownable.sol.

Although this is the case the delegateMulti function is not using the modifier called onlyOwner.

Proof of Concept

This delegateMulti function is vulnerable to incorrect access control.

// Line 57-63
    function delegateMulti(
        uint256[] calldata sources,
        uint256[] calldata targets,
        uint256[] calldata amounts
    ) external {
        _delegateMulti(sources, targets, amounts);
    }

Exploit

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;

import "./ERC20MultiDelegate.sol";

contract tERC20MultiDelegate{
   
   ERC20MultiDelegate public x1;

   constructor(ERC20MultiDelegate _x1) {

      x1 = ERC20MultiDelegate(_x1);

   }

   function testAccess() external payable {
      uint256[] calldata sources = new uint256[](1);
      sources[0] = uint256(1e10); 
      uint256[] calldata targets = new uint256[](1);
      targets[0] = uint256(2e10);
      uint256[] calldata amounts = new uint256[](1);
      amounts[0] = uint256(1e18);

      x1.delegateMulti(sources, targets, amounts);
   
   }

   }

Tools Used

VS Code.

Recommended Mitigation Steps

Add the access control modifiers to functions that are missing them such as external payable and admin functions.

Assessed type

Access Control

[H-03] Unchecked array length in the ERC20MultiDelegate contract

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L85-L89

Vulnerability details

Impact

When sourcesLength and targetsLength are bigger then expected.

The the loop will topple the block gas max amount.

Then transactions being made do not commit.

Which leads to sourcesLength and targetsLength loop becoming a costly function which proves to be a vulnerability flaw.

Proof of Concept

The vulnerable function indirectly linked to this loop is delegateMulti

// Line 85-89
        for (
            uint transferIndex = 0;
            transferIndex < Math.max(sourcesLength, targetsLength);
            transferIndex++
        ) {

Tools Used

VS Code.

Recommended Mitigation Steps

There is a flaw with this loop that can cause denial of service if it runs for too long and the funds in the contract deplete due to gas over exhaustion.

So, loops that can grow significantly or the length is ever unknown can cost a lot of gas.

Assessed type

Loop

Approve return value in the constructor ignored

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L17

Vulnerability details

Impact

The approve function in the ERC20 standard returns a boolean value indicating whether the approval was successful or not. In the constructor, it ins't checked this return value.

If the approve function fails for any reason, it will return false. However, because the constructor doesn't check the return value, it will continue executing as if the approval was successful. This could lead to unexpected behaviour in the contract (the tokens can't be delegated).

Proof of Concept

contracts/ERC20MultiDelegate.sol#16-19:
ERC20ProxyDelegator.constructor(ERC20Votes,address) constructor
_token.approve(msg.sender,type()(uint256).max) - line #17ì6

Tools Used

Slither

Recommended Mitigation Steps

Use safeApprove instead of approve.

Assessed type

Token-Transfer

A Possible Reentrancy Attack

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/ed25379c06e42c8218eb1e80e141412496950685/contracts/ERC20MultiDelegate.sol#L114

Vulnerability details

Vulnerabilitiy Details

The _delegateMultin has no non-reentrant modifier which can lead to a possible reentrancy attack

PoC (Proof Of Concept)

ERC20MultiDelegate.sol#L114
At this line u can see a function called as _mintBatch

...
...    if (targetsLength > 0) {
114 ->    _mintBatch(msg.sender, targets, amounts[:targetsLength], "");
...    }
...   

In this function there is a another function called as _doSafeBatchTransferAcceptanceCheck has a onERC1155BatchReceived

...
320 - _doSafeBatchTransferAcceptanceCheck
...

3.I tried several ways to exploit it, but i think that it is possible exploit some functions with _mintBatch

4.Done

Tools Used

VSCODE

Recommended Mitigation Steps

Use A Modifier NonReentrant

Assessed type

Reentrancy

Lack of Fallback Mechanism for Proxy Contract Deployment Failures

Lines of code

https://github.com/code-423n4/2023-10-ens/blob/main/contracts/ERC20MultiDelegate.sol#L173-L190

Vulnerability details

Impact

If the deployment of the ERC20ProxyDelegator fails for any reason (e.g., token authorization failure), the entire transaction will be reverted. This could result in a bad user experience as the user's intended operation would not be completed, potentially leaving their funds in an undesirable state or locked position.

Proof of Concept

contracts/ERC20MultiDelegate.sol

    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;
    }

Recommended Mitigation Steps

  • Implement a validation check post the deployment of the ERC20ProxyDelegator to ensure that it has been correctly initialized and set up.
  • Introduce a fallback mechanism that can either return the user's funds or provide an alternative remediation path in case the proxy contract deployment fails.

Assessed type

Token-Transfer

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.