Guides

Stake LP in a farm

The chef ABI for Motoswap farms, as contract reference. No farm program is running or planned.

No farm program is running

Launch farming (VampChef) ran once, for 14 days at token launch, and has ended. MasterChef is deployed on Ethereum but not running: no emission campaign is running and none is planned. The reference below documents both chefs' ABI and reward math as contract reference, not a live program you can stake into. LP already staked in MasterChef can be withdrawn at any time; see Emergency withdraw.

Motoswap has two farm contracts. They share the accrual core and the reward split, and differ on exits and fees.

Launch farm, ended (VampChef)MasterChef, not running
DeploymentDeployed on Ethereum; campaign endedDeployed on Ethereum; no campaign running and none planned
What you stakeUniswap V2 LP tokens (launch farming)Motoswap LP tokens; no campaign, so nothing earns here
EmissionFlat: rewardBudget / DURATION, one window of 14 days (DURATION() on the contract is the source of truth), one campaign everNo campaign running or planned. Contract math: period k emits initialPeriodReward >> k. halvingPeriod and numPeriods are settable state (42 days and 8 at initialize). Read them live.
Deposit feeNonePer pool, depositFeeBps, capped at MAX_DEPOSIT_FEE_BPS = 1000
WithdrawInstant, one callTwo calls: withdraw queues, claimWithdrawal pays after the cooldown
Reward payout50% liquid, 50% streamed over 180 daysSame
Address/config.addresses.vampChef/config.addresses.masterChef

Check which chef you are talking to

While launchPhase was vamp, GET /config served the launch farm address under both vampChef and masterChef (see the deployment status table). Every pool in GET /farms carries a chef field naming the contract it lives on, so deposit to that rather than to a key you picked. Use the VampChef ABI for the launch farm: claimWithdrawal, WithdrawQueued and deposit fees do not exist on it.

Discover the pool ids

const farms = await fetch("https://api.motoswap.org/farms").then((r) => r.json());

for (const p of farms.pools) {
  console.log(p.chef, p.pid, p.lpToken, p.allocPoint, p.depositFeeBps, p.aprBps);
}

The guide's fetch calls are simple GETs, which work from any origin. A request that triggers a CORS preflight (a script-set If-None-Match, a JSON POST) only passes for the Motoswap app origin, so make those server-side.

GET /farms is unpaginated because the pool set is bounded (one add() per pool, hard-capped at MAX_POOLS = 500). Every pool has:

  • chef - the farm contract the pool lives on.
  • pid - the on-chain pool id on that chef.
  • lpToken - the staked token. On the launch farm, a Uniswap V2 pair.
  • allocPoint - pool weight (share of emission = allocPoint / totalAllocPoint).
  • depositFeeBps - 0-1000 (up to 10%). Always 0 on the launch farm.
  • pair - the pair details for an LP pool; null if single-sided.
  • aprBps - the live APR the app shows, in basis points.

Stake

Both chefs take the same call.

import { parseAbi } from "viem";

const chefAbi = parseAbi([
  "function deposit(uint256 pid, uint256 amount)",
  "function harvest(uint256 pid)",
  "function withdraw(uint256 pid, uint256 amount)",
  "function emergencyWithdraw(uint256 pid)",
  "function pendingReward(uint256 pid, address user) view returns (uint256)",
  "function stakedBalance(uint256 pid, address user) view returns (uint256)",
]);
const erc20Abi = parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]);

// 1. Approve the LP token to the chef.
await wallet.writeContract({ address: lpToken, abi: erc20Abi, functionName: "approve", args: [chef, amount] });

// 2. Deposit. `amount` is a bigint in LP-token wei.
await wallet.writeContract({ address: chef, abi: chefAbi, functionName: "deposit", args: [pid, amount] });

deposit settles first: any pending reward is paid out, then the stake grows. The chef credits the balance that actually arrived, and on MasterChef the deposit fee comes off that amount and goes to feeRecipient. The Deposit event carries the credited amount.

Depositing for someone else

