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 | |
|---|---|---|
| Deployment | Deployed on Ethereum; campaign ended | Deployed on Ethereum; no campaign running and none planned |
| What you stake | Uniswap V2 LP tokens (launch farming) | Motoswap LP tokens; no campaign, so nothing earns here |
| Emission | Flat: rewardBudget / DURATION, one window of 14 days (DURATION() on the contract is the source of truth), one campaign ever | No 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 fee | None | Per pool, depositFeeBps, capped at MAX_DEPOSIT_FEE_BPS = 1000 |
| Withdraw | Instant, one call | Two calls: withdraw queues, claimWithdrawal pays after the cooldown |
| Reward payout | 50% liquid, 50% streamed over 180 days | Same |
| 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;nullif 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:
vested = amount / 2, rounded down.amount - vestedMOTO is transferred to you in the same transaction. An odd wei goes to the liquid half.vestedMOTO is handed toRewardVestingEscrow.depositFor(you, vested).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)cooldownEndisnow + 24 hours(COOLDOWN), capped at the campaign'semissionEnd. Read it from the event or fromuserInfo.- The cooldown is waived, so
cooldownEndis the current block time, whencooldownWaivedFor(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,depositForandharveston that pool all revertCooldownActive(). Claim it first. claimWithdrawalbeforecooldownEndrevertsCooldownNotElapsed().
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 fromemissionStarttoemissionEnd, 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. PeriodkemitsinitialPeriodReward >> k, spread linearly overcampaignHalvingPeriod. The running campaign's schedule iscampaignHalvingPeriodandcampaignNumPeriods;halvingPeriodandnumPeriodsconfigure 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 timet, andemissionBetween(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
| Error | Chef | Cause |
|---|---|---|
Unauthorized() | both | depositFor top-up of someone else's existing position. |
InsufficientStaked() | both | withdraw with zero, or more than stakedBalance. |
InvalidPool() | both | pid >= poolLength(). |
ZeroAddress() | both | depositFor with a zero user. |
CooldownActive() | MasterChef | deposit, depositFor, harvest or withdraw while a withdrawal is queued on that pool. |
CooldownNotElapsed() | MasterChef | claimWithdrawal before cooldownEnd. |
NoPendingWithdrawal() | MasterChef | claimWithdrawal with nothing queued. |
RewardsExhausted() is not reachable from a user call. It guards the owner's
recoverUndistributedRewards only.