Skip to content

Conversation

naddison36
Copy link
Collaborator

@naddison36 naddison36 commented Jun 30, 2025

A new staking strategy that uses merkle proofs to verify the operations of validators running on the beacon chain.

Objectives

  • Consolidation of validators to reduce the operation costs.
  • Consolidation to a single staking strategy to reduce the Vault gas costs.
  • Better protection against front-running of deposits to a new validator.
  • On-chain withdrawals reduces the dependency on third party node operators managing validator private keys.
  • Merkle proofs of the beacon chain is much more secure than Oracles providing validator balances.

Links

Relevant Pectra Release changes

  • EIP-6110 On-Chain Validator Deposits
  • EIP-7002 Execution layer triggerable withdrawals
  • EIP-7251 Increase the MAX_EFFECTIVE_BALANCE
  • EIP-7685 General purpose execution layer requests

Contracts

The scope of the contracts of this build is

  • CompoundingStakingSSVStrategy is the new staking contract.
  • CompoundingValidatorManager is inherited by CompoundingStakingSSVStrategy
  • CompoundingStakingStrategyView has two view functions for CompoundingStakingSSVStrategy
  • BeaconProofs called from CompoundingValidatorManager

The following libraries have also be built

  • BeaconRoots
  • BeaconProofsLib
  • PartialWithdrawal
  • Merkle
  • Endian
oethContracts oethContracts

Hoodi

Contract Name Address
CompoundingStakingSSVStrategyProxy 0xb5B92DEFC0a9623E31e10Fe0BaE02610bf76fd09
CompoundingStakingStrategyView 0x13eDDe0650E41f3B54E43f6783EA6eFD49F0C804
BeaconProofs 0x2E6854F44b9bF035ca53ec121A78621729B537ff
OETHVaultProxy 0xD0cC28bc8F4666286F3211e465ecF1fe5c72AC8B
OETHProxy 0xbebDca6eF7452953e0AB5cebE2A174B71208B13a
NativeStakingSSVStrategyProxy 0x95DdB082cf2b8Bb4a4A358A8817a848cE32e446b
NativeStakingFeeAccumulatorProxy 0x27281CE00322Ee8b7C078788Fb624D051F5F7689

States

stakingValueTransitions stakingValidatorStates stakingStates

Beacon chain data

BeaconBlock
├── slot: Slot
├── proposer_index: ValidatorIndex
├── parent_root: Root
├── state_root: Root  # Merkle root of BeaconState
│   └── BeaconState  # Does not include all fields
│       ├── validators: List[Validator, MAX_VALIDATORS]
│       │   ├── pubkey: BLSPubkey
│       │   ├── withdrawal_credentials: Bytes32
│       │   ├── effective_balance: uint64
│       │   ├── slashed: boolean
│       │   ├── activation_epoch: Epoch
│       │   ├── exit_epoch: Epoch
│       │   └── withdrawable_epoch: Epoch
│       ├── balances: List[Gwei, MAX_VALIDATORS]
│       ├── eth1_data: Eth1Data
│       │   ├── deposit_root: Root
│       │   ├── deposit_count: uint64
│       │   └── block_hash: Hash32
│       ├── eth1_deposit_index: uint64
│       └── deposit_requests_start_index: uint64
│       └── deposit_balance_to_consume: Gwei
│       └── pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT]
└── body_root: Root
    └── BeaconBlockBody  # Does not include all fields
        ├── eth1_data: Eth1Data
        ├── attestations: List[Attestation, MAX_ATTESTATIONS]
        ├── slashings: List[Slashing, MAX_SLASHINGS]
        ├── sync_aggregate: SyncAggregate
        ├── deposits: List[Deposit, MAX_DEPOSITS]
        ├── execution_payload: Root # at index 9
        │   └── ExecutionPayload
        │       ├── block_number: uint64  # at index 6
        └── requests: List[Request, MAX_REQUESTS]
            ├── type: uint8 (deposit, exit, consolidation)
            ├── data: Bytes (e.g., DepositData, ExitData)
            └── source: Hash32 (EL block hash)

