3.1 Voting Amplification With Sybil Attacks

  • ID: PVE-001

  • Severity: Medium

  • Likelihood: High

  • Impact: Medium

  • Target: PopToken

  • Category: Business Logic []

  • CWE subcategory: CWE-841 []

Description

In Pop, the protocol token PopToken is enhanced with voting support so that it can be used to cast and record the votes. Moreover, the PopToken contract allows for dynamic delegation of a voter to another, though the delegation is not transitive. When a submitted proposal is being tallied, the number of votes are counted prior to the proposal’s proposal.startBlock.

Our analysis on the PopToken shows that the its voting use may be vulnerable to a new type of so-called Sybil attacks []. For elaboration, let’s assume at the very beginning there is a malicious actor named Malice, who owns 100 PPT tokens. Malice has an accomplice named Trudy who currently has 0 balance of PPTs. This Sybil attack can be launched as follows:

function _delegate ( address delegator, address delegatee) internal {
currentDelegate= _delegates[delegator];
uint 256 delegatorBalance= balanceOf (delegator) ; // balance of underlying PPTs (not scaled );
	_delegates[ delegator] = delegatee;
	
	emit Delegate Changed (delegator, currentDelegate, delegatee) ;
	
	_move Delegates ( currentDelegate, delegatee, delegatorBalance) ;
}	
	
function _move Delegates (
	address srcRep ,
	address dstRep ,
	uint 256 amount
)	internal {	
	if ( s rc Re p != dst Rep && amount > 0 ) {	
	if ( s rc Re p != address ( 0 ) ) {	
	// decrease old representative	
	uint 32 srcRepNum = num Checkpoints [ srcRep ] ;	
	uint 256 srcRepOld = srcRepNum > 0	
	? checkpoints [srcRep] [srcRepNum − 1 ] . v o t e s	
	: 0 ;	
	uint 256 srcRepNew = s rc Rep Old − amount ;	
	_writeCheckpoint ( src Rep , srcRepNum , src Rep Old ,	srcRepNew ) ;
	}	
		
	i f ( dstRep  != address ( 0 ) ) {	
	// increase new representative	
	uint 32 dstRepNum = num Checkpoints [ dstRep ] ;	
	uint 256 dst Rep Old = dstRepNum > 0	
			? checkpoints [ dst Rep ] [ dstRepNum − 1 ].votes	
			: 0 ;	
			uint 256 dstRepNew = dstRep Old + amount ;	
			_writeCheckpoint ( dstRep , dstRepNum , dstRepOld ,	dstRepNew ) ;
		}		
	}			
}				
  1. Malice initially delegates the voting to Trudy. Right after the initial delegation, Trudy can have

    100 votes if he chooses to cast the vote.

  2. Malice transfers the full 100 balance to M1 who also delegates the voting to Trudy. Right after this delegation, Trudy can have 200 votes if he chooses to cast the vote. The reason is that the PopToken contract’s transfer() does NOT _moveDelegates() together. In other words, even now Malice has 0 balance, the initial delegation (of Malice) to Trudy will not be affected, therefore Trudy still retains the voting power of 100 PPTs. When M1 delegates to Trudy, since M1 now has 100 PPTs, Trudy will get additional 100 votes, totaling 200 votes.

  3. We can repeat by transferring Mi’s 100 PPT balance to Mi+1 who also delegates the votes to Trudy. Every iteration will essentially add 100 voting power to Trudy. In other words, we can effectively amplify the voting powers of Trudy arbitrarily with new accounts created and iterated!

Recommendation

To mitigate, it is necessary to accompany every single transfer() and transferFrom() with the _moveDelegates() so that the voting power of the sender’s delegate will be moved to the destination’s delegate. By doing so, we can effectively mitigate the above Sybil attacks.

Status

This issue has been fixed by removing the delegate functionality from the PopToken contract as reflected in the following commit: 551c931.

Last updated