Shadow Farming/Staking

Shadow Farming is a unique development that is going to revolutionize the whole DeFi farming sphere

This is a brand new way to stake LP tokens. All tokens are kept by the user, which makes the staking process more secure and transparent. Moreover, there’s no need to send tokens back and forth, making this product almost gasless.

All you need to do is go to the official SpaceSwap site, choose the pool, proceed to Uniswap, add liquidity to the corresponding pool and receive the LP tokens in your wallet.

Then you just need to go back to SpaceSwap and activate the pool. No need to send LP tokens to SpaceSwap! The system automatically reads the information from your wallet.

This minimizes the number of transactions and steps needed at the start of the mining process.

Current smart contract: https://etherscan.io/address/0xe3e17fa901591aeecdf0a928489b5362fce3c0ca#code

Abstract

Shadow Staking is a layer 2 instrument for farming projects and earning rewards for LP tokens without the need to send them to a smart contract. This is a decision intended on minimizing users’ commision expenses and lowering the risks by keeping the LP tokens in a user’s account. Conceptually, the product is divided into 4 parts:

  • Smart contracts

  • Back-end

  • IPFS DB

  • Front-end

Smart contracts

At the level of smart contracts, a number of actions are processed, such as:

  • compensations

  • keeping a list of users

  • the amount of compensation sent to each user

  • an active pools list and their allocation of Points

  • Epoch lists and their compensations

  • interactions with the MILK2 smart contract

Working with SC keys

Smart contracts (SC’s) should contain a list of public keys that are used as a signature for withdrawing from the back-end part of the instrument. Only an owner of a smart contract is able to edit the list of keys. Moreover, they can add new keys or activate and deactivate already added keys. However, the owner is not able to delete keys because it won’t be possible to approve the authenticity of the sums addressed to users.

To generate and check the signature, a standard PKCS #1 v1.5 RSA for SHA256 is used.

Methods of working with keys:

add(e, n)
getKey(id)
disable(id)
enable(id)
withdraw/harvest
mapping(address => uint256) lastHarvestSaveBlock
mapping(address => uint256) harvestSum

withdraw(i, amount,lastBlockNumber, currentBlockNumber,signi)

  • i – number of a pair of keys [di, ei] with the help of a closed key (di) used to sign the message

  • signi() – the signature of the message itself

  • amount – contains the reward amount to be sent to a user

  • lastBlockNumber – contains the block number withdraw method that was called on previously. If the value in the smart contract repository doesn’t coincide with lastBlockNumber, then the transaction is declined

  • currentBlockNumber – contains the number of the block with a user’s reward calculation in it. If the withdraw method was successful, it is saved in the smart contract repository

  • bytes32 signi - RSA (di, [address userAddress, uint256 amount, uint256 lastBlockNumber, uint256 currentBlock]) – the user’s address is used for identification. If the wallet address pending withdrawal doesn’t coincide with userAddress, the transaction is declined

While the withdrawal method is being processed, the smart contract checks the message through an open key (ei, ni). It also tests the userAddress parameter. The latter has to coincide with a user’s number calling for withdrawal.

currentBlockNumber has to be smaller than the current block and the value in the compensation history repository by userAddress must coincide with lastBlockNumber. If these conditions are fulfilled, then the smart contract sends rewards to the user, adding the amount to the general user’s payout. It also changes lastBlockNumber for currentBlockNumber in the compensation history repository.

Withdraw function use for registration of new users

When a new user is registered, the corresponding withdrawal method is processed (with the following parameters in the message):

  • userAddress – the address of a registered user

  • amount with 0 value

  • lastBlockNumber with 0 value or Null (the default value in the compensation repository for new addresses)

  • currentBlockNumber – the value of the block on the server side

Such a request is easily detected by the “lastBlockNumber == Null value”, as after the first implementation of this function, it must be inserted into the current block value while the operation is being processed. Moreover, information about the new user is included in all the necessary repositories. It is necessary to use the current block instead of currentBlockNumber, which must be taken into account. And as long as the amount = 0, no token payouts will be made.

The information is also included in the user’s repository and the number of registered users is increased by 1.

getLastBlock(address)

This is a function used to get the block number that contains the last withdrawal method requested by a user. If a user hasn’t been registered, the method returns 0.

getTotalRevards()

This returns the amount of paid rewards to a user’s address.

GetPoolsCount

The number of pools entered in the smart contract.

GetPool

A method used for getting the full information about the pool – weight, address of LP token and block.

{
token: ‘0x….’,
block: 1109912,
weight: 100,
}

GetMultiplier(from, to)

This is for accessing the multiplicator within a period of blocks [from; to].

