Yield Farming Contract Development
A yield farming contract distributes rewards among liquidity providers proportional to their pool share. The mathematics is straightforward, but implementation is full of nuances: from errors in accumulated reward formulas to vulnerabilities allowing the reward pool to be drained through deposit manipulation. The most famous example is MasterChef from SushiSwap, whose forks have cost protocols tens of millions through various implementation bugs.
Accumulated Rewards Formula: Why Naive Implementation Doesn't Work
Naive approach: store lastClaimedBlock for each user and calculate rewards as (currentBlock - lastClaimedBlock) * rewardPerBlock * userShare. The problem is that userShare changes with every deposit/withdrawal from other users. Recalculating for all users on each change is an O(n) operation that costs millions of gas with 1000 participants.
MasterChef algorithm (Compound-style) solves this through accRewardPerShare — accumulated reward per unit of stake, which only increases:
accRewardPerShare += (newRewards / totalStaked)
For each user, rewardDebt is stored — the "debt" at the moment of last interaction:
rewardDebt = userAmount * accRewardPerShare
pendingReward = (userAmount * accRewardPerShare) - rewardDebt
On deposit/withdrawal, we update accRewardPerShare for the current moment, pay out pending rewards, and update rewardDebt. This is O(1) regardless of the number of participants.
Integer precision problem: accRewardPerShare is stored multiplied by 1e12 (or 1e18 for 18-decimal tokens) to avoid precision loss during division. Without this multiplication, with small deposits and large totalStaked, the accumulated reward rounds to 0.
Main Vulnerabilities of Farming Contracts
Flash loan harvest manipulation
Attack: in a single transaction, take a large flash loan, deposit it into the farming contract, collect a disproportionately large share of accumulated rewards, withdraw the deposit, and return the flash loan. Works if harvest() doesn't require a minimum staking time.
Protection: minimum lock period (even 1 block significantly complicates the attack) or snapshot-based rewards (rewards are distributed based on balance at snapshot time, not current).
Not all protocols apply lock period — it's a UX tradeoff. If lock period is unacceptable, the formula should be designed so that instant deposit-harvest-withdrawal provides no profit (through deposit/withdrawal fees).
Reentrancy via harvest + ERC-777
If the reward token is ERC-777 (or any token with a hook on transfer), the token calls a callback on the recipient during reward payout. If the callback re-calls harvest() or withdraw() — reentrancy occurs. Standard protection through ReentrancyGuard from OpenZeppelin. Importantly: the guard should be on all functions that change state AND interact with external contracts.
Reward token depletion
The contract promises rewardPerBlock but doesn't verify that the reward pool has enough tokens. If the reward pool is depleted, transfer reverts — users can neither receive rewards nor withdraw deposits (if harvest is built into withdraw). Pattern: on withdrawal, first withdraw the stake, then attempt to pay rewards with insufficient balance handling.
Multi-Pool Implementation
Extension of MasterChef for multiple staking tokens (multi-pool farming):
struct PoolInfo {
IERC20 stakingToken;
uint256 allocPoint; // pool weight in reward distribution
uint256 lastRewardBlock;
uint256 accRewardPerShare; // multiplied by 1e12
uint256 totalStaked;
}
struct UserInfo {
uint256 amount;
uint256 rewardDebt;
}
PoolInfo[] public poolInfo;
mapping(uint256 => mapping(address => UserInfo)) public userInfo;
uint256 public rewardPerBlock;
uint256 public totalAllocPoint;
allocPoint distributes rewardPerBlock among pools: a pool with allocPoint = 100 when totalAllocPoint = 200 receives 50% of rewards. This allows managing incentives without changing the overall emission rate.
Deposit Fee and Protection Against Whale Manipulation
Deposit fee (0.1-0.5%) — an additional mechanism against flash loan attacks and a treasury revenue source. Implemented as a deduction on deposit:
uint256 depositFee = (amount * depositFeeBP) / 10000;
uint256 amountAfterFee = amount - depositFee;
stakingToken.safeTransfer(feeRecipient, depositFee);
depositFeeBP in basis points (100 = 1%). Changing depositFeeBP through governance with timelock is mandatory, otherwise the owner can set 100% fee and confiscate all deposits.
Stack and Testing
Foundry for testing: fuzzing on boundary values of accRewardPerShare, multi-user scenario tests (10 users with different deposits/withdrawals), invariant checking through invariant tests.
Key invariant: SUM(pendingRewards for all users) <= balance(rewardToken) in the contract. Violating this invariant means the contract promises more than it has.
Echidna for property-based testing of reward mathematics — generates random sequences of operations and verifies that invariants are not violated.
Process and Timeline
Design (1 day). Choosing reward model (single token vs multi-pool), parameters (rewardPerBlock, depositFee, lock period), governance model for parameter changes.
Development (2-3 days). Basic implementation plus extensions. OpenZeppelin for ReentrancyGuard, SafeERC20, Ownable. Custom logic is minimal.
Testing (1-2 days). Foundry fuzzing, multi-user scenarios, edge cases: zero totalStaked, multiplication overflow, reward pool depletion.
Total: 3-5 days to a contract ready for audit. For production, we recommend external audit — farming contracts hold TVL and are actively attacked.







