Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/abstract/AbstractVotingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,24 @@ abstract contract AbstractVotingModule is IVotingModule, Initializable, EIP712Up
return _getAbstractVotingModuleStorage().totalCycleVotingPower[cycle];
}

// ============ Events ============

/// @notice Emitted when a vote is cast with additional parameters for downstream implementations
/// @param voter The voter who cast the vote
/// @param points The voting points allocated to each recipient
/// @param votingPower The voter's total voting power
/// @param nonce The unique nonce used for replay protection
/// @param signature The EIP-712 signature authorizing the vote
/// @param additionalData Arbitrary bytes data passed for downstream use (e.g., multipliers)
event VoteCastWithParams(
address indexed voter,
uint256[] points,
uint256 votingPower,
uint256 nonce,
bytes signature,
bytes additionalData
);

// ============ Internal Functions ============

/// @notice Processes a single vote with signature verification
Expand Down Expand Up @@ -305,6 +323,39 @@ abstract contract AbstractVotingModule is IVotingModule, Initializable, EIP712Up
/// @param votingPower Total voting power of the voter
function _processVote(address voter, uint256[] calldata points, uint256 votingPower) internal virtual;

/// @notice Processes a vote with additional data for downstream implementations
/// @dev Called by _castSingleVoteWithParams. Default impl calls _processVote then
/// _handleAdditionalVoteData. Override _handleAdditionalVoteData to act on additionalData.
/// @param voter Address of the voter
/// @param points Array of points allocated to each recipient
/// @param votingPower Total voting power of the voter
/// @param additionalData Arbitrary bytes data from the caller
function _processVoteWithParams(
address voter,
uint256[] calldata points,
uint256 votingPower,
bytes calldata additionalData
) internal virtual {
_processVote(voter, points, votingPower);
_handleAdditionalVoteData(voter, points, votingPower, additionalData);
}

/// @notice Hook for downstream implementations to act on additional vote data
/// @dev No-op by default. Override in implementations that need to act on
/// the additionalData bytes parameter (e.g., multiplier indices, metadata).
/// @param voter Address of the voter
/// @param points Array of points allocated to each recipient
/// @param votingPower Total voting power of the voter
/// @param additionalData Arbitrary bytes data from the caller
function _handleAdditionalVoteData(
address voter,
uint256[] calldata points,
uint256 votingPower,
bytes calldata additionalData
) internal virtual {
// Default: no-op
}

/// @notice Validates vote points distribution
/// @dev Checks if points array is valid according to module rules
/// @param points Array of points to validate
Expand Down
96 changes: 96 additions & 0 deletions src/base/BasisPointsVotingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,66 @@ contract BasisPointsVotingModule is AbstractVotingModule {
_castSingleVote(voter, points, nonce, signature);
}

/// @notice Casts a vote with an EIP-712 signature and additional bytes parameters
/// @dev Same as castVoteWithSignature but also passes arbitrary bytes data to downstream
/// implementations (e.g., multiplier indices, metadata, conditional voting params).
/// See _handleAdditionalVoteData for the downstream hook.
/// @param voter The address of the voter casting the vote
/// @param points Array of basis points to allocate to each recipient
/// @param nonce Unique nonce for this vote to prevent replay attacks
/// @param signature EIP-712 signature authorizing this vote
/// @param additionalData Arbitrary bytes data passed to downstream implementations
function castVoteWithSignatureAndParams(
address voter,
uint256[] calldata points,
uint256 nonce,
bytes calldata signature,
bytes calldata additionalData
) external {
_castSingleVoteWithParams(voter, points, nonce, signature, additionalData);
}

/// @notice Casts a direct vote with additional bytes data (no EIP-712 signature required)
/// @dev msg.sender is the voter. Validates points, records the vote, and invokes the
/// _handleAdditionalVoteData hook. Reverts if the caller already voted this cycle.
/// Emits VoteWithData. See Issue #62.
/// @param points Array of basis points to allocate to each recipient
/// @param data Arbitrary bytes data forwarded to _handleAdditionalVoteData
function voteWithData(uint256[] calldata points, bytes calldata data) external {
if (hasVotedInCurrentCycle(msg.sender)) revert AlreadyVotedInCurrentCycle();
if (!_validateVotePoints(points)) revert InvalidPointsDistribution();

uint256 votingPower = _calculateTotalVotingPower(msg.sender);
_processVoteWithParams(msg.sender, points, votingPower, data);

emit VoteWithData(msg.sender, points, data);
}

