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
27 changes: 27 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@
- [MsgUpdateClearingAccountMappings](#tx.pse.v1.MsgUpdateClearingAccountMappings)
- [MsgUpdateDistributionSchedule](#tx.pse.v1.MsgUpdateDistributionSchedule)
- [MsgUpdateExcludedAddresses](#tx.pse.v1.MsgUpdateExcludedAddresses)
- [MsgUpdateMinDistributionGap](#tx.pse.v1.MsgUpdateMinDistributionGap)

- [Msg](#tx.pse.v1.Msg)

Expand Down Expand Up @@ -5818,6 +5819,7 @@ During distribution, the allocated amount is split equally among all recipients.
```
ScheduledDistribution defines a single allocation event at a specific timestamp.
Multiple clearing accounts can allocate tokens at the same time.
Each distribution is identified by a unique, sequential id.
```


Expand All @@ -5826,6 +5828,7 @@ Multiple clearing accounts can allocate tokens at the same time.
| ----- | ---- | ----- | ----------- |
| `timestamp` | [uint64](#uint64) | | `timestamp is when this allocation should occur (Unix timestamp in seconds).` |
| `allocations` | [ClearingAccountAllocation](#tx.pse.v1.ClearingAccountAllocation) | repeated | `allocations is the list of amounts to allocate from each clearing account at this time.` |
| `id` | [uint64](#uint64) | | `id is the unique, sequential identifier for this distribution. Used as the storage key in the AllocationSchedule map.` |



Expand Down Expand Up @@ -5996,6 +5999,7 @@ Params store gov manageable parameters.
| ----- | ---- | ----- | ----------- |
| `excluded_addresses` | [string](#string) | repeated | `excluded_addresses is a list of addresses excluded from PSE distribution. This list includes account addresses that should not receive PSE rewards. Can be modified via governance proposals.` |
| `clearing_account_mappings` | [ClearingAccountMapping](#tx.pse.v1.ClearingAccountMapping) | repeated | `clearing_account_mappings defines the mapping between clearing accounts and their sub accounts (multisig wallets). These mappings can be modified via governance proposals.` |
| `min_distribution_gap_seconds` | [uint64](#uint64) | | `min_distribution_gap_seconds is the minimum required gap in seconds between consecutive distributions.` |



Expand Down Expand Up @@ -6327,6 +6331,28 @@ All existing distributions are removed and replaced with the provided distributi




<a name="tx.pse.v1.MsgUpdateMinDistributionGap"></a>

### MsgUpdateMinDistributionGap

```
MsgUpdateMinDistributionGap is a governance operation to update the minimum time gap
between consecutive scheduled distributions. The new gap is validated against the
existing on-chain schedule to ensure consistency.
```



| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `authority` | [string](#string) | | `authority is the address authorized to update the gap (governance module address).` |
| `min_distribution_gap_seconds` | [uint64](#uint64) | | `min_distribution_gap_seconds is the minimum time gap (in seconds) between consecutive distributions.` |





<!-- end messages -->

<!-- end enums -->
Expand All @@ -6349,6 +6375,7 @@ Msg defines the Msg service.
| `UpdateClearingAccountMappings` | [MsgUpdateClearingAccountMappings](#tx.pse.v1.MsgUpdateClearingAccountMappings) | [EmptyResponse](#tx.pse.v1.EmptyResponse) | `UpdateClearingAccountMappings is a governance operation to update clearing account to recipient mappings.` | |
| `UpdateDistributionSchedule` | [MsgUpdateDistributionSchedule](#tx.pse.v1.MsgUpdateDistributionSchedule) | [EmptyResponse](#tx.pse.v1.EmptyResponse) | `UpdateDistributionSchedule is a governance operation to update the distribution schedule.` | |
| `DisableDistributions` | [MsgDisableDistributions](#tx.pse.v1.MsgDisableDistributions) | [EmptyResponse](#tx.pse.v1.EmptyResponse) | `DisableDistributions is a governance operation to disable distributions.` | |
| `UpdateMinDistributionGap` | [MsgUpdateMinDistributionGap](#tx.pse.v1.MsgUpdateMinDistributionGap) | [EmptyResponse](#tx.pse.v1.EmptyResponse) | `UpdateMinDistributionGap is a governance operation to update the minimum gap between distributions.` | |

<!-- end services -->

Expand Down
12 changes: 11 additions & 1 deletion docs/static/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -16804,6 +16804,11 @@
"$ref": "#/definitions/tx.pse.v1.ClearingAccountMapping"
},
"description": "clearing_account_mappings defines the mapping between clearing accounts and their sub accounts (multisig wallets).\nThese mappings can be modified via governance proposals."
},
"min_distribution_gap_seconds": {
"type": "string",
"format": "uint64",
"description": "min_distribution_gap_seconds is the minimum required gap in seconds between consecutive distributions."
}
},
"description": "Params store gov manageable parameters."
Expand Down Expand Up @@ -16874,9 +16879,14 @@
"$ref": "#/definitions/tx.pse.v1.ClearingAccountAllocation"
},
"description": "allocations is the list of amounts to allocate from each clearing account at this time."
},
"id": {
"type": "string",
"format": "uint64",
"description": "id is the unique, sequential identifier for this distribution.\nUsed as the storage key in the AllocationSchedule map."
}
},
"description": "ScheduledDistribution defines a single allocation event at a specific timestamp.\nMultiple clearing accounts can allocate tokens at the same time."
"description": "ScheduledDistribution defines a single allocation event at a specific timestamp.\nMultiple clearing accounts can allocate tokens at the same time.\nEach distribution is identified by a unique, sequential id."
}
}
}
10 changes: 7 additions & 3 deletions integration-tests/modules/pse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,16 @@ func TestPSEDistribution(t *testing.T) {

chain.Governance.ExpeditedProposalFromMsgAndVote(
ctx, t, nil, "-", "-", "-", govtypesv1.OptionYes,
&psetypes.MsgUpdateMinDistributionGap{
Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(),
MinDistributionGapSeconds: 0,
},
&psetypes.MsgUpdateDistributionSchedule{
Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(),
Schedule: []psetypes.ScheduledDistribution{
{Timestamp: uint64(distributionStartTime.Add(30 * time.Second).Unix()), Allocations: allocations},
{Timestamp: uint64(distributionStartTime.Add(60 * time.Second).Unix()), Allocations: allocations},
{Timestamp: uint64(distributionStartTime.Add(90 * time.Second).Unix()), Allocations: allocations},
{ID: 1, Timestamp: uint64(distributionStartTime.Add(30 * time.Second).Unix()), Allocations: allocations},
{ID: 2, Timestamp: uint64(distributionStartTime.Add(60 * time.Second).Unix()), Allocations: allocations},
{ID: 3, Timestamp: uint64(distributionStartTime.Add(90 * time.Second).Unix()), Allocations: allocations},
},
},
&psetypes.MsgUpdateClearingAccountMappings{
Expand Down
8 changes: 8 additions & 0 deletions proto/tx/pse/v1/distribution.proto
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ message ClearingAccountAllocation {

// ScheduledDistribution defines a single allocation event at a specific timestamp.
// Multiple clearing accounts can allocate tokens at the same time.
// Each distribution is identified by a unique, sequential id.
message ScheduledDistribution {
// timestamp is when this allocation should occur (Unix timestamp in seconds).
uint64 timestamp = 1 [
Expand All @@ -54,5 +55,12 @@ message ScheduledDistribution {
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"allocations\""
];

// id is the unique, sequential identifier for this distribution.
// Used as the storage key in the AllocationSchedule map.
uint64 id = 3 [
(gogoproto.customname) = "ID",
(gogoproto.moretags) = "yaml:\"id\""
];
}

5 changes: 5 additions & 0 deletions proto/tx/pse/v1/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ message Params {
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"clearing_account_mappings\""
];

// min_distribution_gap_seconds is the minimum required gap in seconds between consecutive distributions.
uint64 min_distribution_gap_seconds = 3 [
(gogoproto.moretags) = "yaml:\"min_distribution_gap_seconds\""
];
}
19 changes: 19 additions & 0 deletions proto/tx/pse/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ service Msg {

// DisableDistributions is a governance operation to disable distributions.
rpc DisableDistributions(MsgDisableDistributions) returns (EmptyResponse);

// UpdateMinDistributionGap is a governance operation to update the minimum gap between distributions.
rpc UpdateMinDistributionGap(MsgUpdateMinDistributionGap) returns (EmptyResponse);
}

message MsgDisableDistributions {
Expand Down Expand Up @@ -90,4 +93,20 @@ message MsgUpdateDistributionSchedule {
];
}

// MsgUpdateMinDistributionGap is a governance operation to update the minimum time gap
// between consecutive scheduled distributions. The new gap is validated against the
// existing on-chain schedule to ensure consistency.
message MsgUpdateMinDistributionGap {
option (cosmos.msg.v1.signer) = "authority";
option (amino.name) = "pse/MsgUpdateMinDistributionGap";

// authority is the address authorized to update the gap (governance module address).
string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];

// min_distribution_gap_seconds is the minimum time gap (in seconds) between consecutive distributions.
uint64 min_distribution_gap_seconds = 2 [
(gogoproto.moretags) = "yaml:\"min_distribution_gap_seconds\""
];
}

message EmptyResponse {}
1 change: 1 addition & 0 deletions x/deterministicgas/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ func DefaultConfig() Config {
&psetypes.MsgUpdateClearingAccountMappings{},
&psetypes.MsgUpdateDistributionSchedule{},
&psetypes.MsgDisableDistributions{},
&psetypes.MsgUpdateMinDistributionGap{},

// distribution
&distributiontypes.MsgUpdateParams{}, // This is non-deterministic because all the gov proposals are non-deterministic anyway
Expand Down
4 changes: 2 additions & 2 deletions x/deterministicgas/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ func TestDeterministicGas_DeterministicMessages(t *testing.T) {
// To make sure we do not increase/decrease deterministic and extension types accidentally,
// we assert length to be equal to exact number, so each change requires
// explicit adjustment of tests.
assert.Equal(t, 94, nondeterministicMsgCount)
assert.Equal(t, 95, nondeterministicMsgCount)
assert.Equal(t, 68, deterministicMsgCount)
assert.Equal(t, 12, extensionMsgCount)
assert.Equal(t, 150, nonExtensionMsgCount)
assert.Equal(t, 151, nonExtensionMsgCount)
}

func TestDeterministicGas_GasRequiredByMessage(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions x/deterministicgas/spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ and the formula for them is
| `/tx.pse.v1.MsgUpdateClearingAccountMappings` |
| `/tx.pse.v1.MsgUpdateDistributionSchedule` |
| `/tx.pse.v1.MsgUpdateExcludedAddresses` |
| `/tx.pse.v1.MsgUpdateMinDistributionGap` |

[//]: # (GENERATED DOC.)
[//]: # (DO NOT EDIT MANUALLY!!!)
60 changes: 49 additions & 11 deletions x/pse/keeper/distribution.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (k Keeper) ProcessNextDistribution(ctx context.Context) error {
}

// Remove the completed distribution from the schedule
if err := k.AllocationSchedule.Remove(ctx, timestamp); err != nil {
if err := k.AllocationSchedule.Remove(ctx, scheduledDistribution.ID); err != nil {
return err
}

Expand All @@ -64,7 +64,7 @@ func (k Keeper) ProcessNextDistribution(ctx context.Context) error {
func (k Keeper) PeekNextAllocationSchedule(ctx context.Context) (types.ScheduledDistribution, bool, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)

// Get iterator for the allocation schedule (sorted by timestamp ascending)
// Get iterator for the allocation schedule (sorted by id ascending)
iter, err := k.AllocationSchedule.Iterate(ctx, nil)
if err != nil {
return types.ScheduledDistribution{}, false, err
Expand All @@ -76,18 +76,18 @@ func (k Keeper) PeekNextAllocationSchedule(ctx context.Context) (types.Scheduled
return types.ScheduledDistribution{}, false, nil
}

// Extract the earliest scheduled distribution
// Extract the earliest scheduled distribution (sorted by id ascending)
kv, err := iter.KeyValue()
if err != nil {
return types.ScheduledDistribution{}, false, err
}

timestamp := kv.Key
scheduledDist := kv.Value

// Check if distribution time has arrived
// Since the map is sorted by timestamp, if the first item is in the future, all items are
shouldProcess := timestamp <= uint64(sdkCtx.BlockTime().Unix())
// Since IDs are sequential and timestamps are monotonically increasing,
// the first item by ID is also the earliest by time.
shouldProcess := scheduledDist.Timestamp <= uint64(sdkCtx.BlockTime().Unix())

return scheduledDist, shouldProcess, nil
}
Expand Down Expand Up @@ -217,18 +217,18 @@ func (k Keeper) distributeAllocatedTokens(
}

// SaveDistributionSchedule persists the distribution schedule to blockchain state.
// Each scheduled distribution is stored in the AllocationSchedule map, indexed by its timestamp.
// Each scheduled distribution is stored in the AllocationSchedule map, indexed by its ID.
func (k Keeper) SaveDistributionSchedule(ctx context.Context, schedule []types.ScheduledDistribution) error {
for _, scheduledDist := range schedule {
if err := k.AllocationSchedule.Set(ctx, scheduledDist.Timestamp, scheduledDist); err != nil {
return errorsmod.Wrapf(err, "failed to save distribution at timestamp %d", scheduledDist.Timestamp)
if err := k.AllocationSchedule.Set(ctx, scheduledDist.ID, scheduledDist); err != nil {
return errorsmod.Wrapf(err, "failed to save distribution with id %d", scheduledDist.ID)
}
}
return nil
}

// GetDistributionSchedule returns the complete allocation schedule as a sorted list.
// The schedule is sorted by timestamp in ascending order.
// The schedule is sorted by id in ascending order.
// Returns an empty slice if no allocations are scheduled.
// Note: Past schedule allocations removed after processing, so this only contains future schedule allocations.
func (k Keeper) GetDistributionSchedule(ctx context.Context) ([]types.ScheduledDistribution, error) {
Expand All @@ -248,7 +248,7 @@ func (k Keeper) GetDistributionSchedule(ctx context.Context) ([]types.ScheduledD
schedule = append(schedule, kv.Value)
}

// Note: Collections map iterates in ascending order of keys (timestamps),
// Note: Collections map iterates in ascending order of keys (IDs),
// so the schedule is already sorted. No need to sort again.
return schedule, nil
}
Expand All @@ -266,6 +266,15 @@ func (k Keeper) UpdateDistributionSchedule(
return errorsmod.Wrapf(types.ErrInvalidAuthority, "expected %s, got %s", k.authority, authority)
}

// Validate minimum gap between distributions
params, err := k.GetParams(ctx)
if err != nil {
return err
}
if err := types.ValidateDistributionGap(newSchedule, params.MinDistributionGapSeconds); err != nil {
return err
}

// Clear all existing schedule entries
if err := k.AllocationSchedule.Clear(ctx, nil); err != nil {
return errorsmod.Wrap(err, "failed to clear existing allocation schedule")
Expand All @@ -275,6 +284,35 @@ func (k Keeper) UpdateDistributionSchedule(
return k.SaveDistributionSchedule(ctx, newSchedule)
}

// UpdateMinDistributionGap updates the minimum time gap between distributions via governance.
// The new gap is validated against the existing on-chain schedule to ensure consistency.
func (k Keeper) UpdateMinDistributionGap(
ctx context.Context,
authority string,
minGapSeconds uint64,
) error {
if k.authority != authority {
return errorsmod.Wrapf(types.ErrInvalidAuthority, "expected %s, got %s", k.authority, authority)
}

// Validate new gap against existing schedule
schedule, err := k.GetDistributionSchedule(ctx)
if err != nil {
return err
}
if err := types.ValidateDistributionGap(schedule, minGapSeconds); err != nil {
return errorsmod.Wrapf(err, "existing schedule violates proposed min gap of %d seconds", minGapSeconds)
}

// Update params
params, err := k.GetParams(ctx)
if err != nil {
return err
}
params.MinDistributionGapSeconds = minGapSeconds
return k.SetParams(ctx, params)
}

// DisableDistributions is a governance operation that disables distributions.
func (k Keeper) DisableDistributions(ctx context.Context, authority string) error {
// Check authority
Expand Down
6 changes: 5 additions & 1 deletion x/pse/keeper/distribution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func TestDistribution_GenesisRebuild(t *testing.T) {
// Create and store allocation schedule with all clearing accounts
schedule := []types.ScheduledDistribution{
{
ID: 1,
Timestamp: time1,
Allocations: []types.ClearingAccountAllocation{
{ClearingAccount: types.ClearingAccountCommunity, Amount: sdkmath.NewInt(5000)},
Expand All @@ -74,6 +75,7 @@ func TestDistribution_GenesisRebuild(t *testing.T) {
},
},
{
ID: 2,
Timestamp: time2,
Allocations: []types.ClearingAccountAllocation{
{ClearingAccount: types.ClearingAccountCommunity, Amount: sdkmath.NewInt(10000)},
Expand All @@ -88,7 +90,7 @@ func TestDistribution_GenesisRebuild(t *testing.T) {

// Store in allocation schedule map
for _, scheduledDist := range schedule {
err = pseKeeper.AllocationSchedule.Set(ctx, scheduledDist.Timestamp, scheduledDist)
err = pseKeeper.AllocationSchedule.Set(ctx, scheduledDist.ID, scheduledDist)
requireT.NoError(err)
}

Expand Down Expand Up @@ -183,6 +185,7 @@ func TestDistribution_PrecisionWithMultipleRecipients(t *testing.T) {
startTime := uint64(time.Now().Add(-1 * time.Hour).Unix())
schedule := []types.ScheduledDistribution{
{
ID: 1,
Timestamp: startTime,
Allocations: []types.ClearingAccountAllocation{
{ClearingAccount: types.ClearingAccountFoundation, Amount: allocationAmount},
Expand Down Expand Up @@ -314,6 +317,7 @@ func TestDistribution_EndBlockFailure(t *testing.T) {
startTime := uint64(time.Now().Add(-1 * time.Hour).Unix())
schedule := []types.ScheduledDistribution{
{
ID: 1,
Timestamp: startTime,
Allocations: []types.ClearingAccountAllocation{
{ClearingAccount: types.ClearingAccountFoundation, Amount: allocationAmount},
Expand Down
4 changes: 2 additions & 2 deletions x/pse/keeper/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (k Keeper) InitGenesis(ctx context.Context, genState types.GenesisState) er

// Populate allocation schedule from genesis state
for _, scheduledDist := range genState.ScheduledDistributions {
if err := k.AllocationSchedule.Set(ctx, scheduledDist.Timestamp, scheduledDist); err != nil {
if err := k.AllocationSchedule.Set(ctx, scheduledDist.ID, scheduledDist); err != nil {
return err
}
}
Expand Down Expand Up @@ -70,7 +70,7 @@ func (k Keeper) ExportGenesis(ctx context.Context) (*types.GenesisState, error)
return nil, err
}

// Export allocation schedule using keeper method (already sorted by timestamp)
// Export allocation schedule using keeper method (already sorted by id)
genesis.ScheduledDistributions, err = k.GetDistributionSchedule(ctx)
if err != nil {
return nil, err
Expand Down
Loading
Loading