Shadow Staking/Farming Technology

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 your 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 Farming 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 innovation’s intent is to minimize users’ commision expenses and lower their risks by keeping LP tokens in the 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 the smart contracts, a number of actions are processed, such as:

  • compensations

  • maintaining a list of users

  • calculating the amount of compensation sent to each user

  • maintaining an active pools list and their allocation of points

  • maintaining Epoch lists and their compensations

  • interacting 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 the 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 withdrawal method that was called on previously. If the value in the smart contract repository doesn’t reconcile with lastBlockNumber, then the transaction is declined.

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

  • bytes32 signi - RSA (di, [address userAddress, uint256 amount, uint256 lastBlockNumber, uint256 currentBlock]) – a user’s address is used for identification. If the wallet address pending withdrawal doesn’t reconcile 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 match 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 correspond 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 is 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 function is 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 like a Sushi contract) a list of blocks that form chunks of time (milestones). Let’s mark p[0] as the block for the payout’s beginning and p[1] as the block for the first multiplicator change, … , with p[4] being the block for 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 – the ETH address of a user.

The server identifies the 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 analyzing concrete addresses.

‌Regardless of the implementation, the calculation method will be the same. The pool payout amount 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;

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.

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 IPFS blocks:

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

‌txHash - the 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