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:


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. The purpose of this innovation is to minimize users’ commision expenses and lower their risks by keeping the LP tokens on their accounts.

Conceptually, the product is currently divided into 3 parts: ‌

  • Smart contracts

  • Back-end

  • 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 and the back-end contain information about specially allocated back-end addresses that have the right to sign transactions — the signature function is only used when the harvest method is applied and the user withdraws their reward tokens. This system guarantees the reliability of the data regarding funds owed to the user, based on the information relating to the availability, quantity and history of changes in the balance of tokens. The data referred to here only concerns LP-tokens on the ERC-20 and BEP-20 standards, which the back-end constantly synchronizes with the contract and is where all complex mathematical calculations are processed.

‌Methods of working with keys:

  • signi(id) – the signature of the message itself

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

  • lastBlockNumber – contains the block number of withdrawal method previously called. If the value in the smart contract repository doesn’t reconcile with the lastBlockNumber, then the transaction is declined

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

  • bytes32 signi - RSA (id, [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 a trusted wallet. It also tests the userAddress parameter. The latter has to match with the user’s number calling for withdrawal. ‌currentBlockNumber has to be smaller than the current block and the value in the compensation history repository and the userAddress must correspond with lastBlockNumber. If these conditions are fulfilled, then the smart contract sends the 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.


‌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.


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


‌‌The number of pools entered in the smart contract.


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


token: ‘0x….’,

block: 1109912,

weight: 100,


GetMultiplier(from, to)

Only the getСurrentMultiplier () method is significant for the contract. All additional calculations and history of changes are stored at the back-end.

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]



Getter for getting a multiplication.

An implementation example:

getCurrentMultiplier() {


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. ‌


‌‌Shows the number of registered users.


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


‌‌The back-end 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 reward 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.

‌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 followed, except for the 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.

Examples 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 LP tokens a user has ‌

rewardDebt - the payout debt of a user ‌

accLoss - the storage rate of lost income and withdrawals made

Last updated