await wallet.writeContract({
  address: chef,
  abi: parseAbi(["function depositFor(uint256 pid, uint256 amount, address user)"]),
  functionName: "depositFor",
  args: [pid, amount, otherUser],
});

A third party can only open a new position. If user already has a stake in that pool, the call reverts Unauthorized() unless the caller is user or the chef's trustedZapper. The reason: a deposit settles the target's pending reward, and nobody else should be able to trigger that for you. A third-party call with amount == 0 returns without doing anything.

Live pending rewards

Off-chain APIs are indexer-driven and lag the chain. For live rewards read the chain directly:

const pending = await publicClient.readContract({
  address: chef,
  abi: chefAbi,
  functionName: "pendingReward",
  args: [pid, user],
});

Harvest

harvest(pid) pays your pending reward without touching your stake. It does exactly what deposit(pid, 0) does, under a name a wallet shows correctly.

await wallet.writeContract({ address: chef, abi: chefAbi, functionName: "harvest", args: [pid] });

How a reward is paid: the liquid half and the streamed half

Every payout (harvest, and the settle inside deposit and withdraw) goes through one internal function on both chefs:

  1. vested = amount / 2, rounded down.
  2. amount - vested MOTO is transferred to you in the same transaction. An odd wei goes to the liquid half.
  3. vested MOTO is handed to RewardVestingEscrow.depositFor(you, vested).
  4. RewardPaid(user, pid, amount) is emitted with the full amount, not the liquid half.

The escrow keeps one schedule per wallet, 180 days long (VEST_DURATION), and folds every new deposit into it:

  • What the old schedule had already released but you had not claimed is banked. It stays claimable at any time.
  • What had not released yet, plus the new amount, is re-spread linearly over a fresh 180 days starting at that block.

So each harvest restarts the clock on the unreleased tail. Harvesting often does not lose anything, but it pushes the end date out. vestEndOf(account) returns the end of the schedule as it stands.

const escrowAbi = parseAbi([
  "function claimable(address account) view returns (uint256)", // released + banked, payable now
  "function vesting(address account) view returns (uint256)",   // not released yet
  "function vestEndOf(address account) view returns (uint64)",  // 0 when there is no schedule
  "function claim() returns (uint256 amount)",
]);

await wallet.writeContract({ address: rewardVestingEscrow, abi: escrowAbi, functionName: "claim" });

claim() pays the caller only. Claiming does not reset the schedule.

On MasterChef, if the reward pot cannot cover a payout, the chef pays what is available, emits RewardShortfall(user, pid, owed, paid), and forfeits the unpaid part. The deployed VampChef does not have RewardShortfall yet (see watch events). It does not revert, so your principal can always exit. The budget is pulled up front at startEmission, so this is a backstop.

Withdraw from launch farming (ended)

One call. It settles your reward and returns the LP in the same transaction.

await wallet.writeContract({ address: chef, abi: chefAbi, functionName: "withdraw", args: [pid, amount] });
// Emits Withdraw(user, pid, amount)

InsufficientStaked() if amount is zero or more than your stake.

Withdraw from MasterChef (two-step)

withdraw queues the exit and claimWithdrawal pays it.

const masterChefAbi = parseAbi([
  "function withdraw(uint256 pid, uint256 amount)",
  "function claimWithdrawal(uint256 pid)",
  "function cooldownWaivedFor(uint256 pid) view returns (bool)",
  "function userInfo(uint256 pid, address user) view returns (uint256 amount, uint256 rewardDebt, uint256 pendingWithdrawal, uint64 cooldownEnd)",
]);

// 1. Queue. Settles rewards, stops the queued amount earning.
await wallet.writeContract({ address: masterChef, abi: masterChefAbi, functionName: "withdraw", args: [pid, amount] });
// Emits WithdrawQueued(user, pid, amount, cooldownEnd)

