Skip to content
Open
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
166 changes: 166 additions & 0 deletions docs/architecture/RELEASE_STAKE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Release Stake

## Overview

`release_stake` is a message in the restaking system that allows a **consumer contract** to request the release of tokens from its provider contract.

### Message Structure
```rust
ReleaseStake {
staker: String,
amount: Uint128,
denom: String,
}
```

## Core Process
1. **Initiation:** A consumer contract sends the `release_stake` message to the provider.
2. **Validation:** The provider validates the request by checking the amount and ensuring proper authorization.
3. **State Update:** Stake accounting is updated in the provider contract, reflecting the released tokens.
3.1. *Question:* shouldn't this force the clearing of any unbonding claims and/or finalizing pending reward distributions.
4. **Finalization:** The provider processes any unbonding claims and optionally forwards the `release_stake` message up the chain.

> **Exception:** If the contract is the ultimate provider (e.g., Token Staking), the process terminates by sending a bank transfer to the original staker.

*Question:* my understanding is that during the unbonding period, the tokens remain locked. During this period they still may be slashed or changed (delegation scenarios). So then when this period has ended, and any pending rewards are accounted for/distributed, can `release_stake` happen.

## Contract Behaviors
The behavior of `release_stake` varies depending on the contract type. Contracts are categorized into **Pure Pass-Through Contracts** and **State-Mutating Contracts**.

### Pure Pass-Through Contracts
These contracts do not alter the stake state and simply forward the message upstream to the provider:

#### 1. Token Staking
- Receives `release_stake` from a consumer.
- Creates a **Bank Send** transaction to the staker.
- Terminates the flow; does not forward to a provider as it is a leaf node.

#### 2. Fan In
- Maps output denominations to input denominations and forwards the request to the correct provider.
- Ensures the input denom is correctly remapped.

### State-Mutating Contracts
These contracts manage stake accounting and modify state:

#### 1. Token Weighting
- Receives `release_stake` after unbonding.
- Makes sure it’s not recalculating weights before propagating `release_stake`.
- Adjusts **token weight ratios** and stake state.
- Forwards the message to the provider.

#### 2. Delegations

The delegations contract manages stake allocation across multiple delegates. When processing `release_stake`, it must handle proportional distribution of released amounts across delegators.

**Key Challenges:**
- Must handle potentially large numbers of delegators (1000+)
- Needs to maintain proper proportional distributions
- Cannot iterate through all delegators due to gas limits
- Must handle multiple release_stake requests during unbonding period
- Must update delegation ratios after release

**State Management:**
```rust
#[cw_serde]
pub struct UnbondingClaim {
pub total_unbonding: Uint128, // Amount being unbonded in this claim
pub released_amount: Uint128, // Amount processed/released so far
pub end_time: u64, // When unbonding will end
}

// Maps (denom, delegate) -> vector of unbonding claims
pub const DELEGATE_UNBONDING: Map<(&str, &Addr), Vec<UnbondingClaim>>;
```

**Process Flow:**

1. When consumer calls `release_stake`:
```rust
pub fn release_stake(
deps: DepsMut,
env: Env,
info: MessageInfo,
staker: String, // delegate address
amount: Uint128,
denom: String,
) -> Result<Response, ContractError> {
// Validate sender is consumer contract
// Verify delegate has sufficient stake
// Verify no pending rewards need distribution
// Record historical stake amount for proper reward calculation
// Create new UnbondingClaim
// Update total staked amount
// Emit events
}
```

2. When delegator calls `release_unbonded`:
```rust
pub fn release_unbonded(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
// For each delegate where delegator has stake:
// For each unprocessed claim:
// If unbonding period complete:
// Update delegator's stake and liens
// Recalculate delegation ratios:
// - Get all current delegations
// - Subtract the released amount
// - Normalize remaining delegations to sum to 100%
// - Update STAKER_DELEGATIONS
// Forward portion upstream via release_stake message
// Mark as processed
// Update released_amount in claim

// Send accumulated messages
}
```

#### 3. Fan Out
- Handles multiple output flows.
- Updates **outflow accounting** to reflect the proportionate release.
- Maintains unbonding claims instead of directly triggering `release_stake`.
- The staker must call `ReleaseUnbonded` to propagate `release_stake` upstream.

