PSE Multi-Recipient mappping for clearing accounts#21
Conversation
There was a problem hiding this comment.
Pull Request Overview
This pull request introduces support for multiple recipients per clearing account in the PSE module's fund distribution system. Previously limited to a single recipient per clearing account, the system now supports one or more recipients with precise equal distribution of allocations and remainder handling to ensure no tokens are lost to rounding.
Key Changes:
- Updated protocol buffers to use
repeated recipient_addressesinstead of singlerecipient_address, with a newRecipientDistributionmessage for detailed event tracking - Modified distribution logic to split allocations equally among recipients, with any remainder going to the first recipient to guarantee 100% distribution
- Enhanced validation to prevent Community clearing account mappings, check for empty recipient lists, and detect duplicate recipients within the same clearing account
Reviewed Changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
proto/tx/pse/v1/distribution.proto |
Changed ClearingAccountMapping to use repeated recipient_addresses field with updated documentation |
proto/tx/pse/v1/event.proto |
Added RecipientDistribution message and updated EventAllocationDistributed to include recipients list and distribution details |
x/pse/types/distribution.pb.go |
Generated code for the updated distribution proto with repeated field handling |
x/pse/types/event.pb.go |
Generated code for new RecipientDistribution message and updated event structure |
x/pse/types/params.go |
Enhanced validation logic to check Community account restrictions, empty recipient lists, invalid addresses, and duplicate recipients |
x/pse/types/params_test.go |
Comprehensive test coverage for multi-recipient scenarios, validation edge cases, and Community account restrictions |
x/pse/keeper/distribution.go |
Implemented equal distribution logic with remainder handling and updated event emission for multiple recipients |
x/pse/keeper/distribution_test.go |
Added precision tests for multi-recipient distribution with remainder handling verification |
x/pse/keeper/genesis_test.go |
Updated initialization tests to use recipient address arrays |
x/pse/keeper/params_test.go |
Updated parameter tests to use new repeated field structure |
app/upgrade/v6/pse_init.go |
Modified default mappings to initialize with single-element arrays for recipient addresses |
app/upgrade/v6/pse_init_test.go |
Enhanced upgrade tests to verify multi-recipient distribution scenarios with both single and multiple recipients |
docs/api.md |
Updated API documentation to reflect new multi-recipient structure and distribution behavior |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Community clearing account should NOT have recipient mappings | ||
| // It uses score-based distribution logic instead | ||
| if mapping.ClearingAccount == ClearingAccountCommunity { | ||
| return errorsmod.Wrapf(ErrInvalidParam, "mapping %d: community clearing account cannot have recipient mappings", i) | ||
| } |
There was a problem hiding this comment.
The Community clearing account validation should be performed before other validations. If a Community mapping exists, the function returns early, but it would be more efficient and clearer to check this condition before validating the recipient addresses list (lines 97-113). Consider moving this check to immediately after line 88 where the clearing_account emptiness is checked.
| // If multiple recipients exist, the amount is split equally among them. | ||
| // Distribution always allocates 100% of the total amount with no tokens lost to rounding: | ||
| // - Base amount per recipient: total_amount / num_recipients (integer division) | ||
| // - Any remainder from division goes to the first recipient |
There was a problem hiding this comment.
[nitpick] The comment describes the distribution logic in detail, which is helpful. However, this level of implementation detail in the proto file might be better suited for code comments. Consider keeping the proto comments more concise and focused on the message structure, moving the detailed distribution logic explanation to the keeper implementation comments.
| // If multiple recipients exist, the amount is split equally among them. | |
| // Distribution always allocates 100% of the total amount with no tokens lost to rounding: | |
| // - Base amount per recipient: total_amount / num_recipients (integer division) | |
| // - Any remainder from division goes to the first recipient | |
| // The total amount is distributed among recipients, and the sum of all recipient amounts equals total_amount. |
|
|
||
| // DefaultClearingAccountMappings returns the default clearing account mappings for the given chain ID. | ||
| // Community clearing account is not included in the mappings. | ||
| // Each clearing account has a single default recipient address. |
There was a problem hiding this comment.
The comment on line 74 states "Each clearing account has a single default recipient address" which might be misleading. While it's true for the default initialization, the system now supports multiple recipients per clearing account. Consider clarifying: "Each clearing account is initialized with a single default recipient address (can be expanded to multiple recipients via governance)".
| // Each clearing account has a single default recipient address. | |
| // Each clearing account is initialized with a single default recipient address (can be expanded to multiple recipients via governance). |
| requireT.NotEmpty(mapping.RecipientAddresses, | ||
| "mapping for %s should have at least one recipient address", mapping.ClearingAccount) |
There was a problem hiding this comment.
Duplicate validation check: Lines 56-59 contain duplicate checks for RecipientAddresses. The first check on line 56-57 verifies the field is not empty, and then lines 58-59 repeat essentially the same check. One of these should be removed.
| requireT.NotEmpty(mapping.RecipientAddresses, | |
| "mapping for %s should have at least one recipient address", mapping.ClearingAccount) |
| // Community clearing account should NOT have recipient mappings | ||
| // It uses score-based distribution logic instead | ||
| if mapping.ClearingAccount == ClearingAccountCommunity { | ||
| return errorsmod.Wrapf(ErrInvalidParam, "mapping %d: community clearing account cannot have recipient mappings", i) |
There was a problem hiding this comment.
The error message should be capitalized consistently. Change "community" to "Community" to match the naming convention used in the codebase (e.g., line 92 uses "Community clearing account").
| return errorsmod.Wrapf(ErrInvalidParam, "mapping %d: community clearing account cannot have recipient mappings", i) | |
| return errorsmod.Wrapf(ErrInvalidParam, "mapping %d: Community clearing account cannot have recipient mappings", i) |
| // Use amount that doesn't divide evenly by 3 | ||
| allocationAmount := sdkmath.NewInt(1000) // 1000 / 3 = 333 remainder 1 |
There was a problem hiding this comment.
The comment says "Use amount that doesn't divide evenly by 3", but the actual amount (1000) divides evenly by 2 (for the Alliance account with 2 recipients). Consider updating the comment to be more comprehensive: "Use amount that doesn't divide evenly by 3 (for Foundation) and does divide evenly by 2 (for Alliance)" to clarify the test coverage.
| // Use amount that doesn't divide evenly by 3 | |
| allocationAmount := sdkmath.NewInt(1000) // 1000 / 3 = 333 remainder 1 | |
| // Use amount that doesn't divide evenly by 3 (for Foundation) and does divide evenly by 2 (for Alliance) | |
| allocationAmount := sdkmath.NewInt(1000) // 1000 / 3 = 333 remainder 1; 1000 / 2 = 500 remainder 0 |
| // Distribution Precision Handling: | ||
| // The allocation amount is split equally among all recipients using integer division. | ||
| // To ensure 100% of tokens are distributed with no loss to rounding: | ||
| // - Each recipient receives: allocation.Amount / numRecipients (base amount) | ||
| // - Any remainder from division goes to the first recipient | ||
| // This guarantees: sum(recipient_allocations) == allocation.Amount |
There was a problem hiding this comment.
The comment heading "Distribution Precision Handling:" should be consistent with other comment styles in the file. Consider using a simpler format like "Split allocation equally among recipients" or following the existing comment style without the colon and special formatting.
| // Distribution Precision Handling: | |
| // The allocation amount is split equally among all recipients using integer division. | |
| // To ensure 100% of tokens are distributed with no loss to rounding: | |
| // - Each recipient receives: allocation.Amount / numRecipients (base amount) | |
| // - Any remainder from division goes to the first recipient | |
| // This guarantees: sum(recipient_allocations) == allocation.Amount | |
| // Split allocation equally among recipients. | |
| // The allocation amount is divided using integer division. | |
| // To ensure 100% of tokens are distributed with no loss to rounding: | |
| // Each recipient receives allocation.Amount / numRecipients (base amount). | |
| // Any remainder from division goes to the first recipient. | |
| // This guarantees that the sum of recipient allocations equals allocation.Amount. |
ysv
left a comment
There was a problem hiding this comment.
@ysv reviewed 1 of 6 files at r2, all commit messages.
Reviewable status: 1 of 13 files reviewed, 10 unresolved discussions (waiting on @masihyeganeh, @miladz68, and @TxCorpi0x)
proto/tx/pse/v1/event.proto line 10 at r2 (raw file):
// RecipientDistribution represents a single recipient and the amount they received. message RecipientDistribution {
suggested name: AllocatedAmount
proto/tx/pse/v1/event.proto line 29 at r2 (raw file):
// recipients contains the list of recipients and the exact amount each received. // The sum of all recipient amounts equals total_amount. repeated RecipientDistribution recipients = 2 [
I think field name should be different. Maybe allocated_amounts
x/pse/keeper/distribution.go line 156 at r2 (raw file):
if i == 0 && !remainder.IsZero() { amountToSend = amountToSend.Add(remainder) }
I prefer to keep remainder undistributed. All account should be equally treated independently on order they have
Code quote:
// Calculate amount for this recipient
// First recipient gets the base amount plus any remainder to avoid dust
amountToSend := amountPerRecipient
if i == 0 && !remainder.IsZero() {
amountToSend = amountToSend.Add(remainder)
}
miladz68
left a comment
There was a problem hiding this comment.
@miladz68 reviewed 7 of 13 files at r1, 6 of 6 files at r2, all commit messages.
Reviewable status: all files reviewed, 10 unresolved discussions (waiting on @masihyeganeh and @TxCorpi0x)
TxCorpi0x
left a comment
There was a problem hiding this comment.
Reviewable status: all files reviewed, 10 unresolved discussions (waiting on @masihyeganeh and @ysv)
proto/tx/pse/v1/event.proto line 10 at r2 (raw file):
Previously, ysv (Yaroslav Savchuk) wrote…
suggested name: AllocatedAmount
These are actually distributed (this is being used for the events after distribution is done), may be DistributedAmount
proto/tx/pse/v1/event.proto line 29 at r2 (raw file):
Previously, ysv (Yaroslav Savchuk) wrote…
I think field name should be different. Maybe allocated_amounts
Or according to the previous comment, DistributedAmounts
x/pse/keeper/distribution.go line 156 at r2 (raw file):
Previously, ysv (Yaroslav Savchuk) wrote…
I prefer to keep remainder undistributed. All account should be equally treated independently on order they have
Imaging 1000 and 3 participants, each get 333.33, we need to send the truncated values to the addresses (bank module send accepts Int), in that case the sum of the amounts would not get equal to the allocated values in the allocations.
ysv
left a comment
There was a problem hiding this comment.
@ysv reviewed 7 of 13 files at r1, 4 of 6 files at r2, 1 of 1 files at r3, all commit messages.
Reviewable status: all files reviewed, 10 unresolved discussions (waiting on @masihyeganeh and @TxCorpi0x)
proto/tx/pse/v1/event.proto line 10 at r2 (raw file):
Previously, TxCorpi0x wrote…
These are actually distributed (this is being used for the events after distribution is done), may be
DistributedAmount
Both AllocatedAmount & DistributedAmount are good
proto/tx/pse/v1/event.proto line 29 at r2 (raw file):
Previously, TxCorpi0x wrote…
Or according to the previous comment,
DistributedAmounts
Both AllocatedAmount & DistributedAmount are good
x/pse/keeper/distribution.go line 156 at r2 (raw file):
Previously, TxCorpi0x wrote…
Imaging 1000 and 3 participants, each get 333.33, we need to send the truncated values to the addresses (bank module send accepts Int), in that case the sum of the amounts would not get equal to the allocated values in the allocations.
yes , I think it is better to do it this way.
We should probably also suggest numbers like 4,5, 8 accounts so it is divisible. But IMO it is better to leave this remainder, @miladz68 WDYT ?
miladz68
left a comment
There was a problem hiding this comment.
@miladz68 reviewed 1 of 1 files at r3, all commit messages.
Reviewable status: all files reviewed, 10 unresolved discussions (waiting on @masihyeganeh and @TxCorpi0x)
ysv
left a comment
There was a problem hiding this comment.
@ysv reviewed 6 of 6 files at r4, all commit messages.
Reviewable status: all files reviewed, 3 unresolved discussions (waiting on @masihyeganeh and @TxCorpi0x)
Description
This pull request introduces support for multiple recipients per clearing account in the fund distribution logic. Previously, each clearing account could only have a single recipient; now, each can have one or more, and allocations are split equally among all recipients with precise handling of any remainder. The changes cover updates to the protocol buffers, documentation, initialization, distribution logic, and tests to reflect and verify this new behavior.
Protocol and API changes:
ClearingAccountMappingto use a repeated fieldrecipient_addressesinstead of a singlerecipient_address, ensuring each clearing account can have multiple recipients.RecipientDistributionmessage to the event proto, and changed theEventAllocationDistributedto emit the actual amount sent to each recipient, with clear documentation about the split and remainder handling.Distribution logic changes:
x/pse/keeper/distribution.goto split allocation amounts equally among all recipients, with any remainder going to the first recipient, ensuring all tokens are distributed precisely.Initialization and test changes:
RecipientAddressesfield, and added scenarios for both single and multiple recipients per clearing account.Documentation improvements:
These updates ensure that fund allocations can be flexibly and accurately distributed to multiple recipients per clearing account, with clear auditability and no loss due to rounding.
Reviewers checklist:
Authors checklist
This change is