Light ModeLight
Light ModeDark

One Bug Per Day

One H/M every day from top Wardens

Checkmark

Join over 1095 wardens!

Checkmark

Receive the email at any hour!

Ad

Limited Voting Options Allow Ballot Creation Spam

mediumCode4rena

Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/crosschain/keeper/keeper_cross_chain_tx_vote_inbound_tx.go#L95 https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/observer/keeper/msg_server_add_blame_vote.go#L39 https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/observer/keeper/msg_server_add_block_header.go#L79

Vulnerability details

Impact

The message functions for certain types of voting allow only successful observation votes. This creates a problem if a compromised or faulty observer creates spam ballots with false observations. In this situation, honest observers have to spend resources processing the ballot but cannot cast an opposite vote to slash the spammer when the ballot reaches maturity.

Theoritically, a similar situation could happen with all ballots that do not reach the threshold. But in non-vulnerable cases, honest observer can cast an opposite vote to make the ballot reach threshold and be accounted for slashing. The real issue comes when honest observers cannot cast the honest opposite vote.

Proof of Concept

For certain types of votes, the only vote that is possible to be cast is an VoteType_SuccessObservation.

  • https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/crosschain/keeper/keeper_cross_chain_tx_vote_inbound_tx.go#L95
go
ballot, err = k.zetaObserverKeeper.AddVoteToBallot(ctx, ballot, msg.Creator, observerTypes.VoteType_SuccessObservation)
  • https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/observer/keeper/msg_server_add_blame_vote.go#L39
go
ballot, err = k.AddVoteToBallot(ctx, ballot, vote.Creator, types.VoteType_SuccessObservation)
  • https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/observer/keeper/msg_server_add_block_header.go#L79
go
ballot, err = k.AddVoteToBallot(ctx, ballot, msg.Creator, types.VoteType_SuccessObservation)

A compromised or faulty observer can then create ballots for spam observations that do not exist, and honest observers can never vote for the opposite option. Since the ballot's threshold will never be reached, the ballot will remain with the status BallotStatus_BallotInProgress.

Eventually, the ballot will be processed when the DistributeObserverRewards function gathers the list of matured ballots.

  • https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/emissions/abci.go#L63
go
ballotIdentifiers := keeper.GetObserverKeeper().GetMaturedBallotList(ctx)

But, since BuildRewardsDistribution does not account for ballots with status BallotStatus_BallotInProgress, the ballot spammer will have a totalRewardUnits == 0 instead of a negative value. This way, no slashing will be executed as a punishment for the false observation, but observers will have to process the spam observation ballot anyway.

  • https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/observer/types/ballot.go#L77-L102
go
func (m Ballot) BuildRewardsDistribution(rewardsMap map[string]int64) int64 { totalRewardUnits := int64(0) switch m.BallotStatus { case BallotStatus_BallotFinalized_SuccessObservation: // ... SNIP ... case BallotStatus_BallotFinalized_FailureObservation: // ... SNIP ... return totalRewardUnits

The compromised observer is then free to create as many false observation ballots as he wishes, effectively spamming the network and draining honest observer's resources without a punishment.

Additional Note

Besides wasting network participant's execution resources, the ballots are never deleted from storage after the distribution. But there is a comment evidencing this is an obviously known issue and, thus, included as merely a reminder in this report.

  • https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/emissions/abci.go#L127C1-L128C50
go
// TODO : Delete Ballots after distribution // https://github.com/zeta-chain/node/issues/942

Tools Used

Manual: code editor.

Recommended Mitigation Steps

Similarly to what happens in other types of votes, allow honest observers to cast a negative observation vote if they wish. Thus, when the ballot reaches maturity and is processed for reward distribution, the dishonest or faulty observer is slashed to pay for any resources spent with the false observation.

This is how msg_tss_voter.go and keeper_cross_chain_tx_vote_outbound_tx.go work:

  • https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/crosschain/keeper/msg_tss_voter.go#L68C1-L78C3
go
if msg.Status == common.ReceiveStatus_Success { ballot, err = k.zetaObserverKeeper.AddVoteToBallot(ctx, ballot, msg.Creator, observerTypes.VoteType_SuccessObservation) if err != nil { return &types.MsgCreateTSSVoterResponse{}, err } } else if msg.Status == common.ReceiveStatus_Failed { ballot, err = k.zetaObserverKeeper.AddVoteToBallot(ctx, ballot, msg.Creator, observerTypes.VoteType_FailureObservation) if err != nil { return &types.MsgCreateTSSVoterResponse{}, err } }
  • https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/crosschain/keeper/keeper_cross_chain_tx_vote_outbound_tx.go#L105C1-L108C3
go
ballot, err = k.zetaObserverKeeper.AddVoteToBallot(ctx, ballot, msg.Creator, observerTypes.ConvertReceiveStatusToVoteType(msg.Status)) if err != nil { return nil, err }

Special attention must be give to ensure the corrected voting type's Digest function output is generalised for all types of votes. Similarly to what is done with MsgVoteOnObservedOutboundTx.

  • https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/crosschain/types/message_vote_on_observed_outbound_tx.go#L76C1-L86C2
go
func (msg *MsgVoteOnObservedOutboundTx) Digest() string { // ... SNIP ... // Set status to ReceiveStatus_Created to make sure both successful and failed votes are added to the same ballot m.Status = common.ReceiveStatus_Created // ... SNIP ...

Assessed type

DoS