SIG-625: accept ERC721 approved operators for position lifecycle auth#69
Conversation
There was a problem hiding this comment.
Summary
This PR refactors position lifecycle authorization in TradeModule and SignalsRouter to accept ERC721 approved operators (getApproved and isApprovedForAll) in addition to direct NFT owners. The new _resolvePositionOwner(positionId) function resolves the actual NFT owner for event emission and fee-policy trader attribution, while keeping all fund pulls/pushes on msg.sender. The router's take-and-return custody pattern (_takePosition/_returnPositionIfNeeded) is removed entirely — the router now calls Core directly as an approved operator, eliminating spurious NFT Transfer events and fixing trader attribution in all trade events.
Cross-PR Context
Sibling PRs (SIG-625)
No sibling PRs found in other repositories.
Impact Map Check
- v1-subgraph:
TradeModule.solchanges affect trade event handlers. However, no event signatures changed — same event names, same parameter types and count. Thetraderfield now contains the actual NFT owner instead of the router address, which is a semantic improvement. Subgraph mappings continue to work without modification. No sibling PR required. - v1-sdk: No ABI signature changes. The
_resolvePositionOwnerfunction isinternaland does not affect the external ABI. No SDK update needed. No sibling PR required. - Historical data: The PR description correctly notes that historical subgraph data indexed before deployment will still have the router address as
trader. Reindexing may be needed post-deployment.
Issues
No issues found.
Suggestions
No suggestions.
Verdict
APPROVE: The authorization change is well-scoped and follows standard ERC721 approval semantics. Key verification points:
-
_resolvePositionOwnercorrectness: ChecksownerOffirst (fast path for direct owner), thengetApprovedandisApprovedForAll— the complete ERC721 authorization surface. Returns the actual owner (notmsg.sender), which is the correct value for event emission and fee-policy attribution. -
Fund flow integrity: All
_pullPaymentand_pushPaymentcalls remain onmsg.sender, not the resolved owner. This means the operator (caller) provides funds forincreasePositionand receives proceeds fordecreasePosition/closePosition/claimPayout. This is the standard ERC721 operator pattern — the approved party acts on behalf of the owner and handles the economic side. -
Sponsored position handling: In
_processClaimPayout, the sponsored payout split sendsuserProfittomsg.sender(operator) andsponsorReturnto the sponsor. The testtest_claimPayout_WIN_approvedOperator_gets_user_profit_sponsor_gets_principalexplicitly verifies this behavior. -
Router simplification: The custody pattern removal is clean —
_takePosition/_returnPositionIfNeededare replaced by_requirePositionOwner(ownership check only), and Core handles authorization via the approval mechanism. The router'sonERC721Receivedstill exists for theopenPositionWithSwapflow where the position NFT is minted to the router. -
Event signature stability: No event signatures changed. All trade events (
PositionIncreased,PositionDecreased,PositionClosed,PositionClaimed,PositionSettled,TradeFeeCharged) retain their existing parameter types and order. Only the semantic meaning of thetraderfield changes (now actual owner instead of router). -
Test coverage: Comprehensive regression tests cover
setApprovalForAllandapproveoperator flows across increase, decrease, claim, and batch claim. Fee-policy trader attribution is verified viaMockTraderFeePolicy. Router tests verify no spurious NFT transfers occur.
### Context Fork test for the operator upgrade lifecycle introduced in SIG-625 (PR #69). Validates that after upgrading TradeModule and deploying a new SignalsRouter on a production fork, the approved-operator pattern works end-to-end with real on-chain state. ### Decisions - Uses `ForkBaseTest` base class for prod fork infrastructure (RPC, contract addresses, owner safe) - Tests all four lifecycle operations: increase, decrease, close, claim — each verifying events reference the trader address (not the router) and no spurious NFT transfers occur - Includes a negative test ensuring non-owners cannot abuse router approval to manipulate others' positions - Market settlement in `claimPayout` test uses `markSettlementFailed` → `finalizeSecondarySettlement` path (deterministic tick placement without oracle dependency) - Early-return pattern (`if (address(router) == address(0)) return`) gracefully skips when fork env lacks required contract addresses ### Sibling PRs - `v1-contract` PR #69: SIG-625: accept ERC721 approved operators for position lifecycle auth (CLOSED)
Context
TradeModule lifecycle functions (
increasePosition,decreasePosition,closePosition,claimPayout) requiredmsg.senderto be the exact NFT owner. This forced SignalsRouter into a take→return custody pattern (transfer NFT to router, execute, transfer back), which caused all trade events to emit the router address astraderand generated spuriousTransferevents. Subgraph, fee policy, and user stats were all attributed to the router contract instead of the actual user.SIG-625
Decisions
_resolvePositionOwner(positionId)in TradeModule that accepts ERC721getApprovedandisApprovedForAlloperators, returning the actual NFT owner. All lifecycle functions now use the resolved owner for event emission and fee-policy trader attribution, while keeping token pulls/pushes onmsg.sender._takePosition/_returnPositionIfNeededcustody pattern entirely. Router lifecycle entrypoints now verifyownerOf(positionId) == msg.senderupfront and call Core directly as an approved operator._decreasePositionInternalreturns a 5th value (trader) sodecreasePositionandclosePositioncan emit the resolved owner without a redundantownerOfcall.MockTraderFeePolicytest double that returns different fees based onparams.traderto prove fee-policy attribution correctness.Impact
Event semantics change for router-mediated lifecycle calls:
PositionIncreased,PositionDecreased,PositionClosed,PositionClaimed,PositionSettled,TradeFeeCharged—traderfield now contains the actual NFT owner instead of the router address.Transferevents per router lifecycle call are eliminated.v1-subgraph: Trade event handlers already index on thetraderfield. No mapping changes needed — the correct address now arrives automatically. However, historical data indexed before this change will still have the router address.v1-sdk: No ABI signature changes. No SDK update needed.No sibling PRs exist yet. Subgraph reindexing may be needed after deployment to correct historical router-attributed trades.
Risk Areas
_resolvePositionOwner: now acceptsgetApprovedandisApprovedForAllin addition to direct ownership — broadens the set of callers that can operate on a position._splitSponsoredPayoutlogic pushes funds tomsg.sender(operator) for the user portion.batchClaimPayoutresolves ownership per-position inside the loop — each_processClaimPayoutcall runs a separate_resolvePositionOwnercheck.