Skip to content
Merged
Changes from 16 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
310 changes: 310 additions & 0 deletions doc/bapp_onboarding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
# bApp Onboarding Guide

This guide outlines the steps for based applications developers looking to build on the bApps platform.

## 1. Creating and Configuring a bApp

1. **Define core attributes**:
- `bApp`: a unique 20-byte EVM address that uniquely identifies the bApp.
- `tokens`: A list of ERC-20 tokens to be used in the bApp's security mechanism. For the native ETH token, use the special address [`0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L62).
- `sharedRiskLevels`: a list with $\beta$ values, one for each token, representing the bApp's tolerance for risk (token over-usage).
2. **Optional Non-Slashable Validator Balance**: If the bApp uses non-slashable validator balance, it should be configured off-chain, in the bApp's network.
3. **Register the bApp**: Use the [`registerBApp`](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L249) function of the smart contract:
```solidity
function registerBApp(
address bApp,
address[] calldata tokens,
uint32[] calldata sharedRiskLevels,
string calldata metadataURI
)
```
- `metadataURI`: A link to a JSON file containing additional details about your bApp, such as its name, description, logo, and website.
4. **Update Configuration**: After registratering, the bApp configuration can be updated only by the `owner` account. Namely, more tokens can be added with [`addTokensToBApp`](https://github.com/ssvlabs/based-applications/blob/bd55fb02e517c52a7151d516f174f3c1562be502/src/BasedAppManager.sol#L279), the tokens' shared risk levels updated with [`updateBAppTokens`](https://github.com/ssvlabs/based-applications/blob/bd55fb02e517c52a7151d516f174f3c1562be502/src/BasedAppManager.sol#L293), and the metadata updated with [`updateMetadataURI`](https://github.com/ssvlabs/based-applications/blob/bd55fb02e517c52a7151d516f174f3c1562be502/src/BasedAppManager.sol#L271).

## 2. Securing the bApp

Once the bApp is registered, strategies can join it and allocate capital to secure it.

### 2.1 Opting in

Choose a reason for hiding this comment

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

I think we are missing how to register operators (accounts) and create strategy

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought it was a guide mainly for the bApp to register itself and understand how others join.
If strategies registering is really necessary, I can add it
Wdyt @liorrutenberg ?


The strategy opts-in to the bApp by using the [`optInToBApp`](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L333) function of the smart contract:
```solidity
function optInToBApp(
uint256 strategyId,
address bApp,
address[] calldata tokens,
uint32[] calldata obligationPercentages,
bytes calldata data
)
```
- `tokens`: List of tokens to obligate to the bApp.
- `obligationPercentages`: The proportion of each token's balance to commit to the bApp.

Choose a reason for hiding this comment

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

The encoding format of the obligation percentages is not clear.
We decided on 5000 representing 50.00% right?
It needs to be clear

Maybe @mtabasco or @riccardo-ssvlabs can provide an example of a cli-command that will show examples of how to create the transactions. This will make sure will use proper encoding when creating the txs.

I think clef allows using json as tx data, so maybe a json would be a good idea?
But the contract guys probably know what is the best way to give such examples

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well-spotted.
Because it's a uint32, we could have had far better precision than 2 decimals
But I think that's the current encoding, yes.
I'll add this mention to the encoding

Copy link
Contributor

Choose a reason for hiding this comment

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

I think one simple way to send transactions using a cli-command is with Foundry's cast.

# install foundry
curl -L https://foundry.paradigm.xyz | bash

cast send <CONTRACT_ADDRESS> "optInToBApp(uint256,address,address[],uint32[],bytes)" \
    <STRATEGY_ID> <BAPP_ADDRESS> \
    '["0xTokenAddress1","0xTokenAddress2"]' '[1000,2000]' "0xDataParam" \
    --private-key <YOUR_PRIVATE_KEY> --rpc-url <RPC_URL>

You can use --keystore param to access key store instead of passing raw private key to the cli command.

- `data`: An extra optional field for off-chain information required by the bApp for participation.

For example, if `tokens = [SSV]` and `obligationPercentages = [50%]`, then 50% of the strategy's `SSV` balance will be obligated to the bApp.

The strategy’s owner can later update its obligations by modifying existing ones or adding a new token obligation. Obligations can be increased instantly ([`fastUpdateObligation`](https://github.com/ssvlabs/based-applications/blob/bd55fb02e517c52a7151d516f174f3c1562be502/src/BasedAppManager.sol#L519)), but decreasing obligations requires a timelock ([`proposeUpdateObligation`](https://github.com/ssvlabs/based-applications/blob/bd55fb02e517c52a7151d516f174f3c1562be502/src/BasedAppManager.sol#L537) → [`finalizeUpdateObligation`](https://github.com/ssvlabs/based-applications/blob/bd55fb02e517c52a7151d516f174f3c1562be502/src/BasedAppManager.sol#L563)) to ensure slashable capital can’t be pulled out instantly.

### 2.2 Strategy's Funds

To compose their balances, strategies:
1. receive ERC20 (or ETH) via [**deposits**](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L376) from accounts.
2. inherent the non-slashable validator balance from its owner account. Accounts [**delegate**](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L201) validator balances between themselves, and the strategy inherits all balances delegated to its owner.

If a token is allocated to a bApp ([`usedTokens[strategyId][token] != 0`](https://github.com/ssvlabs/based-applications/blob/bd55fb02e517c52a7151d516f174f3c1562be502/src/BasedAppManager.sol#L127)), accounts need to propose a withdrawal and wait a timelock before finalizing it, ensuring the slashable collateral cannot be removed instantly.

## 3. Participant Weight

bApp clients track the weight of each participant in the bApp. For that, clients will:

1. **Gather Obligated Balances**: First, for each token used by the bApp, it should get the obligated balance from each strategy.
```go
ObligatedBalance mapping(Token -> Strategy -> Amount)
```
2. **Sum Obligations**: From `ObligatedBalance`, it can sum all obligations and compute the total amount obligated to the bApp by all strategies.
```go
TotalBAppBalance mapping(Token -> Amount)
```
3. **Calculate Risk**: For each token, it should get the risk (token-over usage) of each strategy.
```go
Risk mapping(Token -> Strategy -> Float)
```
4. **Compute Risk-Aware Weights**: With this information, it can compute the weight of a participant for a certain token by

$$W_{\text{strategy, token}} = c_{\text{token}} \times \frac{ObligatedBalance[\text{token}][\text{strategy}]}{TotalBAppBalance[\text{token}]} e^{-\beta_{\text{token}} \times max(1, Risk[\text{token}][\text{strategy}])}$$

where $c_{\text{token}}$ is a normalization constant defined as

$$c_{\text{token}} = \left( \sum_{\text{strategy}} \frac{ObligatedBalance[\text{token}][\text{strategy}]}{TotalBAppBalance[\text{token}]} e^{-\beta_{\text{token}} \times max(1, Risk[\text{token}][\text{strategy}])} \right)^{-1}$$


> [!NOTE]
> If the bApp uses validator balance, the client should also read a `map[Strategy]ValidatorBalance` with the amount from each strategy. As this capital doesn't involve any type of risk, all risk values can be set to 0. Thus, for this capital, this is equivalent to
> $$W_{\text{strategy, validator balance}} = \frac{ObligatedBalance[\text{validator balance}][\text{strategy}]}{TotalBAppBalance[\text{validator balance}]}$$


5. **Combine into the Final Weight**: With the per-token weights, the final step is to compute a final weight for the participant using a **combination function**. Such function is defined by the bApp and can be tailored to its specific needs. Traditional examples include the arithmetic mean, geometric mean, and harmonic mean.


**Example**: Let's consider a bApp that uses tokens $A$ and $B$, and considers $A$ to be twice as important as $B$. Then, it could use the following weighted harmonic mean as its combination function:

$$W^{\text{final}}_{\text{strategy}} = c_{\text{final}} \times \frac{1}{\frac{2/3}{W_{\text{strategy, A}}} + \frac{1/3}{W_{\text{strategy, B}}}}$$

where $c_{\text{final}}$ is a normalization constant computed as

$$c_{\text{final}} = \left( \sum_{\text{strategy}} \frac{1}{\frac{2/3}{W_{\text{strategy, A}}} + \frac{1/3}{W_{\text{strategy, B}}}} \right)^{-1}$$

Choose a reason for hiding this comment

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

still doesn't render well on github

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It renders locally. I'll try a change


### 3.1 Fecthing obligated balances, validator balances, and risks

In this subsection, we detail how the data for computing the participants' weights can be read from the chain state.

**Map of obligation balances**

```r
function ObligatedBalances(bApp)
obligatedBalances = New(Map<Token, Map<Strategy, Amount>>)

# Get bApp tokens
bAppTokens = api.GetbAppTokens(bApp)

# Loop through every strategy
strategies = api.GetStrategies()
for strategy in strategies do

# Check if strategy participates in the bApp
ownerAccount := api.GetStrategyOwnerAccount(strategy)
if api.GetStrategyOptedInToBApp(ownerAccount, bApp) != strategy then
# If not, continue
continue

# Get strategy balance
balance = api.GetStrategyBalance(strategy)

# Add obligated balance for each bApp token
for token in bAppTokens do
obligationPercentage = api.GetObligation(strategy, bApp, token)
obligatedBalances[token][strategy] = obligationPercentage * balance[token]

return obligatedBalances
```

**Map of validator balances**

```r
function ValidatorBalances(bApp)
validatorBalances = New(Map<Strategy, Amount>)

# Loop through every strategy
strategies = api.GetStrategies()
for strategy in strategies do

# Get account that owns the strategy
ownerAccount = api.GetStrategyOwnerAccount(strategy)

# Check if strategy participates in the bApp
if api.GetStrategyOptedInToBApp(ownerAccount, bApp) != strategy then
# If not, continue
continue

# Store validator balance
validatorBalances[strategy] = ComputeEffectiveValidatorBalance(ownerAccount)

return obligatedBalances


function ComputeEffectiveValidatorBalance(account)

total = 0

# Get all other accounts that delegated to it along with the percentages
delegatorsToAccount = New(Map<Account, Percentage>)
delegatorsToAccount = api.GetDelegatorsToAccount(account)

# Add the delegated balances
for delegator, percentage in delegatorsToAccount
total += GetOriginalValidatorBalance(delegator) * percentage

return total


function GetOriginalValidatorBalance(account)

total = 0

# Get SSV validators from account
validatorsPubKeys = SSVNode.GetValidatorsPubKeys(account)

for PubKey in validatorsPubKeys
# Get validator balance and active status
balance, isActive = ETHNode.GetValidatorBalance(PubKey)

if isActive
total += balance

return total
```

**Map of risks**

```r
function Risks(bApp)
risks = New(Map<Token, Map<Strategy, Percentage>>)

# Get bApp tokens
bAppTokens = api.GetbAppTokens(bApp)

# Loop through every strategy
strategies = api.GetStrategies()
for strategy in strategies do

# Check if strategy participates in the bApp
ownerAccount := api.GetStrategyOwnerAccount(strategy)
if api.GetStrategyOptedInToBApp(ownerAccount, bApp) != strategy then
# If not, continue
continue

# Store risk (i.e. sum of all obligation percentages)
risks[token][strategy] = api.AddAllObligationsForToken(strategy, token)

return risks
```

**API Calls**

For reference, we list the API calls used in the above snippets along with the chain state variables that should be read for each call:
- `GetbAppTokens(bApp)`: [`bAppTokens`](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L84)
- `GetStrategies()`: [`strategies`](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L89)
- `GetStrategyOptedInToBApp(account, bApp)`: [`accountBAppStrategy`](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L94)
- `GetStrategyBalance(strategy)`: [`strategyTokenBalances`](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L109)
- `GetObligation(strategy, bApp, token)`: [`obligations`](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L115)
- `GetStrategyOwnerAccount(strategy)`: [`strategies`](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L89)
- `GetTotalDelegation(account)`: [`totalDelegatedPercentage`](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L104)
- `GetDelegatorsToAccount(account)`: [`delegations`](https://github.com/ssvlabs/based-applications/blob/92a5d3d276148604e3fc087c1c121f78b136a741/src/BasedAppManager.sol#L99)


## Appendix

## Numerical example for participant weight

### 1. BApp Configuration

Consider a bApp with the following configuration:

| Configuration | Value |
|------------------------------|--------------------|
| `Tokens` | [SSV] |
| `SharedRiskLevels` ($\beta$) | [2] |
| Uses validator balance | True |
| Final weight combination function | $W^{\text{final}}_{\text{strategy}} = c_{\text{final}} \times \frac{1}{\frac{2/3}{W_{\text{strategy, SSV}}} + \frac{1/3}{W_{\text{strategy, VB}}}}$ |

This setup means:
- The only slashable token in use is SSV, with $\beta = 2$.
- Validator balance is included in the model.
- The combination function is a harmonic mean, where SSV carries twice the weight of validator balance.


### 2. Strategies securing the bApp

The following strategies have opted-in to the bApp:

| Strategy | SSV Balance | SSV Obligation | Risk for SSV token | Validator Balance |
|----------|-------------|----------------|--------------------|-------------------|
| 1 | 100 | 50% | 1.5 (150%) | 32 |
| 2 | 200 | 10% | 1 (100%) | 96 |

The obligated balances are:
- Strategy 1: $100 * 50\% = 50$ SSV
- Strategy 2: $200 * 10\% = 20$ SSV

Thus, in total, the bApp has:
- $50 + 20 = 70$ SSV
- $32 + 96 = 128$ validator balance

### 3.1 Weight for SSV

First, compute the normalization constant for the SSV token:

$$c_{\text{SSV}} = \left( \frac{50}{70}\times e^{-\beta_{\text{SSV}} \times max(1, 1.5)} + \frac{20}{70}\times e^{-\beta_{\text{SSV}} \times max(1, 1)} \right)^{-1} \approx 13.47$$

Using this coefficient, we can compute the weights:

$$W_{1, \text{SSV}} = c_{\text{SSV}} \times \frac{50}{70} \times e^{-\beta_{\text{SSV}} \times max(1, 1.5)} = 0.479$$

$$W_{2, \text{SSV}} = c_{\text{SSV}} \times \frac{20}{70} \times e^{-\beta_{\text{SSV}} \times max(1, 1)} = 0.521$$

Thus, the weights for the SSV token are:
- Strategy 1: 47.9%
- Strategy 2: 52.1%

Note that, despite Strategy 1 obligating $50/70 \approx 71\%$ of the total SSV, its weight drops to $47.9\%$ due to its higher risk.

### 3.2 Weight for Validator Balance

For validator balance:

$$c_{\text{VB}} = \left( \frac{32}{128} + \frac{96}{128}\right)^{-1} = 1$$

$$W_{1, \text{VB}} = c_{\text{VB}} \times \frac{32}{128} = 0.25$$

$$W_{2, \text{VB}} = c_{\text{VB}} \times \frac{96}{128} = 0.75$$

Thus, the validator balance weights are:
- Strategy 1: 25%
- Strategy 2: 75%

Since validator balance carries no risk, it remains proportional to the amount contributed.

### 3.3 Final Weight

Using the harmonic mean combination, we have:

$$c_{\text{final}} = \left( \frac{1}{\frac{2/3}{W_{\text{1, SSV}}} + \frac{1/3}{W_{\text{1, VB}}}} + \frac{1}{\frac{2/3}{W_{\text{2, SSV}}} + \frac{1/3}{W_{\text{2, VB}}}} \right)^{-1} \approx 1.05$$

$$W_1^{\text{final}} = c_{\text{final}} \times \left( \frac{1}{\frac{2/3}{W_{\text{1, SSV}}} + \frac{1/3}{W_{\text{1, VB}}}} \right) = 0.387$$

$$W_2^{\text{final}} = c_{\text{final}} \times \left( \frac{1}{\frac{2/3}{W_{\text{2, SSV}}} + \frac{1/3}{W_{\text{2, VB}}}} \right) = 0.613$$

Thus, the final weights are:
- Strategy 1: 38.7%
- Strategy 2: 61.3%
Loading