// 2. At or after cooldownEnd:
await wallet.writeContract({ address: masterChef, abi: masterChefAbi, functionName: "claimWithdrawal", args: [pid] });
// Emits WithdrawClaimed(user, pid, amount)
  • cooldownEnd is now + 24 hours (COOLDOWN), capped at the campaign's emissionEnd. Read it from the event or from userInfo.
  • The cooldown is waived, so cooldownEnd is the current block time, when cooldownWaivedFor(pid) is true: no campaign is emitting, or the pool (or every pool) has zero allocation. It is still two calls.
  • One queued withdrawal per (user, pid). While one is queued, withdraw, deposit, depositFor and harvest on that pool all revert CooldownActive(). Claim it first.
  • claimWithdrawal before cooldownEnd reverts CooldownNotElapsed().

Emergency withdraw (forfeit rewards)

await wallet.writeContract({ address: chef, abi: chefAbi, functionName: "emergencyWithdraw", args: [pid] });

Returns your whole active stake at once and forfeits all pending reward (RewardForfeited). On MasterChef it skips the cooldown for the active stake. An amount you already queued with withdraw is not touched and still exits through claimWithdrawal.

With no campaign running on MasterChef there is no reward to forfeit, so this is the plain way out: one call, one signature. The Farm page on motoswap.org uses it for MasterChef stakes.

Events emitted

// Both chefs
event Deposit(address indexed user, uint256 indexed pid, uint256 amount);
event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount);
event RewardPaid(address indexed user, uint256 indexed pid, uint256 amount);        // full amount, both halves
event RewardForfeited(address indexed user, uint256 indexed pid, uint256 amount);

// MasterChef only (VampChef: in the source, not in the deployed implementation yet)
event RewardShortfall(address indexed user, uint256 indexed pid, uint256 owed, uint256 paid);

// VampChef only
event Withdraw(address indexed user, uint256 indexed pid, uint256 amount);

// MasterChef only
event WithdrawQueued(address indexed user, uint256 indexed pid, uint256 amount, uint64 cooldownEnd);
event WithdrawClaimed(address indexed user, uint256 indexed pid, uint256 amount);

// RewardVestingEscrow
event Deposited(address indexed user, uint256 amount, uint256 scheduleTotal, uint64 start);
event Claimed(address indexed user, uint256 amount);

Emission math

Launch farm:

  • One campaign, started once with startEmission(budget, startTime). rewardPerSecond = rewardBudget / DURATION, flat from emissionStart to emissionEnd, then zero.

MasterChef (no campaign is running and none is planned; this is the contract math):

  • One campaign at a time, started with startEmission(initialPeriodReward, startTime) by the owner. A new one can start after the previous one ends.
  • Period 0 emits initialPeriodReward. Period k emits initialPeriodReward >> k, spread linearly over campaignHalvingPeriod. The running campaign's schedule is campaignHalvingPeriod and campaignNumPeriods; halvingPeriod and numPeriods configure the next one.
  • The whole campaign total is pulled from the owner at startEmission.

Both:

  • currentRewardPerSecond() returns the live rate (0 outside the window).
  • emittedByTime(t) is the cumulative emission up to time t, and emissionBetween(from, to) the emission in a window.
  • A pool's share is allocPoint / totalAllocPoint.

APR, client-side, with every input as a plain number in the same currency:

const SECONDS_PER_YEAR = 31_536_000;
const poolRewardPerYear = rewardPerSecond * SECONDS_PER_YEAR * (allocPoint / totalAllocPoint); // in whole MOTO
const apr = (poolRewardPerYear * motoPriceUsd) / poolTvlUsd;

GET /farms already serves this as aprBps per pool.

Common errors

ErrorChefCause
Unauthorized()bothdepositFor top-up of someone else's existing position.
InsufficientStaked()bothwithdraw with zero, or more than stakedBalance.
InvalidPool()bothpid >= poolLength().
ZeroAddress()bothdepositFor with a zero user.
CooldownActive()MasterChefdeposit, depositFor, harvest or withdraw while a withdrawal is queued on that pool.
CooldownNotElapsed()MasterChefclaimWithdrawal before cooldownEnd.
NoPendingWithdrawal()MasterChefclaimWithdrawal with nothing queued.

RewardsExhausted() is not reachable from a user call. It guards the owner's recoverUndistributedRewards only.

On this page