@@ -11,7 +11,6 @@ import {
1111 SET_WEIGHT_RECORDS,
1212 STEP_WEIGHT_RECORDS,
1313 SET_SHARES,
14- GET_STATE,
1514 REGISTER_STREAM,
1615 REMOVE_STREAM,
1716 SET_DISTRIBUTION,
@@ -24,10 +23,13 @@ import {WeightRecord, DistributionKind, Share, PendingOp} from "../../src/lib/FV
2423/// @dev Weights, and per-orchestrator shares, are WAD-scaled: 1e18 == 1.0 == 100%.
2524int256 constant WAD = 1e18 ;
2625
27- /// @dev Mock-only caps; f02 requires these limits to exist but never fixes their values .
26+ /// @dev f02's caps, fixed by FIP-0118 .
2827uint64 constant MAX_STREAMS = 8 ;
2928uint256 constant MAX_RECIPIENTS = 64 ;
3029
30+ /// @dev FRC-0042's floor. Below it a method is internal API, closed to EVM callers.
31+ uint64 constant FIRST_EXPORTED_METHOD_NUMBER = 1 << 24 ;
32+
3133/// @dev Same value as WAD, typed uint256, so summing shares needs no signed-to-unsigned cast.
3234uint256 constant SHARE_TOTAL = 1e18 ;
3335
@@ -36,8 +38,8 @@ struct LedgerRow {
3638 uint256 amount;
3739}
3840
39- /// @dev Enumerable, prunable address->uint256 balance -- plain mappings- plus- array, not the
40- /// builtin-actor 's CBOR- behind-a- CID shape (fine: nothing implements that wire format yet) .
41+ /// @dev Enumerable, prunable address->uint256 balance, as plain mappings plus an array. This is
42+ /// not f02 's on-chain shape, which is CBOR behind a CID, and no contract can read either one .
4143struct Ledger {
4244 mapping (address => uint256 ) amount;
4345 mapping (address => uint256 ) indexPlusOne; // 0 == not tracked
@@ -103,11 +105,21 @@ struct PendingView {
103105 address writer;
104106}
105107
108+ /// @notice Everything mockState reports, bundled so call sites don't juggle an 8-way tuple.
109+ struct MockState {
110+ uint256 totalMintedReward;
111+ uint256 totalBurnMinted;
112+ uint256 totalServiceMinted;
113+ uint64 nextTransitionEpoch;
114+ uint64 swaTimelockEpochs;
115+ StreamView[] streams;
116+ TombstoneView[] tombstones;
117+ PendingView[] pendingWrites;
118+ }
119+
106120/// @notice Mock for the Filecoin Reward actor (f02), covering its stream-splitting methods.
107121/// @dev Etch at REWARD_ACTOR_ADDRESS via MockRewardTest, which also re-etches CALL_ACTOR_BY_ID
108122/// to reach handle_filecoin_method below.
109- /// @dev GetState persists due writes rather than only projecting them; behaviorally identical
110- /// once `effectiveEpoch` has passed.
111123contract FVMRewardActor {
112124 /// @dev Survives vm.etch: immutables are baked into runtime bytecode at deploy time.
113125 Vm private immutable VM;
@@ -198,7 +210,7 @@ contract FVMRewardActor {
198210 emit BlockRewardAwarded (br, minerPortion, servicePortion, burnAmount);
199211 }
200212
201- /// @notice Test helper: an EXPLICIT stream's wallet-to-share map, without the GetState round trip .
213+ /// @notice Test helper: an EXPLICIT stream's wallet-to-share map.
202214 function getShares (uint64 streamId ) external view returns (Share[] memory ) {
203215 return _streams[streamId].shares;
204216 }
@@ -214,7 +226,8 @@ contract FVMRewardActor {
214226 }
215227
216228 /// @notice Test helper: the clamp(v_start + slope*(e-t_start), floor, cap) math, exposed
217- /// directly since it isn't a dispatched method (GetState already projects each weight).
229+ /// directly: an SWA has to mirror this schedule itself, and the mock is where a divergence
230+ /// between its copy and f02's should surface.
218231 function clampWeight (WeightRecord memory record , uint64 epoch ) external pure returns (int256 ) {
219232 return _clampWeight (record, epoch);
220233 }
@@ -234,11 +247,14 @@ contract FVMRewardActor {
234247 external
235248 returns (uint32 , uint64 , bytes memory )
236249 {
250+ // restrict_internal_api: the internal API (AwardBlockReward, ThisEpochReward,
251+ // UpdateNetworkKPI, Constructor) is closed to EVM callers, and everything reaching a mock
252+ // through CALL_ACTOR_BY_ID is one. ThisEpochReward is not a back door.
253+ if (method < FIRST_EXPORTED_METHOD_NUMBER) return (USR_FORBIDDEN, 0 , "" );
237254 _settle ();
238255 if (method == SET_WEIGHT_RECORDS) return _queueWeightWrite (PendingOp.SET_WEIGHT, params);
239256 if (method == STEP_WEIGHT_RECORDS) return _queueWeightWrite (PendingOp.STEP_WEIGHT, params);
240257 if (method == SET_SHARES) return _setShares (params);
241- if (method == GET_STATE) return _getState ();
242258 if (method == REGISTER_STREAM) return _registerStream (params);
243259 if (method == REMOVE_STREAM) return _removeStream (params);
244260 if (method == SET_DISTRIBUTION) return _setDistribution (params);
@@ -305,6 +321,10 @@ contract FVMRewardActor {
305321
306322 uint256 total;
307323 for (uint256 i = 0 ; i < newShares.length ; i++ ) {
324+ if (newShares[i].share == 0 ) return (USR_ILLEGAL_ARGUMENT, 0 , "" );
325+ for (uint256 j = 0 ; j < i; j++ ) {
326+ if (newShares[j].wallet == newShares[i].wallet) return (USR_ILLEGAL_ARGUMENT, 0 , "" );
327+ }
308328 total += newShares[i].share;
309329 }
310330 if (total != SHARE_TOTAL) return (USR_ILLEGAL_ARGUMENT, 0 , "" );
@@ -318,7 +338,13 @@ contract FVMRewardActor {
318338 return (0 , 0 , "" );
319339 }
320340
321- function _getState () internal view returns (uint32 , uint64 , bytes memory ) {
341+ /// @notice Test helper: the mock's whole state, read directly rather than through a method.
342+ /// @dev f02 exposes no reads at all, so an SWA or SRA must mirror anything it needs in its own
343+ /// state. Tests are not so constrained, and reading here keeps that asymmetry visible.
344+ /// @dev A true view: it does not settle. Advancing the epoch and reading without an
345+ /// intervening mutating call shows nothing applied, exactly as f02 behaves. Use mockSettle to
346+ /// apply due writes.
347+ function mockState () external view returns (MockState memory ) {
322348 uint64 nowEpoch = uint64 (block .number );
323349
324350 StreamView[] memory streams = new StreamView [](_streamIds.length );
@@ -358,20 +384,21 @@ contract FVMRewardActor {
358384 });
359385 }
360386
361- return (
362- 0 ,
363- 0 ,
364- abi.encode (
365- totalMintedReward,
366- totalBurnMinted,
367- totalServiceMinted,
368- nextTransitionEpoch,
369- swaTimelockEpochs,
370- streams,
371- tombstones,
372- pendingWrites
373- )
374- );
387+ return MockState ({
388+ totalMintedReward: totalMintedReward,
389+ totalBurnMinted: totalBurnMinted,
390+ totalServiceMinted: totalServiceMinted,
391+ nextTransitionEpoch: nextTransitionEpoch,
392+ swaTimelockEpochs: swaTimelockEpochs,
393+ streams: streams,
394+ tombstones: tombstones,
395+ pendingWrites: pendingWrites
396+ });
397+ }
398+
399+ /// @notice Test helper: applies due writes, as f02 does at the head of every mutating call.
400+ function mockSettle () external {
401+ _settle ();
375402 }
376403
377404 function _registerStream (bytes calldata params ) internal returns (uint32 , uint64 , bytes memory ) {
@@ -465,6 +492,9 @@ contract FVMRewardActor {
465492 if (msg .sender != swa) return (USR_FORBIDDEN, 0 , "" );
466493 // Params CBOR: [id, op]
467494 (uint64 id , PendingOp op ) = _decodeCancelPendingParams (params);
495+ // StepWeightRecords is the one uncancellable op: the discretionary path must not be able
496+ // to revoke a governance-gated write.
497+ if (op == PendingOp.STEP_WEIGHT) return (USR_ILLEGAL_ARGUMENT, 0 , "" );
468498 if (_pendingExists[id][op]) {
469499 delete _pending[id][op];
470500 _pendingExists[id][op] = false ;
@@ -827,8 +857,10 @@ contract FVMRewardActor {
827857 }
828858
829859 /// @dev Per-record sanity required at write time: 0 <= floor <= cap <= 1.
860+ /// @dev validate_weight_record: floor <= v_start <= cap <= DENOM. The lower bound on floor
861+ /// is implicit in f02, where these three are u64; here they are signed and it is not.
830862 function _sane (WeightRecord memory w ) internal pure returns (bool ) {
831- return w.floor >= 0 && w.floor <= w.cap && w.cap <= WAD;
863+ return w.floor >= 0 && w.floor <= w.cap && w.cap <= WAD && w.vStart >= w.floor && w.vStart <= w.cap ;
832864 }
833865
834866 /// @dev Sum of every registered stream's weight at `atEpoch`, excluding `excludeIds`.
0 commit comments