Shadow Staking/Farming Technology
Last updated
Last updated
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/0x20be3e5a0e37136901d91687f6f8faabad698ae1
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. 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.
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 rewards paid to a user’s address.
GetPoolsCount
The number of pools entered in the smart contract.
GetPool
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]
}
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 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