#### 4. Splitter
- Maintains split proportions for multiple consumers.
- Updates split accounting based on the `release_stake` message.

#### 5. Operators
- Maps the **operator address** to the original staker address.
- Updates stake accounting and ensures the message flows through correctly.
- Maintains unbonding claims until the staker calls `ReleaseUnbonded`.
Comment on lines +131 to +134
Copy link
Copy Markdown
Member

@ueco-jb ueco-jb Jan 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Updates stake accounting and ensures the message flows through correctly.

I was thinking about some specific use cases of unset_operator - that might trigger several unstake events down the line. New operator is set, so the staking new funds (and unstaking those) should be allowed as well.
Now imagine the case with the operators:

  • operator1, staked 1000 token
  • set to operator2, 1000 tokens are locked
  • in the meantime, stake more tokens now to the operator2
  • at this point, should another unset be allowed?

I think this case would be mitigated if we would not allow for more then one unbonding per staker. If you already have an active one, then you can still stake or unstake but that's it.

I know this is a rather general doc, but wanted to share some thoughts.


## State of Contracts Implementing The Release Stake


| Contract | `release_stake` Implementation | Current Status | Needed Changes |
| --------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Token Staking | `release_stake` implemented | Fully Implemented | Tokens are available for immediate withdrawal via `withdraw_unbonded`. |
| Fan In | `release_stake` updates `TOTAL_STAKE` and forwards upstream | Partially Implemented | Add reward accounting (not mandatory), we need partial/full release policy, add validation before forwarding |
| Fan Out | `release_stake` implemented: updates state and liens, but user must call `ReleaseUnbonded` to propagate upstream | Partially Implemented | Needs integration of reward logic (right now is `todo!()` in `withdraw_rewards`/`distribute_rewards`), ensure that all claims and outflows are settled before user triggers `ReleaseUnbonded` to finally push `release_stake` upstream |
| Delegations | still `todo!()` | Not Implemented | ensure delegators’ claims settled, distribute rewards, maybe also handle partial/full release before forwarding |
| Token Weighting | no explicit `release_stake` present | Not Implemented | implement `release_stake`: ensure no weight recalculations in progress, fix token weight ratios, then forward upstream |
| Operators | `release_stake` implemented | Partially Implemented | make sure that operator removal and slashing are in consideration. Ensure unbonding claims handling until `ReleaseUnbonded` called by staker, then forward upstream |
| Splitter | `release_stake` is still `todo!()` | Not Implemented | Implement logic for multiple splits, handle rewards/unbonding claims, then forward |

## Known Trigger Points
`release_stake` can be triggered in several scenarios:

1. **User-Initiated Claims** (ReleaseUnbonded):
- A user calls `ReleaseUnbonded` on a contract after the unbonding period expires.
- This triggers `release_stake` messages to propagate up to the provider.

2. **Administrative Actions:**
- **DefineFlow (Fan Out):** Changing outflows creates unbonding claims for old outflows.
- **SetOperator (Operators):** Changing operators results in unbonding claims for the old operator.
- **DefineDelegates (Delegations):** Modifying delegation configuration creates unbonding claims that are finalized via `ReleaseUnbonded`.

## Questions
- when it comes to validating the request by the `provider`, should the `provider` always trust the `consumer's` request (`consumer` is always whitelisted) or the `provider` should do additional validation - eg, check that the request doesn't exceed what's already staked?
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to trust the consumer's request. The simple things like "exceed what's staked" should be caught by the checked math operations, and the provider doesn't really have any context of how the release_stake amount was calculated anyway.

- do we allow for partial `release_stake` - eg, a consumer would like to free 1/3 of the staked tokens
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will be up to the provider contract to handle its own stake logic correctly.

- for `token_weighting` contract: what happens when the token-weighting contract receieve a msg to release stake when it's currently calculating new weight? Basically both requests in the same block
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice thinking. The only case we need to avoid here is where we might call may_load_at_height at the current height as opposed to the correct may_load. This should already be avoided for the most part, but it's good to keep in mind.

- for `fan-out` contract: what if we change the denom and request `release_stake` on the old name? It will fail ofc, but I guess we have to call the old denom name.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we support renaming a denom after it has been defined.

- for `operators` contract: that's for the future, but shouldn't we check if an operator is being slashed, before releasing the stake, there might be better options? Another one is to proceed with the removal of operators and their stake