Feat PancakeSwap V3 MasterChef NFT Staking#647
Open
VeXHarbinger wants to merge 9 commits into
Open
Conversation
…(unconditional zero-liquidity skip)
- Add PancakeswapV3Masterchef.abi.json (complete, with poolInfo/userPositionInfos) - Add Pancakeswap.getV3PoolIdFromMasterChef() with pid-0 fix (verifies via poolInfo(0).v3Pool so the CAKE/WBNB pool at pid 0 is not falsely rejected) - Add Pancakeswap.unstakeNft() with precondition: checks NFT is currently owned by MasterChef before calling withdraw(), giving a friendly 400 instead of a revert - Add Pancakeswap.stakeNft() with pure address-order isBaseToken0 (no WETH heuristic) - Add 4 nft-staking route handlers (masterchef-knows-pool, masterchef-stake, masterchef-unstake, masterchef-unstake-and-close) - All handlers use fastify.httpErrors.* — no reply.status(500).send() - unstake-and-close uses await tx.wait(1) confirmation, not setTimeout(2000) - Schemas live in pancakeswap/schemas.ts (not inline in route files) - Register nftStaking wrapper in pancakeswap.routes.ts - 35 unit tests across 4 suites: happy paths, edge cases, missing params Fixes fengtality review comments (blocking + important) from PR hummingbot#638
Closed
13 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: PancakeSwap V3 MasterChef NFT Staking — 4 routes, ABI, unit tests
Dependency
Full integration testing requires gateway#646 to be merged first.
PR #646 provides the PancakeSwap CLMM foundation this branch builds on:
the
pancakeswapV3MasterchefAddresscontract addresses, thepositions-ownedaddress-order fix, the
executeSwapBigInt fix, and thequotePositionprecisionfix. The MasterChef staking routes are non-functional without those pieces in place.
Related PRs
Why This Exists — The Problem Being Solved
PancakeSwap V3 LP positions earn trading fees passively, but that is only half the
yield available on BSC. PancakeSwap's MasterChef V3 contract is the yield-farming
layer on top: by depositing a V3 NFT position into MasterChef, an LP also earns
CAKE token rewards for the entire duration the position stays staked.
Without this PR, a Hummingbot strategy using Gateway can open and manage V3 positions
but has no way to stake them — it leaves CAKE rewards on the table every block. This
PR adds the complete lifecycle:
How MasterChef V3 Staking Works
MasterChef V3 is an ERC721 receiver contract. The deposit mechanism is
safeTransferFrom— you do not call adeposit()function. You transfer theNFT to the MasterChef contract, which triggers its
onERC721Receivedcallback andregisters the deposit. This is the correct Uniswap V3 fork staking pattern and is
why
stakeNft()callsnftManager.safeTransferFrom(wallet, masterChefAddress, tokenId)rather than any MasterChef-specific deposit method.
Withdrawal is the reverse:
masterChef.withdraw(tokenId, walletAddress)transfersthe NFT back and harvests all accumulated CAKE in a single on-chain call, which is
why unstake is a single transaction (no separate harvest step needed).
Why Each Design Decision Was Made
1 —
masterchef-knows-poolis a separate read-only endpointNot every V3 pool is registered in MasterChef. The MasterChef owner adds pools
explicitly; a valid NFT position in an unregistered pool will revert on stake with
no useful error. A Hummingbot strategy needs to check eligibility before paying
gas. This endpoint is read-only (no wallet, no gas) and intentionally separate from
masterchef-stakeso strategies can query it independently and build decision logicaround it.
The pid-0 ambiguity:
v3PoolAddressPidis a Soliditymapping(address => uint256)that returns0for both unregistered addressesand the legitimately registered pool at pid 0 (typically CAKE/WBNB on BSC).
A naive
poolId === 0 → not registeredcheck would permanently lock out the firstregistered pool. The fix is to call
poolInfo(0).v3Poolwhen the mapping returns 0and match the address — only a mismatch means unregistered.
2 —
httpErrorsinstead ofreply.status(500).sendHummingbot Python strategies parse Gateway error responses to decide whether to
retry (server error, transient) or abort and alert (bad request, caller must
fix something). Returning HTTP 500 for "MasterChef not approved" or "NFT not staked"
makes these indistinguishable from a crashed server. All four handlers now use
fastify.httpErrors.badRequest()for caller-fixable preconditions andfastify.httpErrors.internalServerError()for unexpected failures, matching therepo convention in
CLAUDE.mdand giving Hummingbot the signal it needs.3 —
ownerOfprecondition check beforewithdrawCalling
masterChef.withdraw(tokenId, ...)when the NFT is not staked causes anopaque ethers revert — the on-chain error propagates as an unstructured string that
tells the caller nothing actionable. The check costs one
eth_calland eliminatesa class of confusing failures before they reach the chain.
4 —
tx.wait(1)instead ofsetTimeout(2000)in unstake-and-closeBSC block time is approximately 3 seconds. A fixed 2-second sleep is shorter than
one block, meaning the NFT would frequently still be owned by MasterChef when
closePositionis called, causing the close to revert.tx.wait(1)blocks untilthe unstake transaction is included in a confirmed block — the NFT is guaranteed to
be back in the wallet before close is attempted, regardless of network conditions.
5 — Address-order comparison for
isBaseToken0The previous code used a WETH special-case to determine which token is "base" — a
pattern imported from Ethereum mainnet code that breaks on BSC where the wrapped
native is WBNB, not WETH. Uniswap V3 forks define
token0canonically as thelexicographically lower address. Using
token0.address.toLowerCase() < token1.address.toLowerCase()matches exactly how the pool contract itself determines token ordering and works
correctly on every EVM network without special-casing any token.
6 — TypeBox schemas in
schemas.ts, not inlineInline schema definitions in route files create inconsistency and make it harder
to audit the full API surface. Centralising all four schema pairs in
src/connectors/pancakeswap/schemas.tsis consistent with every other connector inthis repo and makes the TypeBox → Swagger generation path uniform.
New Endpoints
All routes are registered under
/connectors/pancakeswap/nft-staking/.masterchef-knows-poolregisteredboolean andpidmasterchef-stakesafeTransferFromto begin CAKE reward accrualmasterchef-unstakewithdraw(tokenId, wallet)to return the NFT and harvest all accumulated CAKE in one transactionmasterchef-unstake-and-closePre-flight Requirements for Staking
Before calling
masterchef-stakethe caller must:masterchef-knows-poolfirst.setApprovalForAll(masterchefAddress, true)on the NonfungiblePositionManager contract (0xEfF92A263d31888d860bD50809A8D171709b7b1con BSC). This is a one-time approval per wallet.Changed Files
New Files
src/connectors/pancakeswap/PancakeswapV3Masterchef.abi.jsonpoolInfo,userPositionInfos,withdraw,v3PoolAddressPid, all eventssrc/connectors/pancakeswap/nft-staking/index.tssrc/connectors/pancakeswap/nft-staking/masterchef-knows-pool.tsPOST /masterchef-knows-poolhandlersrc/connectors/pancakeswap/nft-staking/masterchef-stake.tsPOST /masterchef-stakehandlersrc/connectors/pancakeswap/nft-staking/masterchef-unstake.tsPOST /masterchef-unstakehandlersrc/connectors/pancakeswap/nft-staking/masterchef-unstake-and-close.tsPOST /masterchef-unstake-and-closehandlertest/connectors/pancakeswap/nft-staking/masterchef-knows-pool.test.tstest/connectors/pancakeswap/nft-staking/masterchef-stake.test.tstest/connectors/pancakeswap/nft-staking/masterchef-unstake.test.tstest/connectors/pancakeswap/nft-staking/masterchef-unstake-and-close.test.tsModified Files
src/connectors/pancakeswap/pancakeswap.tsmasterChefContract property;init()instantiates it; addedgetV3PoolIdFromMasterChef(),getPoolMasterchefData(),stakeNft(),unstakeNft()methodssrc/connectors/pancakeswap/pancakeswap.routes.tspancakeswapNftStakingRoutesWrapperandnftStakingkey in exportssrc/connectors/pancakeswap/schemas.tsSwagger / OpenAPI Documentation
All four endpoints are fully documented in the auto-generated Swagger UI at
/docs.POST /connectors/pancakeswap/nft-staking/masterchef-knows-poolSummary: Check whether a V3 pool is registered in MasterChef
networkbscbsc,mainnet,arbitrum,basepoolAddressregisteredpidregistered: true)POST /connectors/pancakeswap/nft-staking/masterchef-stakeSummary: Stake a V3 NFT position into MasterChef for CAKE rewards
networkbscwalletAddresstokenIdsignaturestatus1= successfeePOST /connectors/pancakeswap/nft-staking/masterchef-unstakeSummary: Unstake a V3 NFT position from MasterChef and harvest CAKE
networkbscwalletAddresstokenIdsignaturestatus1= successfeePOST /connectors/pancakeswap/nft-staking/masterchef-unstake-and-closeSummary: Unstake a V3 NFT from MasterChef and close the position
networkbscwalletAddresstokenIdunstakeSignaturecloseSignaturestatus1= success)feeUnit Tests
35 tests across 4 suites. All passing.
masterchef-knows-pool.test.tspoolAddress; empty body; network forwarded correctlymasterchef-stake.test.tstokenId; missingwalletAddress; empty body; NFT not owned (400); MasterChef not approved (400); pool not registered (400); zero liquidity (400); RPC error (500)masterchef-unstake.test.tstokenId; missingwalletAddress; empty body; NFT not staked (400); wallet not found (400); RPC error (500)masterchef-unstake-and-close.test.tstokenId; missingwalletAddress; empty body; NFT not staked — close not called (400); unstake OK but close fails — error includes unstake tx hash (500); wallet not found (400)Integration Testing
The following manual steps require gateway#646 merged and a running Gateway instance pointed at BSC.
Prerequisites
Test 1 — Check pool registration
Test 2 — Stake NFT
Test 3 — Unstake NFT
Test 4 — Unstake and close in one call
Expected Error Scenarios
NFT not staked:
{"statusCode":400,"error":"Bad Request","message":"NFT 1234 is not staked in MasterChef (current owner: 0xWallet...). You must stake the position first."}Pool not registered:
{"statusCode":400,"error":"Bad Request","message":"Pool 0x... is not registered in MasterChef V3"}MasterChef not approved:
{"statusCode":400,"error":"Bad Request","message":"Insufficient NFT approval. Please approve the position NFT (1234) for the Pancakeswap Position Manager (0x556B9306565093C855AEA9AE92A594704c2Cd59e)"}Backwards Compatibility
All changes are additive:
/nft-staking/sub-path — no existing routes modified.pancakeswap.routes.tsaddsnftStakingkey alongsiderouter,amm,clmm— no key renamed or removed.pancakeswap.tsadds new public methods and a privatemasterChefproperty — no existing method signatures changed.schemas.tsadditions only — no existing schema types mutated.Developer Notes —
.github/copilot-instructions.md.github/copilot-instructions.mdwas added in this branch and contains identicalcontent to
CLAUDE.md. Both files carry the same agent directives, including anexplicit instruction to keep them in sync with each other.
Why it exists:
VS Code automatically reads
.github/copilot-instructions.mdfor any workspacewhere a GitHub Copilot subscription is active — including when Claude is accessed
via the GitHub Copilot API. This means contributors who work in VS Code get the
same agent context (lenses, coding style, architecture rules) that
CLAUDE.mdprovides to Claude directly, without needing to reference
CLAUDE.mdexplicitlyin every prompt.
How sync is maintained:
Both files contain the directive:
Any agent (Copilot, Claude, Gemini) that edits project rules will read this
directive and update both files in the same commit.
What the lenses add:
The lenses are named, focused review constraints that any AI code-review agent
applies before proposing a solution. Each lens narrows the acceptable answer space
from a specific perspective:
chain/network/chainNetworksemantics consistent; prevents wallet-scoping errorshttpErrorsusagegetSafeWalletFilePath()for all file I/O.mdfiles render-clean (headings, lists, fenced blocks, no bare URLs)descriptionfields, add Swaggerexamples, and updateCLAUDE.md/copilot-instructions.mdoperationId,summary,tags, response schemas matching handler return typesThe lenses were designed so that an agent reviewing a PR sees the same checklist
a human reviewer would apply — reducing back-and-forth on style and convention
issues before a maintainer looks at the code.