Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Self-delgated users can have their delegation unknowingly hijacked during crowdfunding #38

Open
code423n4 opened this issue Apr 14, 2023 · 3 comments
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working H-01 satisfactory satisfies C4 submission criteria; eligible for awards selected for report This submission will be included/highlighted in the audit report sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")

Comments

@code423n4
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2023-04-party/blob/440aafacb0f15d037594cebc85fd471729bcb6d9/contracts/crowdfund/ETHCrowdfundBase.sol#L169-L234

Vulnerability details

Impact

Self-delegation can be hijacked

Proof of Concept

PartyGovernance.sol#L886-L906

function _adjustVotingPower(address voter, int192 votingPower, address delegate) internal {
    VotingPowerSnapshot memory oldSnap = _getLastVotingPowerSnapshotForVoter(voter);
    address oldDelegate = delegationsByVoter[voter];
    // If `oldDelegate` is zero and `voter` never delegated, then have
    // `voter` delegate to themself.
    oldDelegate = oldDelegate == address(0) ? voter : oldDelegate;
    // If the new `delegate` is zero, use the current (old) delegate.
    delegate = delegate == address(0) ? oldDelegate : delegate;

    VotingPowerSnapshot memory newSnap = VotingPowerSnapshot({
        timestamp: uint40(block.timestamp),
        delegatedVotingPower: oldSnap.delegatedVotingPower,
        intrinsicVotingPower: (oldSnap.intrinsicVotingPower.safeCastUint96ToInt192() +
            votingPower).safeCastInt192ToUint96(),
        isDelegated: delegate != voter
    });
    _insertVotingPowerSnapshot(voter, newSnap);
    delegationsByVoter[voter] = delegate;
    // Handle rebalancing delegates.
    _rebalanceDelegates(voter, oldDelegate, delegate, oldSnap, newSnap);
}

Self-delegation is triggered when a user specifies their delegate as address(0). This means that if a user wishes to self-delegate they will can contribute to a crowdfund with delegate == address(0).

ETHCrowdfundBase.sol#L169-L181

function _processContribution(
    address payable contributor,
    address delegate,
    uint96 amount
) internal returns (uint96 votingPower) {
    address oldDelegate = delegationsByContributor[contributor];
    if (msg.sender == contributor || oldDelegate == address(0)) {
        // Update delegate.
        delegationsByContributor[contributor] = delegate;
    } else {
        // Prevent changing another's delegate if already delegated.
        delegate = oldDelegate;
    }

This method of self-delegation is problematic when combined with _processContribution. When contributing for someone else, the caller is allowed to specify any delegate they wish. If that user is currently self delegated, then the newly specified delegate will overwrite their self delegation. This allows anyone to hijack the voting power of a self-delegated user.

This can create serious issues for ReraiseETHCrowdfund because party NFTs are not minted until after the entire crowdfund is successful. Unlike InitialETHCrowdfund, this allows the attacker to hijack all of the user's newly minted votes.

Example:
minContribution = 1 and maxContribution = 100. User A contributes 100 to ReraiseETHCrowdfund. They wish to self-delegate so they call contribute with delegate == address(0). An attacker now contributes 1 on behalf of User A with themselves as the delegate. Now when the NFTs are claimed, they will be delegated to the attacker.

Tools Used

Manual Review

Recommended Mitigation Steps

Self-delegation should be automatically hardcoded:

+   if (msg.sender == contributor && delegate == address(0)) {
+       delegationsByContributor[contributor] = contributor;
+   }

    address oldDelegate = delegationsByContributor[contributor];
    if (msg.sender == contributor || oldDelegate == address(0)) {
        // Update delegate.
        delegationsByContributor[contributor] = delegate;
    } else {
        // Prevent changing another's delegate if already delegated.
        delegate = oldDelegate;
    }
@code423n4 code423n4 added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Apr 14, 2023
code423n4 added a commit that referenced this issue Apr 14, 2023
@0xean
Copy link

0xean commented Apr 14, 2023

This appears valid at first pass and allows anyone to steal self delegations.

@c4-sponsor
Copy link

0xble marked the issue as sponsor confirmed

@c4-sponsor c4-sponsor added the sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") label Apr 17, 2023
@c4-judge
Copy link

0xean marked the issue as satisfactory

@c4-judge c4-judge added the satisfactory satisfies C4 submission criteria; eligible for awards label Apr 25, 2023
@C4-Staff C4-Staff added selected for report This submission will be included/highlighted in the audit report H-01 labels Apr 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working H-01 satisfactory satisfies C4 submission criteria; eligible for awards selected for report This submission will be included/highlighted in the audit report sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")
Projects
None yet
Development

No branches or pull requests

5 participants