/// @notice Casts multiple direct votes with additional data in a single transaction
/// @dev Processes each (voter, points, data) tuple atomically. If any entry fails, the
/// entire batch reverts. Limited to MAX_BATCH_SIZE entries. Does NOT require
/// EIP-712 signatures — suitable for keeper / relayer flows where voting power is
/// determined by on-chain token holdings. See Issue #62.
/// @param voters Array of voter addresses
/// @param points Array of point allocations per voter
/// @param data Array of arbitrary bytes data per voter
function voteWithDataBatch(address[] calldata voters, uint256[][] calldata points, bytes[] calldata data)
external
onlyOwner
{
if (voters.length != points.length) revert ArrayLengthMismatch();
if (voters.length != data.length) revert ArrayLengthMismatch();
if (voters.length > MAX_BATCH_SIZE) revert BatchTooLarge();

for (uint256 i = 0; i < voters.length; i++) {
if (hasVotedInCurrentCycle(voters[i])) revert AlreadyVotedInCurrentCycle();
if (!_validateVotePoints(points[i])) revert InvalidPointsDistribution();
uint256 votingPower = _calculateTotalVotingPower(voters[i]);
_processVoteWithParams(voters[i], points[i], votingPower, data[i]);
emit VoteWithData(voters[i], points[i], data[i]);
}
}

/// @notice Casts multiple votes in a single transaction for gas efficiency
/// @dev Processes multiple votes atomically. If any vote fails, the entire batch reverts.
/// Limited to MAX_BATCH_SIZE votes per transaction to prevent gas limit issues.
Expand Down Expand Up @@ -252,6 +312,42 @@ contract BasisPointsVotingModule is AbstractVotingModule {
return true;
}

/// @notice Processes a single vote with additional data for downstream implementations
/// @dev Mirrors _castSingleVote but calls _processVoteWithParams to invoke the additionalData hook.
/// @param voter Address of the voter
/// @param points Array of points to allocate to each recipient
/// @param nonce Unique nonce for replay protection
/// @param signature EIP-712 signature from the voter
/// @param additionalData Arbitrary bytes data for downstream implementations
function _castSingleVoteWithParams(
address voter,
uint256[] calldata points,
uint256 nonce,
bytes calldata signature,
bytes calldata additionalData
) internal {
AbstractVotingModuleStorage storage $ = _getAbstractVotingModuleStorage();

if (isNonceUsed(voter, nonce)) revert NonceAlreadyUsed();
if (!_validateVotePoints(points)) revert InvalidPointsDistribution();
if (!validateSignature(voter, points, nonce, signature)) revert InvalidSignature();

$.usedNonces[voter][nonce] = true;

uint256 votingPower = _calculateTotalVotingPower(voter);
_processVoteWithParams(voter, points, votingPower, additionalData);

emit VoteCastWithParams(voter, points, votingPower, nonce, signature, additionalData);
}

/// @notice Hook for downstream implementations to act on additional vote data
/// @dev BasisPointsVotingModule does not use additionalData — override this in
/// downstream implementations that need it (e.g., multiplier indices, metadata).
/// Marked virtual so that subclasses can override without mutability restrictions.
function _handleAdditionalVoteData(address, uint256[] calldata, uint256, bytes calldata) internal virtual override {
// BasisPointsVotingModule: no-op. Override in downstream implementations.
}

// Issue #43: Store required votes at proposal creation in VotingRecipientRegistry
// https://github.com/BreadchainCoop/breadkit/issues/43
// TODO: Implement when VotingRecipientRegistry is added
Expand Down
9 changes: 9 additions & 0 deletions src/interfaces/IVotingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ interface IVotingModule {
/// @notice Thrown when a zero address is provided
error ZeroAddress();

/// @notice Thrown when a voter tries to vote twice in the same cycle via voteWithData
error AlreadyVotedInCurrentCycle();

// ============ Events ============

/// @notice Emitted when a vote is cast with a signature
Expand All @@ -61,6 +64,12 @@ interface IVotingModule {
/// @param nonces Array of nonces used
event BatchVotesCast(address[] voters, uint256[] nonces);

/// @notice Emitted when a direct vote with additional data is cast
/// @param voter The voter who cast the vote
/// @param points The voting points allocated to each recipient
/// @param data Arbitrary bytes data passed by the caller
event VoteWithData(address indexed voter, uint256[] points, bytes data);

/// @notice Emitted when the voting module is initialized
/// @param strategies Array of voting power strategies
event VotingModuleInitialized(IVotingPowerStrategy[] strategies);
Expand Down
Loading