Beacon Container Specs

To get the consolidate beacon chain specification for the last Pectra release

git clone https://github.com/ethereum/consensus-specs.git
cd consensus-specs
make pyspec

build/lib/eth2spec/electra/mainnet.py will have the full, consolidated changes. This includes all the beacon chain container definitions. eg BeaconBlock, BeaconBlockBody, BeaconState, Validator...

Processes

Register a SSV validator

oethProcesses-register

Initial deposit to a new validator

oethProcesses-deposit-new

Deposit more to existing validator

oethProcesses-deposit-existing

Verify validator

oethProcesses-verify-validator

Verify deposit to validator

oethProcesses-verify-deposit

Update strategy balances

oethProcesses-verify-balances

Withdrawals

oethProcesses-withdraw

Admin

oethProcesses-admin

Build

Keeping the contract under 24Kb has been a problem during development. The following will show the contract size of the compile contracts including CompoundingStakingSSVStrategy.

export CONTRACT_SIZE=true
npx hardhat compile

Testing

Unit Tests

Are main strategy unit tests in contracts/test/strategies/compoundingSSVStaking.js

There are also unit tests of the beacon merkle proofs in contracts/test/beacon/beaconProofs.js

yarn test

Fork Tests

There are no fork tests of the strategy as its hard to mock the beacon chain. But there are fork tests of various beacon chain contracts and libraries. These includes:

  • contracts/test/beacon/beaconConsolidation.mainnet.fork-test.js
  • contracts/test/beacon/beaconProofs.mainnet.fork-test.js
  • contracts/test/beacon/beaconRoots.mainnet.fork-test.js
  • contracts/test/beacon/partialWithdrawal.mainnet.fork-test.js

Hoodi Testnet

Hoodi faucet: https://hoodi-faucet.pk910.de/
SSV faucet: https://faucet.ssv.network/

Contracts have been deployed to Hoodi with deployment scripts in contracts/deploy/hoodi

 export DEPLOYER_PK=
yarn run deploy:hoodi
npx hardhat etherscan-verify --network hoodi --api-url https://api-hoodi.etherscan.io

Hoodi testing using Hardhat tasks

# Set `DEFENDER_API_KEY` and `DEFENDER_API_SECRET` in your `.env` file for the Hoodi Defender Relayer

# Get some WETH
npx hardhat depositWETH --amount 64 --network hoodi

# Mint OETH with approval
npx hardhat mint --asset WETH --amount 64 --symbol OETH --network hoodi

# Deposit old to native staking strategy
npx hardhat depositToStrategy --symbol OETH --strategy NativeStakingSSVStrategyProxy --assets WETH --amounts 64 --network hoodi

# Check the WETH was deposited
npx hardhat snapStaking --network hoodi

# Set the amount of Ether than can be staked before needing a reset
npx hardhat setStakeETHThreshold --amount 500 --network hoodi
# Reset the stakeETHTally if needed
npx hardhat resetStakeETHTally --network hoodi

# Register an old sweeping validator
npx hardhat registerValidators --days 30 --validators 1 --network hoodi

# stake to the old sweeping validator
npx hardhat stakeValidators --network hoodi

# Check the validator is active
npx hardhat snapStaking --network hoodi

# Deposit to new compounding staking strategy
npx hardhat depositToStrategy --symbol OETH --strategy CompoundingStakingSSVStrategyProxy --assets WETH --amounts 64 --network hoodi

# Check WETH in the new strategy
npx hardhat snapStakingStrat --network hoodi