The smart contract contains (just as with the Sushi contract) a list of blocks that form chunks of time (milestones). Let’s mark p[0] as the block of the payout’s beginning and p[1] as the block of the first multiplicator change, … , with p[4] being the block of the last multiplicator change (there will be 5 of them in the current implementation). Now let’s mark m[0] - the multiplicator which will be active within the block p[0] and p[1] period. So the reward up to p[0] block will be 0 tokens per block. After the p[4] block, the reward will be m[4] tokens per block.

Formula for computing the function

getInterval(a, b) {
return a > b ? a - b : 0;
}
max(a, b) {
return a > b ? a : b;
}
min(a, b) {
return a < b ? a : b;
}
getMultiplier(from, to) {
return getInterval(min(t, p[1]), max(f, p[0])) * m[0] +
getInterval(min(t, p[2]), max(f, p[1])) * m[1] +
getInterval(min(t, p[3]), max(f, p[2])) * m[2] +
getInterval(min(t, p[4]), max(f, p[3])) * m[3] +
getInterval(max(t, p[4]), max(f, p[4])) * m[4]
}
GetCurrentMultiplier
Getter for getting a multiplication.
An implementation example:
getCurrentMultiplier() {
const n = CURRENT_BLOCK_NUMBER;
if (n < p[0]) {
return 0;
}
if (n < p[1]) {
return m[0];
}
if (n < p[2]) {
return m[1];
}
if (n < p[3]) {
return m[2];
}
if (n < p[4]) {
return m[3];
}
if (n > p[4]) {
return m[4];
}
}
// We can develop implementation, just as in the previous function.
// Kind of iterative, however, we’ll keep this variation for simplicity and visibility.

GetUsersCount

Shows the number of registered users.

GetUser(_id)

Shows a user’s address through their ID in the list of users.

Back-end

The back-end part processes the signature of the messages sent to a smart contract and the calculation of all the rewards for users.

Message signing

Only userAddress is visible – ETH address of a user.

The server identifies ETH address and addresses using the getLastBlock smart contract method. Thus, it identifies the block number from which to start the rewards payout. If it gets 0 or Null as an answer, then no calculation is needed. It is enough to sign the message consisting of (userAddress, 0, 0, 0) which will initiate a user’s registration in the smart contract after sending.

The calculation of rewards

There are many variants possible, from taking snapshots once in N blocks to analysing concrete addresses.

Regardless of the implementation, the calculation method will be the same. The amount of a pool payout for a user to a definite №N block depends on the following parameters:

Smart contract parameters:

  • pool.allocationPoints or the weight of the pool – this is responsible for the amount of tokens per block that will be distributed to a definite pool

  • totalAllocationPoints or the total weight of all pools (stored in a smart contract and is calculated “on the wind” from the list of pools in a smart contract)

  • user.totalRewards – the total amount of all a user’s withdrawals

  • multiplier

IPFS repository parameters:

  • pool.lastUpdateBlock – the number of the last renewal pool block

  • pool.lpSupply – the total value of LP tokens in a pool

  • pool.accPerShare – the estimated cost of the share in a pool

  • user.amount – the amount of LP tokens in a user’s wallet

  • user.rewardDebt – the number of the calculated but not yet paid rewards

  • user.accLoss – the amount of already paid rewards based on the loss of profit (the tokens that a user could have earned if they took part in the pool from the very beginning).

In any N pool (N >= pool.lastRewardBlock) the reward amount for a user is calculated the following way:

pendingReward = user.rewardDebt - user.accLoss + user.amount * accPerSharewhere
accPerShare = pool.accPerShare + multiplier * pool.allocationPoints / totalAllocationPoints / pool.lpSupply
// N >= pool.lastRewardBlock
multiplier = (N - pool.lastRewardBlock) * tokensPerBlock // calculated on the SC side
If a deposit was made and the LP tokens were withdrawn:
pool.lastUpadateBlock = blockNumber
pool.accPerShare = accPerShare
user.rewardDebt += pendingReward
user.amount += tx.amount
user.accLoss = user.amount * pool.accPerShare
pool.lpSupply += tx.amount

During the withdrawal of the reward, the same procedure is provided, except for renewal of a user’s data.

A message is sent with amount = user.rewardDebt + pendingReward

user.accLoss += pendingReward + user.rewardDebt user.rewardDebt = 0;

3. IPFS (coming soon)

IPFS stores all the changes within the contents of each pool: users’ registrations, LP token movements through registered users and the reward withdrawals.

The changes are fixed by snapshots of the pools’ state with added information about a definite user that triggers this change.

An example of data stored in the IPFS blocks:

Block number - the number of the block that the change happened on

txHash - hash of the transaction that caused the change

poolSize - the number of LP tokens in the pool

accPerShare - the storage rate of the rewards for a share in a pool

userAddress - the address of the user that caused the change

amount - the amount of a user’s LP tokens

rewardDebt - the debt of a user’s payouts

accLoss - the storage rate of lost income and withdrawals made