Master Chef Farm
SushiSwap developed a yield farming implementation for multiple farms on the
same smart contract called MasterChef
during DeFi Summer 2020. It quickly
became a very popular way to incentivize liquidity providers to provide their
assets to SushiSwap, pulling liquidity away from Uniswap V2 in the process. This
event was referred to as the SushiSwap Vampire Attack.
Each of farm on a MasterChef
contract is called a pool (not to be confused
with an decentralized exchange pool). Each pool has a token that can be accepted
as a deposit, and one or more tokens that are emitted as a reward as a claimable
amount to the yield farmers.
Using the MasterChefFarmContractPositionHelper
The MasterChefFarmContractPositionHelper
helper class can be used to build a
list of ContractPosition
objects that represent farms on a MasterChef
fork
contract. In this example, we'll look at Stargate LP token staking. We'll
also look at SushiSwap MasterChefV2
LP token staking to complement the
lesson.
Stargate enables cross-chain bridging by incentivizing stablecoin holders on
different L1s and L2s to pool their assets. In return for pooling their assets,
these liquidity providers receive LP tokens that represent their positions.
These LP tokens can be staked in the Stargate MasterChef
contract to earn
STG
rewards.
First, let's generate a new contract position fetcher with
pnpm studio create-contract-position-fetcher stargate
. When prompted for a
group, select Create New
, then enter farm
as the ID and Farms
as the
label. When prompted for a network, select arbitrum
.
Let's now open up our newly generator boilerplate in
src/apps/synthetix/arbitrum/stargate.farm.contract-position-fetcher.ts
:
import { Inject } from "@nestjs/common";
import { IAppToolkit, APP_TOOLKIT } from "~app-toolkit/app-toolkit.interface";
import { Register } from "~app-toolkit/decorators";
import { PositionFetcher } from "~position/position-fetcher.interface";
import { ContractPosition } from "~position/position.interface";
import { Network } from "~types/network.interface";
import { StargateContractFactory } from "../contracts";
import { STARGATE_DEFINITION } from "../stargate.definition";
const appId = STARGATE_DEFINITION.id;
const groupId = STARGATE_DEFINITION.groups.farm.id;
const network = Network.ARBITRUM_MAINNET;
@Register.ContractPositionFetcher({ appId, groupId, network })
export class ArbitrumStargateFarmContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(StargateContractFactory)
private readonly stargateContractFactory: StargateContractFactory
) {}
async getPositions() {
return [];
}
}
Reference the helper class through the AppToolkit
We'll use the MasterChefFarmContractPositionHelper
helper class registered in
our AppToolkit
to quickly build the farm contract positions. We'll call the
getPositions
method on this helper class.
@Register.ContractPositionFetcher({ appId, groupId, network })
export class ArbitrumStargateFarmContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(StargateContractFactory)
private readonly stargateContractFactory: StargateContractFactory
) {}
async getPositions() {
return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions(
{
// ...
}
);
}
}
Add appId
, groupId
, and network
parameters
We'll specify our appId
, groupId
, and network
identifiers. These should
match the values specified in the @Register.ContractPositionFetcher
decorator.
@Register.ContractPositionFetcher({ appId, groupId, network })
export class ArbitrumStargateFarmContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(StargateContractFactory)
private readonly stargateContractFactory: StargateContractFactory
) {}
async getPositions() {
return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions(
{
network: Network.ARBITRUM_MAINNET,
appId: STARGATE_DEFINITION.id,
groupId: STARGATE_DEFINITION.groups.farm.id,
// ...
}
);
}
}
Add dependencies
parameter
We'll use the dependencies
parameter to specify which token groups are
required as dependencies for building this set of contract positions. In the
case of Stargate farms, the user deposits Stargate Pool tokens into the
farms, so we'll reference this app group in the dependencies
array.
@Register.ContractPositionFetcher({ appId, groupId, network })
export class ArbitrumStargateFarmContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(StargateContractFactory)
private readonly stargateContractFactory: StargateContractFactory
) {}
async getPositions() {
return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions(
{
network: Network.ARBITRUM_MAINNET,
appId: STARGATE_DEFINITION.id,
groupId: STARGATE_DEFINITION.groups.farm.id,
dependencies: [
{
appId: STARGATE_DEFINITION.id,
groupIds: [STARGATE_DEFINITION.groups.pool.id],
network,
},
],
// ...
}
);
}
}
Add address
parameter
The address
is the deployed address of the MasterChef
fork contract. On
Arbitrum
, the contract is deployed
here.
Let's go ahead and add this property:
@Register.ContractPositionFetcher({ appId, groupId, network })
export class ArbitrumStargateFarmContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(StargateContractFactory)
private readonly stargateContractFactory: StargateContractFactory
) {}
async getPositions() {
return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions(
{
address: "0xea8dfee1898a7e0a59f7527f076106d7e44c2176",
network: Network.ARBITRUM_MAINNET,
appId: STARGATE_DEFINITION.id,
groupId: STARGATE_DEFINITION.groups.farm.id,
dependencies: [
{
appId: STARGATE_DEFINITION.id,
groupIds: [STARGATE_DEFINITION.groups.pool.id],
network,
},
],
// ...
}
);
}
}
Add resolveContract
parameter
We'll need to know what contract to use to make requests to the chef address.
Grab the ABI JSON from
here,
then put it in src/apps/stargate/contracts/abi/stargate-master-chef.json
.
Next, run pnpm studio generate:contract-factory stargate
to rebuild the
contract factory.
Now, we'll reference StargateMasterChef
as a generic to the getPositions
call, allowing us to safely type our resolveContract
callback. Let's see what
this looks like:
// ...
import { StargateMasterChef } from "../contracts";
// ...
@Register.ContractPositionFetcher({ appId, groupId, network })
export class ArbitrumStargateFarmContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(StargateContractFactory)
private readonly stargateContractFactory: StargateContractFactory
) {}
async getPositions() {
return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions<StargateMasterChef>(
{
address: "0xea8dfee1898a7e0a59f7527f076106d7e44c2176",
network: Network.ARBITRUM_MAINNET,
appId: STARGATE_DEFINITION.id,
groupId: STARGATE_DEFINITION.groups.farm.id,
dependencies: [
{
appId: STARGATE_DEFINITION.id,
groupIds: [STARGATE_DEFINITION.groups.pool.id],
network,
},
],
resolveFactoryContract: ({ address, network }) =>
this.stargateContractFactory.stargateMasterChef({ address, network }),
// ...
}
);
}
}
Add resolvePoolLength
parameter
We'll use the resolvePoolLength
parameter to define how our helper class will
retrieve the number of farming pools. In our case, we'll call the poolLength
method on the chef contract.
@Register.ContractPositionFetcher({ appId, groupId, network })
export class ArbitrumStargateFarmContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(StargateContractFactory)
private readonly stargateContractFactory: StargateContractFactory
) {}
async getPositions() {
return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions<StargateMasterChef>(
{
address: "0xea8dfee1898a7e0a59f7527f076106d7e44c2176",
network: Network.ARBITRUM_MAINNET,
appId: STARGATE_DEFINITION.id,
groupId: STARGATE_DEFINITION.groups.farm.id,
dependencies: [
{
appId: STARGATE_DEFINITION.id,
groupIds: [STARGATE_DEFINITION.groups.pool.id],
network,
},
],
resolveFactoryContract: ({ address, network }) =>
this.stargateContractFactory.stargateMasterChef({ address, network }),
resolvePoolLength: ({ multicall, contract }) =>
multicall.wrap(contract).poolLength(),
// ...
}
);
}
}
Add resolveDepositTokenAddress
parameter
We'll use the resolveDepositTokenAddress
parameter to define how our helper
class will resolve the staked token address for a given pool index. In our case,
we'll call the poolInfo
method with the pool index, and return the lpToken
value from the result.
@Register.ContractPositionFetcher({ appId, groupId, network })
export class ArbitrumStargateFarmContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(StargateContractFactory)
private readonly stargateContractFactory: StargateContractFactory
) {}
async getPositions() {
return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions<StargateMasterChef>(
{
address: "0xea8dfee1898a7e0a59f7527f076106d7e44c2176",
network: Network.ARBITRUM_MAINNET,
appId: STARGATE_DEFINITION.id,
groupId: STARGATE_DEFINITION.groups.farm.id,
dependencies: [
{
appId: STARGATE_DEFINITION.id,
groupIds: [STARGATE_DEFINITION.groups.pool.id],
network,
},
],
resolveFactoryContract: ({ address, network }) =>
this.stargateContractFactory.stargateMasterChef({ address, network }),
resolvePoolLength: ({ multicall, contract }) =>
multicall.wrap(contract).poolLength(),
resolveDepositTokenAddress: ({ poolIndex, contract, multicall }) =>
multicall
.wrap(contract)
.poolInfo(poolIndex)
.then((v) => v.lpToken),
// ...
}
);
}
}
Add resolveRewardTokenAddresses
parameter
We'll use the resolveRewardTokenAddresses
parameter to define how our helper
class will resolve the reward token address for a given pool index. In our case,
the reward is always the STG
token, so we'll call the stargate
method on the
farm contract.
@Register.ContractPositionFetcher({ appId, groupId, network })
export class ArbitrumStargateFarmContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(StargateContractFactory)
private readonly stargateContractFactory: StargateContractFactory
) {}
async getPositions() {
return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions<StargateMasterChef>(
{
address: "0xea8dfee1898a7e0a59f7527f076106d7e44c2176",
network: Network.ARBITRUM_MAINNET,
appId: STARGATE_DEFINITION.id,
groupId: STARGATE_DEFINITION.groups.farm.id,
dependencies: [
{
appId: STARGATE_DEFINITION.id,
groupIds: [STARGATE_DEFINITION.groups.pool.id],
network,
},
],
resolveFactoryContract: ({ address, network }) =>
this.stargateContractFactory.stargateMasterChef({ address, network }),
resolvePoolLength: ({ multicall, contract }) =>
multicall.wrap(contract).poolLength(),
resolveDepositTokenAddress: ({ poolIndex, contract, multicall }) =>
multicall
.wrap(contract)
.poolInfo(poolIndex)
.then((v) => v.lpToken),
resolveRewardTokenAddresses: ({ multicall, contract }) =>
multicall.wrap(contract).stargate(),
// ...
}
);
}
}
In MasterChef V2
, there is optionally a bonus reward token emitted by a
separate contract called a Rewarder
. We can use the
MasterChefV2ClaimableTokenStrategy
to resolve reward token addresses for this
case.
resolvePrimaryClaimableToken
: Resolves the reward token emitted by theMasterChef
contractresolveRewarderAddress
: Resolves the contract address of the rewarder that emits one or more bonus tokens.resolveRewarderContract
: Resolves the typedEther.js
contract to be used to make requests to the rewarder contract.resolveSecondaryClaimableToken
: Resolves the reward token emitted by theRewarder
contract.
Here's an example in the case of SushiSwap:
async getPositions() {
return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions<SushiSwapV2MasterChef>({
// ...
resolveRewardTokenAddresses: this.masterChefRewarderClaimableTokenStrategy.build<
SushiSwapMasterChefV2,
SushiSwapMasterChefV2Rewarder
>({
resolvePrimaryClaimableToken: ({ multicall, contract }) => multicall.wrap(contract).SUSHI(),
resolveRewarderAddress: ({ multicall, contract, poolIndex }) => multicall.wrap(contract).rewarder(poolIndex),
resolveRewarderContract: ({ network, rewarderAddress }) =>
this.contractFactory.sushiSwapRewarder({ address: rewarderAddress, network }),
resolveSecondaryClaimableToken: ({ multicall, poolIndex, rewarderContract }) =>
multicall
.wrap(rewarderContract)
.pendingTokens(poolIndex, ZERO_ADDRESS, 0)
.then(v => v.rewardTokens[0]),
}),
// ...
});
}
Add resolveRewardRate
and rewardRateUnit
parameters
We'll use the resolveRewardRate
and rewardRateUnit
parameters to define how
our helper class will resolve the rate of reward emissions per block or per
second for a given pool index.
MasterChef
works by assigning weights called allocation points to each
pool in the contract. One reward rate is defined in the contract, and this is
divided up across the pools by weight.
We'll use the MasterChefDefaultRewardRateStrategy
strategy class in this case.
resolvePoolAllocPoints
: Resolves the allocation points for the given pool indexresolveTotalAllocPoints
: Resolve the total allocation points for all pools in the contractresolveTotalRewardRate
: Resolves the total reward rate distributed across all the pools in the contract
@Register.ContractPositionFetcher({ appId, groupId, network })
export class ArbitrumStargateFarmContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(StargateContractFactory)
private readonly stargateContractFactory: StargateContractFactory
) {}
async getPositions() {
return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions<StargateMasterChef>(
{
address: "0xea8dfee1898a7e0a59f7527f076106d7e44c2176",
network: Network.ARBITRUM_MAINNET,
appId: STARGATE_DEFINITION.id,
groupId: STARGATE_DEFINITION.groups.farm.id,
dependencies: [
{
appId: STARGATE_DEFINITION.id,
groupIds: [STARGATE_DEFINITION.groups.pool.id],
network,
},
],
resolveFactoryContract: ({ address, network }) =>
this.stargateContractFactory.stargateMasterChef({ address, network }),
resolvePoolLength: ({ multicall, contract }) =>
multicall.wrap(contract).poolLength(),
resolveDepositTokenAddress: ({ poolIndex, contract, multicall }) =>
multicall
.wrap(contract)
.poolInfo(poolIndex)
.then((v) => v.lpToken),
resolveRewardTokenAddresses: ({ multicall, contract }) =>
multicall.wrap(contract).stargate(),
rewardRateUnit: RewardRateUnit.BLOCK,
resolveRewardRate:
this.appToolkit.helpers.masterChefDefaultRewardsPerBlockStrategy.build(
{
resolvePoolAllocPoints: async ({
poolIndex,
contract,
multicall,
}) =>
multicall
.wrap(contract)
.poolInfo(poolIndex)
.then((v) => v.allocPoint),
resolveTotalAllocPoints: ({ multicall, contract }) =>
multicall.wrap(contract).totalAllocPoint(),
resolveTotalRewardRate: ({ multicall, contract }) =>
multicall.wrap(contract).stargatePerBlock(),
}
),
}
);
}
}
In MasterChef V2
, the Rewarder
contract mentioned in the previous section
also has a reward rate for the bonus reward token. We can use
MasterChefV2RewardRateStrategy
to resolve the primary and secondary reward
token reward rates.
resolvePoolAllocPoints
: Resolves the allocation points for the primary reward token for the given pool indexresolveTotalAllocPoints
: Resolve the total allocation points for the the primary reward token for all pools in the contractresolvePrimaryTotalRewardRate
: Resolves the total reward rate for the primary reward tokenresolveRewarderAddress
: Resolves the contract address of the rewarder that emits one or more bonus tokens.resolveRewarderContract
: Resolves the typedEther.js
contract to be used to make requests to the rewarder contract.resolveSecondaryTotalRewardRate
: Resolves the reward rate for the bonus reward token
Here's an example in the case of SushiSwap:
async getPositions() {
return this.appToolkit.helpers.masterChefContractPositionHelper.getContractPositions<SushiSwapV2MasterChef>({
// ...
resolveRewardRate: this.appToolkit.helpers.masterChefV2RewardRateStrategy.build<
SushiSwapMasterChefV2,
SushiSwapMasterChefV2Rewarder
>({
resolvePoolAllocPoints: async ({ poolIndex, contract, multicall }) =>
multicall
.wrap(contract)
.poolInfo(poolIndex)
.then(v => v.allocPoint),
resolveTotalAllocPoints: ({ multicall, contract }) => multicall.wrap(contract).totalAllocPoint(),
resolvePrimaryTotalRewardRate: async ({ multicall, contract }) => multicall.wrap(contract).sushiPerBlock(),
resolveRewarderAddress: ({ multicall, contract, poolIndex }) =>
multicall
.wrap(contract)
.poolInfo(poolIndex)
.then(v => v.rewarder),
resolveRewarderContract: ({ network, rewarderAddress }) =>
this.contractFactory.sushiSwapMasterChefV2Rewarder({ address: rewarderAddress, network }),
resolveSecondaryTotalRewardRate: async ({ multicall, rewarderContract }) =>
multicall
.wrap(rewarderContract)
.rewardPerSecond()
.catch(() => '0'),
}),
// ...
});
}
We're done!