# Request a new SSV validator via the staking contract
# Must include some SSV for the first validator
npx hardhat registerValidator --operatorids 345,346,347,348 --pubkey 0x8ac167403e9e81ceb3f612aed30c0d0667c1dfd6b716802798e9ea20b5fae153fd3b7be2676b239f4b7efaf0b2d6b12c --shares 0xa946648c8a243cd45d3fa5ea0d06fde42cba42985eaed0a4487ed669e3d7e5c710a9c3a951150694599380cde0314992074d615b0227ce5d4fed279f965e29a506e946070d7327879d8545154bba07ca1267452bc525ec0caaf6018122dd14398eb6a048186f2244fb6e40e6f568879175f63fcf86d3026f71f4bf0291badd08ea846b673b9d1cd2930bfadf6c719f84adf51764730105fb99d69b481f16cf3c377d6affe638587a65683868db173749d1c0ba127ac91fe185b5e0798909358cb838eb8659c08f9a07e53b0d354ece6408e1c145d9566b31afc8dee9b602dc10a26d252913b86f445b85dfc09ec94489ae21807065e5183b63dd78530d7295f24273af6c453c8e1d0ba5e9713c486326a5a735caee43d465d49df35a353177751c1a2a72768051a9ab8309c2a620dbbba3e6aa4a73366c480256092218345aab153516acbdbbd4566490518e011be8fc1607f4eaa9c8f8c9bd322bc738f30c5bc546516ed5e3d94224b51653919824057ba2c83547fb79afc1d451d36a066120d351b270266369c7108e81134663c6fcc5f319ca7043e42c42d93b4fbae294e169fd2d8109041fa99a762a307193fffd14f87c3191a6b86239885b46ad240cedd7ae6788aa209ee5b18357d397cb8cdd86d3395f6f8907870c5f743dc24448a2a2652b1cc4ace8278cbc084a433de451a414342c0c1150f37cab8fd3cf817137b5fa99ebae7e8fc33e3316670ee4d0c7ebdb8aa25c3677978654615093f8b95a91abb1b15d675c23c52dffbf6b1eddd82f5d44040f9b19c25c76d11efda396c2c82b9982aa42b309a7f94e3445c015f596e8673bcf8c56b423cb132910ee4a2158f18287e9bc3d1e1ad856f9f576b869a11ecf51b337db8b260d79b8cbfa68ccb056de2e49d77d2826eab59f4b0a56e90f939024e878a3c4e70492df9ec3c61ae0b94d8e606e92ebf4444fca8dd745b0053f9b2657e81c9631f215854d4679d612631f7941aba03fdd068d5a43225c65dbab617a7b2736756394d119909397c0d0663c193a1cab280b1990dc3ce4d805fc2e5a66dca2523e27300894c6a6d67015e25b7406fee582b83ca6afa106185f0e680c53801df1bab343917c75c3106908a0730a3d3f7567c6dda09228d5d6bd6d1bdd6afc4ab6c311f2cb698bbdd43713e254ecbd64a65ca3c44b0f2428ec9bd23d045038ae1a52f3a1c98076281cbeaf5076807c57df72703394e8c4e33e1a51fb5e740176902e38033e9d718f12db9897f82b1826f541e970ec159c92a847b1ebc2b5ea9052a6a3c4f42f2f43723194ad549b0583972703ec4443e6513076c87c8758fd714bd629d45f2eba57d834c8569fb02a701b2db3a3bfeae0e5314a2f05e1d2d8c2909af9b468f25c278acbed3b6a9a8e7bea7fb9195115f0add6179717b4054230ef78c80fa3acfc54a2bd9d1eb4b9bf932755042617fd53725aefd8e3aa0d1d6ec508a8754181497af2b753884e8952df0197a25ab5d785e4ea3979fdfedcccc9e89117975ee9a055930868fde4215535c8582639310c8b0c06b9892406d7e4ddf8c899e662fbac44d232888d5cb7ca19f3e48d098f12fbf1082d3452e606c65af26e9a68a36d4921113c14724325a3fcd0382ee5c73309fd9b92752dc89fff2691926dc0a403ee26dbb19edbc79df12f9efad2b0778eccd680cebf91392c6e8b338e1148d691efae07c5a6702a6297d3fc5bfacf5214829c352b77c627c8602d1a82a61440db4661ca9132618846304e1883fc37e08f7bea99aaea7bbbf2193ec1d113c51571bbe9771eebda5a58ca9b25b7419d794eb147ea52ba250cb9344b27a31eeec851a74547d6 --network hoodi

