# Tokenized Stakers Pool
## Problem
Pooled validation aims at allowing large number of participants with smaller amount of coins to participate in TON validation. This requires solving three technical problems:
1. How to make contract operating at O(1) cost, avoid inconvenient hard limits and prevent any possibility for DoS attacks?
2. How to efficiently and correctly store information about stakes that arrive at different times and with different amounts, given that the incoming profit varies?
3. How to perform withdrawals given that almost all the time coins are locked into validation cycle and are not available for immediate withdrawal?
The design below aims at solving these three problems.
## Key ideas
1. Contract implements a pool of *stakers* and works with a single target for validation. A pool of validators could be implemented as a separate contract used as a target instead of the elector contract.
2. Contract does not store variable-length data, **all operations are O(1)**.
3. Each user pays their own blockchain fees.
4. The information about stakes is encoded into the **price** and **amount** of shares issued by the pool. At the end of the cycle the pool knows the total amount of outstanding shares and an up-to-date amount of coins available (deposits+profits). As profits accumulate, the price of a share grows.
5. The information about withdrawals is encoded in **withdrawal receipts** implemented as NFTs. When the user returns their share-tokens to the pool, the pool issues a **receipt-contract** that indicates the time (timestamp or validation cycle number) when the exact amount of coins will become available for collection. When the receipt is transferred to the pool, it destroys it and returns back the requested amount of coins.
## Specifics
* We should study all quirks in Nominator contract: https://github.com/ton-blockchain/nominator-pool/tree/main/func
* The spec below does not implement voting yet. We may start with a simpler scheme where the operator of the pool votes, then figure out how to tokenize voting by pool members.
* Sometimes a validation cycle may be skipped: https://github.com/ton-blockchain/nominator-pool/blob/main/func/pool.fc#L575
* (Something else? Add here.)
## Questions?
* Not clear what's the API of the Elector? Do we request the stake back manually or toncoins return automatically?
* How to implement voting?
* Do we use election cycle count or timestamp?
* Do we use proper NFTs or OnceTokens for withdrawal receipts and destroy them when redeeming?
## Specification
### Elector proxy
Elector contract requires access from the same chain (-1, aka masterchain). Since token creation/destruction is relatively expensive, we move the actual logic to the workchain. The workchain contract sends messages to the proxy as if it was an elector. And vice versa.
```
contract Proxy {
// hard-coded address of the elector contract and the stakers pool
let elector: Address;
let owner: Address;
fn recv_internal(msg: InternalMessage) {
if msg.address == self.elector {
send(InboundCoins) self.owner.send_raw_message(msg);
} else if msg.address == self.owner {
send(InboundCoins) self.elector.send_raw_message(msg);
}
}
bounced handle() {
if msg.address == self.elector {
send self.owner.unexpected_bounce(msg);
} else if msg.address == self.owner {
// TODO: ignore?
}
}
}
```
### Stakers Pool
This implements the core logic: getting money in and out of Elector (via the Elector Proxy, see above) and issuing and redeeming **Stake Tokens** and **Withdrawal Receipts**.
Stake Tokens are simple jetton contracts with default behavior. Offchain infra may read the current price from the pool and display it in the wallets.
Withdrawal Receipts are NFT-like tokens ("OnceToken") that leave <24h and encode the time and amount of Toncoins to be withdrawn. When the receipt is redeemed, it is destroyed; its identity is verified on the recepient's side.
```
contract StakersPool {
// this address may be changed
// to a ValidatorsPool that manages allocation
// of stake among multiple validators.
let ElectorAddress = 0xe1ec705...; // address of the proxy
let StakeTokenID = 0x1234567...;
let RentReserve = 1 TON;
let MinShare = 1 TON; // also: min withdrawal amount
var coins_validating: uint64; // == C
var shares_outstanding: uint64; // == S
var coins_to_withdraw: uint64;
var validation_cycle: uint64;
// When we receive the stake from the elector,
// we update our current count of coins to take into account profits,
// and increment cycle count to permit withdrawals
fn receive_stake_from_elector(value: Coins) {
self.coins_validating = value - self.coins_to_withdraw;
self.validation_cycle += 1;
}
// Sends all coins to the elector
// except for those requested for withdrawal.
fn send_stake_to_elector() {
// TODO: do we update coins_validating here?
reserve(self.withdrawals_requested + RentReserve);
send(AllCoins) ElectorAddress.deposit();
}
// Adds incoming coins for validation,
// returns the number of stakes
fn deposit() {
// TODO: allocate necessary fees
let c = self.coins_validating;
let s = self.shares_outstanding;
let new_shares = deposit*s/c; // use MULDIVMODR
self.working_coins += msg.value;
self.shares_outstanding += new_shares;
send StakeJetton(amount: new_shares, owner: msg.sender);
}
// Withdrawal is requested when shares are returned.
// Contract increased amount of coins to send out
// and gives back a receipt.
fn request_withdrawal(shares: uint64, user: Address) {
// TODO: allocate necessary fees
let c = self.coins_validating;
let s = self.shares_outstanding;
let coins = shares*c/s; // use MULDIVMODR
self.coins_validating -= coins;
self.shares_outstanding -= shares;
self.coins_to_withdraw += coins;
// Issue an NFT to the user.
// It can be exchanged for coins at the next validation cycle.
send WithdrawalReceipt.init(
amount: coins,
owner: users,
cycle: self.validation_cycle + 1,
);
}
// User has sent a withdrawal receipt.
// If it is sent untimely, it is returned back.
// If it is timely, the NFT is destroyed and
// user received their coins.
fn withdrawal_receipt(
amount: Coins
owner: Address,
cycle: uint64,
msg.sender: WithdrawalReceipt
)
{
// check that it's a correctly issued receipt
let trusted_receipt = WithdrawalReceipt.init(amount, owner, cycle);
verify(msg.sender == trusted_receipt.address());
// Verify timing: the withdrawal can only work
// when the target cycle has arrived and funds are available.
if (cycle > self.validation_cycle) {
// wrong timing - return the token back to the sender
// todo: subtract the gas costs from their amount
send msg.sender.transfer(owner: owner);
} else {
// funds should be ready - send them out
self.coins_to_withdraw -= amount;
send msg.sender.burn(excesses_recipient: self.address());
send owner.transfer(msg.amount: amount);
}
}
}
```
### Stake Token
A default "jetton wallet" implementation with a master address set to the StakersPool.
```
contract StakeToken: FungibleToken {
let master: StakersPool;
}
```
### Withdrawal Receipt
NFT-like token that can be redeemed on maturity date by its owner.
We don't really need to authenticate redemption so that any 3rd party infra could trigger it to collect funds on behalf of the user.
We also do not need to implement NFT `transfer` method since this token is short-lived and we could simplify the logic by outright destroying the token when it's redeemed. If there's not enough funds on the other end we could simply re-create the token and return it back to the user (subtracting the fees from their withdrawal request).
```
// Invariants:
// - must be created by StakersPool
// - message should be coming from a verified address
contract WithdrawalReceipt {
init(amount: Coins, owner: Address, cycle: uint64, msg.sender: Address) {
}
}
```
# Spec
## Elector
1. Отправка стейка. Возможна ошибка сразу, возможна при получении.
- Нужно хендлить баунсы.
- Нужно хендлить асинхронные сообщения.
2. Получение стейка. Нужно послать запрос на получение стейка, и он вернется в следующем сообщении.
## Nominator Pool
### Users actions
1. Deposit (deposit coins, get the tokens)
2. Request withdrawal (return tokens, get the receipt)
3. Finalize withdrawal (burn receipt, receive coins)
### Withdraw Receipt actions
1. Withdraw to the address.
### Validator actions
1. New stake. Validator should send message with opcode NEW_STAKE and body that will be sent to the proxy as message body. Amount of coins that should be send to the proxy is equal to WORKING_COINS. This is validator responsibility to check if the message body is valid, Pool should just re-send body to the Proxy.
### Unknown domain actions
1. Recover stake. Anyone or Validator can send message to recover stake. In this case Pool should send message to the elector using following format:
```
recover_stake#47657424 query_id:uint64 = Msg;
```
Unresolved questions:
- Magic in https://github.com/ton-blockchain/nominator-pool/blob/main/func/pool.fc#L566-L582.
- Who can call this action - anyone or only the validator?
### Proxy actions
It is expected that messages sent to these actions will not be bounced.
1. Return Stake action. This action should be called if the Elector return stake sent by the Pool. This message should be sent as-is from the Elector through Proxy to the Pool.
Unresolved question: what should we do with this message?
Format of the message:
```
return_stake#ee6f454c query_id:uint32 reason:Reason = Msg;
reason0#00000000 = Reason; // no elections active, or source is not in masterchain, or elections already finished
reason1#00000001 = Reason; // incorrect signature
reason2#00000006 = Reason; // factor must be >= 1. = 65536/65536
reason3#00000002 = Reason; // stake smaller than 1/4096 of the total accumulated stakes, return
reason4#00000003 = Reason; // stake for some other elections, return
reason5#00000004 = Reason; // can make stakes for a public key from one address only
reason6#00000005 = Reason; // stake too small, return it
```
2. Confirmation. This action should be called if the Elector returns confirmation message. This message should be sent as-is from the Elector through Proxy to the Pool.
Unresolved question: what should we do with this message?
Format of the message:
```
confirmation#f374484c query_id:uint32 comment:uint32 = Msg;
```
3. Bounced stake. This action should be called if the Elector bounce message with the "Send Stake" message sent by the Proxy. This should not be happened by default, but this is possible situation so we should handle it.
Unresolved question: what should we do with this message?
TODO: format.
4. Recover Stake Error. This action should be called if the Elector returns error message on the "Recover Stake" action.
Format of the message:
```
recover_stake_err#fffffffe query_id:uint32 body:0x47657424 = Msg;
```
5. Recover Stake Ok. This action should be called if the Elector returns ok message on the "Recover Stake" action.
Format of the message:
```
recover_stake_ok#f96f7324 query_id:uint32 = Msg;
```
## Withdraw Receipt
1. Withdraw. Owner of the receipt can withdraw amount of coins that described in the
## Jetton
## Operator(Validator)
## Anyone
## Elector Proxy
Proxy should readressed default Elector messages to the Pool, and the Pool messages to the Elector.
If message that was sent to the Elector was bounced, Proxy should send special "Bounced stake" message to the Pool.