# Deposit to a new validator
# Must include the sig and deposit-message-root for the first deposit
npx hardhat stakeValidator --pubkey 0x8ac167403e9e81ceb3f612aed30c0d0667c1dfd6b716802798e9ea20b5fae153fd3b7be2676b239f4b7efaf0b2d6b12c --sig 0xb276484b5e40626330572d6f2f29905afec33b84e8d1fb15d16f39a5765ea7467402b4f42f1deef4e18d88de662326270e32010e731f527eea0d02f0e0cec91bcb5dc34b4ec9b509703844c3ffb71cea608886386dc4a11f166cee5a5a9d0bb8 --deposit-message-root 0x2fd84757b8e1da3914a415b513e35386da75eab463c2aef1b74cd19987cf8806 --amount 1 --network hoodi

# Verify the validator
# make sure BEACON_PROVIDER_URL is pointing to the Hoodi beacon chain
npx hardhat verifyValidator --index 1222774 --network hoodi
npx hardhat getValidator --index 1222774 --network hoodi

# Verify the deposit
npx hardhat verifyDeposit --root 0x778706f09de3313d13e6bb7a1a0d177fae657119da51c70ed185f8fdde1cb34b --network hoodi

# Snap the ETH balance
npx hardhat snapBalances --network hoodi

# Verify the balances
npx hardhat verifyBalances --network hoodi

# Partial withdrawal from a validator
npx hardhat withdrawValidator --amount 5 --pubkey 0x8f4429ea2bd4228f0ca74b4e232b03f6e0ef5bd2420951240dfe2acc423944aab40530b5f3165f231600e1a90fed3d4e --network hoodi

# Exit from a validator with a zero amount
npx hardhat withdrawValidator --amount 0 --pubkey 0x8ac167403e9e81ceb3f612aed30c0d0667c1dfd6b716802798e9ea20b5fae153fd3b7be2676b239f4b7efaf0b2d6b12c --network hoodi

Deployment

Hoodi

 export DEPLOYER_PK=
yarn run deploy:hoodi

npx hardhat etherscan-verify --contract-name CompoundingStakingSSVStrategy --network hoodi --api-url https://api-hoodi.etherscan.io
npx hardhat etherscan-verify --contract-name BeaconProofs --network hoodi --api-url https://api-hoodi.etherscan.io

npx hardhat tenderlyUpload --name CompoundingStakingSSVStrategy --network hoodi
npx hardhat tenderlyUpload --name BeaconProofs --network hoodi

Code Change Checklist

To be completed before internal review begins:

  • The contract code is complete
  • Executable deployment file
  • Fork tests that test after the deployment file runs
  • Unit tests *if needed
  • The owner has done a full checklist review of the code + tests

Internal review:

  • Two approvals by internal reviewers

Copy link

github-actions bot commented Jun 30, 2025

Warnings
⚠️ 👀 This PR needs at least 2 reviewers

Generated by 🚫 dangerJS against 4e1a4fb

Copy link

codecov bot commented Jun 30, 2025

Codecov Report

❌ Patch coverage is 94.02299% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 40.76%. Comparing base (6a6d164) to head (4e1a4fb).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
.../strategies/NativeStaking/ValidatorRegistrator.sol 50.00% 11 Missing and 1 partial ⚠️
contracts/contracts/beacon/BeaconConsolidation.sol 0.00% 10 Missing ⚠️
...gies/NativeStaking/CompoundingValidatorManager.sol 98.62% 3 Missing ⚠️
...es/NativeStaking/CompoundingStakingSSVStrategy.sol 97.95% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2559      +/-   ##
==========================================
+ Coverage   38.24%   40.76%   +2.51%     
==========================================
  Files         112      122      +10     
  Lines        5331     5748     +417     
  Branches     1412     1524     +112     
==========================================
+ Hits         2039     2343     +304     
- Misses       3290     3402     +112     
- Partials        2        3       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

/// @notice Mapping of the root of a deposit (depositDataRoot) to its data
mapping(bytes32 => DepositData) public deposits;
/// @notice List of deposit roots that are still to be verified as processed on the beacon chain
bytes32[] public depositsRoots;
Copy link
Collaborator

Choose a reason for hiding this comment

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

It could be useful to have a function that returns the length of this list.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unfortunately adding a function that returns the number of deposits will make the contract too big

Copy link
Collaborator

Choose a reason for hiding this comment

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

I added it on my invariant repo and it didn't make the contract too big, I mean, the deployment didn't revert.
Maybe foundry optimize it a bit by default, which reduce the size of the code in comparison with Hardhat or something.

// Delete the last deposit from the list
depositsRoots.pop();

emit DepositVerified(depositDataRoot, deposit.amountGwei * 1 gwei);
Copy link
Collaborator

Choose a reason for hiding this comment

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

deposit.amountGwei * 1 gwei

It will overflow when amountGwei is above 18 gwei, it could be fixed with uint256(deposit.amountGwei) * 1 gwei.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nice pickup. I've fixed

Comment on lines 671 to 674
require(
consolidationLastPubKeyHash == bytes32(0),
"Consolidation in progress"
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this require is not reachable (i.e. it will never revert).

We are looking for a situation where consolidationLastPubKeyHash != bytes32(0).
This can happen only in the situation where requestConsolidation() has been called. But when requestConsolidation() is called, it pause the contract which prevent the snapBalance to be called.

But still not 100% sure. What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's true and it takes 0.078 Kb off the contract size!
I've removed now pause is exclusively used for consolidations.

// After verifying the proof, update the contract storage
deposits[depositDataRoot].status = DepositStatus.VERIFIED;
// Move the last deposit to the index of the verified deposit
depositsRoots[deposit.depositIndex] = depositsRoots[
Copy link
Contributor

Choose a reason for hiding this comment

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

🟥 deposit.depositIndex is a pointer to a row in depositsRoots, but the elements in the depositRoots array get moved around by this delete process, meaning the pointer in the deposit structure can point to the wrong place.

View Functions
****************************************/

function getVerifiedValidators()
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess we plan on having a few big validators on the contract, so this is okay. But this could run out of gas, given enough validators. and usually just a length method and then querying is the most bulletproof method.

This method also might use a bit of contract size in loading these items from storage into memory in the correct format for output.

/// Only the registrator can call this function.
/// @param depositAmountGwei The amount of WETH to stake to the validator in Gwei.
// slither-disable-start reentrancy-eth
function stakeEth(
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 A must-have of ours blocking theft of user funds from the Registrator account. We cap the damage that can be done by only allowing a certain number of total outstanding deposits to unvalidated validators. If, for example, we only allowed 10 outstanding 1 ETH deposits to unverified validators, then the maximum theft amount would be ten, which would be fine.

Copy link
Member

Choose a reason for hiding this comment

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

🔴 I agree. I think we should keep track of staked validators (we already keep track of verified validators). And if the sum of those 2 surpasses some constant (say 20) the stakeEth deposit for the next validator that is REGISTERED should fail.

Otherwise a hijacked registrator can increase the number of verified validators to a point where the verifyBalances isn't able to execute due to a too long verifiedValidators list. Requiring pretty involved refactoring to save the bricked state

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is more important now we have 32 ETH initial deposits.
I need to think about how best to implement this.

/// Only the registrator can call this function.
/// @param depositAmountGwei The amount of WETH to stake to the validator in Gwei.
// slither-disable-start reentrancy-eth
function stakeEth(
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 What if we cannot validator a validator.

Perhaps the register account has been compromised or there has been a doof up on the P2P/SSV side.

We do not have a state transition path back to recovery to a good accounting state, since the validator will not validate, and there is no other transition out?

Copy link
Member

Choose a reason for hiding this comment

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

If we can not validate a validator we would loose 32 ETH. This risk is similar to the old staking strategy. SSV network contracts do not verify the regiterValidator values aside from key/shard lengths. There would be ways to add admin functions that promote a validator to VALIDATED and assign an index to it. In unlikely case this would happen

sparrowDom and others added 30 commits September 4, 2025 16:58
* add basic files for mocking beacon roots

* add tests for multiple deposits to exiting validator. Also fix bug not storing the deposit data

* add a test where deposit is removed in verifyBalances

* more test checks

* Post merge fix

* Disable linter in MockBeaconProofs

* Fixed unit tests

* Added unit test for a front-run initial deposit

* Added more unit tests for front-run deposit

* Alternative fix to setting the withdrawable epoch (#2654)

* fix tests

* More unit tests (#2655)

* Alternative fix to setting the withdrawable epoch

* Added more unit tests

* Fix inconsistent test failure

* add check that the deposits have been removed

* add comment

---------

Co-authored-by: Nicholas Addison <[email protected]>
* Changed CompoundingValidatorManager storage gap to 40

* Fixed Natspec of BALANCES_CONTAINER_GENERALIZED_INDEX

* verifyDeposit has early exit if slot of first pending deposit is zero
Default first pending deposit slot to 1 if the beacon deposit queue is empty
…2658)

* Refactor verifyBalances to use inclusion proofs of pending deposits

* Refactor stakeEth to use the pending deposit root as the deposit identifier.
Ensure the pending deposit root is unique.

* WIP fixing unit tests

* Logical state diagram for the staking strategy

* Fix verifyBalances to not verify pending deposits of exited validators

* More unit test fixes

* Fixes the last of the failing unit tests

* verify deposits to exiting validators (#2664)

* add the support to handle verifying deposits to exited validators

* verifyBalances no longer removes deposits

* remove event

* remove deposit when done to an exiting validator

* Removed setting the validator as EXISTING from verifyDeposit

* Updated staking state diagram

* Can now use a normal for loop of the deposits in verifyBalances

* Moved firstPendingDepositEpoch to just before it is used

* change error message

* add pausable ability to the validator

---------

Co-authored-by: Nicholas Addison <[email protected]>

* Exiting validator (#2665)

* do not exit a validator as long as it has pending deposits

* Fixed verifyBalances - deposit loop inside the validator loop using different iterator

* Prettier

---------

Co-authored-by: Nicholas Addison <[email protected]>

---------

Co-authored-by: Domen Grabec <[email protected]>
* make it mandatory for validator to have active balance before allowing full withdrawals

* minor update

* fix bugs

* add test

* prettier

* Updated the validator state diagram

* make some variables internal to save contract space

* simplification

* Added ACTIVE validator state (#2666)

* Added ACTIVE validator state

* Fixed ACTIVE change
Updated unit tests

* Updated state diagram

* Deploy new strategy to Hoodi

---------

Co-authored-by: Nicholas Addison <[email protected]>
* Allow a validator full exit when it is already exiting

* Updated validator state diagram
* Restricted the pending deposit index to uint32 as the pending deposits container height is only 28

* Added check that pendingDepositIndex is < 2**27 in verifyPendingDeposit
…sit queue is empty (#2669)

* Can verify a deposit if the validator is exiting and the pending deposit queue is empty

* add comment

---------

Co-authored-by: Domen Grabec <[email protected]>
…#2673)

* Allow verification of a non 0x02 validator so it can be marked as INVALID

* Added unit test for verifying an invalid validator type

* pass withdrawal credentials when verifying validator (#2674)

* Fixed verifyValidator Hardhat task

* Fixed beacon proof fork test

---------

Co-authored-by: Domen Grabec <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants