Skip to content

Commit bba6a4f

Browse files
committed
fix(f02): pair id with record so a batch's lengths can't mismatch
Assisted-by: Claude:claude-sonnet-4-6
1 parent 60d6ccb commit bba6a4f

4 files changed

Lines changed: 54 additions & 77 deletions

File tree

src/lib/FVMRewardTypes.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ struct WeightRecord {
1919
int256 cap;
2020
}
2121

22+
/// @notice One entry in a SetWeightRecords/StepWeightRecords batch.
23+
/// @dev Bundling the id with its record makes a batch's two lengths structurally equal -- there
24+
/// is no encoding for a mismatch, so none needs to be checked for.
25+
struct WeightRecordUpdate {
26+
uint64 id;
27+
WeightRecord record;
28+
}
29+
2230
/// @notice One entry in an EXPLICIT stream's wallet-to-share map.
2331
/// @dev Shares across a stream's map must sum to SHARE_TOTAL (1e18); a single share must
2432
/// therefore fit uint64.

src/lib/FVMRewards.sol

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
SET_SHARES,
1818
CLAIM
1919
} from "./FVMRewardMethod.sol";
20-
import {WeightRecord, Share, PendingOp} from "./FVMRewardTypes.sol";
20+
import {WeightRecord, WeightRecordUpdate, Share, PendingOp} from "./FVMRewardTypes.sol";
2121

2222
/// @notice Calls the f02 (Reward actor) methods specified by FIP-0118, for the Stream Weight
2323
/// Actor (solstice#3) and Service Rewards Actor (solstice#4).
@@ -42,9 +42,6 @@ library FVMRewards {
4242
error ValueOutOfRange(int256 value);
4343
/// @dev An implicit stream (null distribution) carries no share map.
4444
error ImplicitStreamWithShares();
45-
/// @dev The wire carries one array of `[id, record]` pairs, so the two inputs must be equal in
46-
/// length; a mismatch has no encoding.
47-
error ArrayLengthMismatch(uint256 ids, uint256 records);
4845

4946
/// @dev Sentinel for the unexpected case where the syscall reverts or returns fewer than 32
5047
/// bytes. Kept at -1 to match `fvm-solidity`'s behavior.
@@ -274,53 +271,42 @@ library FVMRewards {
274271
// -------------------------------------------------------------------------
275272

276273
/// @notice Queues a discretionary weight-schedule write, without reverting on actor error.
277-
function trySetWeightRecords(uint64[] memory ids, WeightRecord[] memory records)
278-
internal
279-
returns (int256 exitCode)
280-
{
281-
return _tryWeightRecords(SET_WEIGHT_RECORDS, ids, records);
274+
function trySetWeightRecords(WeightRecordUpdate[] memory updates) internal returns (int256 exitCode) {
275+
return _tryWeightRecords(SET_WEIGHT_RECORDS, updates);
282276
}
283277

284278
/// @notice Queues a gate-originated weight-schedule write, without reverting on actor error.
285279
/// @dev Encodes identically to SetWeightRecords and differs only in dispatch, but the two are
286280
/// separate calls because f02 treats them differently: a StepWeightRecords write cannot be
287281
/// cancelled, so the discretionary path cannot revoke what the governance gate produced.
288-
function tryStepWeightRecords(uint64[] memory ids, WeightRecord[] memory records)
289-
internal
290-
returns (int256 exitCode)
291-
{
292-
return _tryWeightRecords(STEP_WEIGHT_RECORDS, ids, records);
282+
function tryStepWeightRecords(WeightRecordUpdate[] memory updates) internal returns (int256 exitCode) {
283+
return _tryWeightRecords(STEP_WEIGHT_RECORDS, updates);
293284
}
294285

295286
/// @dev Params CBOR: `[[[id, record], ...]]` -- one array of pairs, inside the single-field
296287
/// tuple wrapper f02's parameter struct produces.
297-
function _tryWeightRecords(uint64 method, uint64[] memory ids, WeightRecord[] memory records)
298-
private
299-
returns (int256 exitCode)
300-
{
301-
if (ids.length != records.length) revert ArrayLengthMismatch(ids.length, records.length);
302-
288+
function _tryWeightRecords(uint64 method, WeightRecordUpdate[] memory updates) private returns (int256 exitCode) {
303289
// [[[id, record], ...]]
304-
(uint256 base, uint256 p) = _begin(method, MAX_HEAD_BYTES + MAX_HEAD_BYTES + ids.length * MAX_UPDATE_BYTES);
290+
(uint256 base, uint256 p) = _begin(method, MAX_HEAD_BYTES + MAX_HEAD_BYTES + updates.length * MAX_UPDATE_BYTES);
305291
p = _writeArrayHeader(p, 1);
306-
p = _writeArrayHeader(p, ids.length);
307-
for (uint256 i = 0; i < ids.length; i++) {
292+
p = _writeArrayHeader(p, updates.length);
293+
for (uint256 i = 0; i < updates.length; i++) {
308294
p = _writeArrayHeader(p, 2);
309-
p = _writeUint(p, ids[i]);
310-
p = _writeRecord(p, records[i]);
295+
p = _writeUint(p, updates[i].id);
296+
p = _writeRecord(p, updates[i].record);
311297
}
312298
return _invoke(base, p);
313299
}
314300

315301
/// @notice Queues a discretionary weight-schedule write, reverting on actor error.
316-
function setWeightRecords(uint64[] memory ids, WeightRecord[] memory records) internal {
317-
int256 exitCode = trySetWeightRecords(ids, records);
302+
function setWeightRecords(WeightRecordUpdate[] memory updates) internal {
303+
int256 exitCode = trySetWeightRecords(updates);
318304
require(exitCode == EXIT_SUCCESS, SetWeightRecordsFailed(exitCode));
319305
}
320306

321307
/// @notice Queues a gate-originated weight-schedule write, reverting on actor error.
322-
function stepWeightRecords(uint64[] memory ids, WeightRecord[] memory records) internal {
323-
int256 exitCode = tryStepWeightRecords(ids, records);
308+
function stepWeightRecords(WeightRecordUpdate[] memory updates) internal {
309+
int256 exitCode = tryStepWeightRecords(updates);
324310
require(exitCode == EXIT_SUCCESS, StepWeightRecordsFailed(exitCode));
325311
}
326312

test/mocks/FVMRewardActor.t.sol

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from "./FVMRewardActor.sol";
2626
import {CLAIM, SWA_TIMELOCK} from "../../src/lib/FVMRewardMethod.sol";
2727
import {FVMRewards} from "../../src/lib/FVMRewards.sol";
28+
import {WeightRecordUpdate} from "../../src/lib/FVMRewardTypes.sol";
2829

2930
/// @dev A distinct external caller, so tests can check authorization by identity rather than
3031
/// by happenstance of who the test contract is. The typed methods below go through FVMRewards
@@ -53,12 +54,12 @@ contract RewardCaller {
5354
exitCode = uint32(uint256(FVMRewards.tryRemoveStream(id)));
5455
}
5556

56-
function setWeightRecords(uint64[] memory ids, WeightRecord[] memory records) external returns (uint32 exitCode) {
57-
exitCode = uint32(uint256(FVMRewards.trySetWeightRecords(ids, records)));
57+
function setWeightRecords(WeightRecordUpdate[] memory updates) external returns (uint32 exitCode) {
58+
exitCode = uint32(uint256(FVMRewards.trySetWeightRecords(updates)));
5859
}
5960

60-
function stepWeightRecords(uint64[] memory ids, WeightRecord[] memory records) external returns (uint32 exitCode) {
61-
exitCode = uint32(uint256(FVMRewards.tryStepWeightRecords(ids, records)));
61+
function stepWeightRecords(WeightRecordUpdate[] memory updates) external returns (uint32 exitCode) {
62+
exitCode = uint32(uint256(FVMRewards.tryStepWeightRecords(updates)));
6263
}
6364

6465
function setDistribution(uint64 id, address writer) external returns (uint32 exitCode) {
@@ -141,26 +142,22 @@ contract FVMRewardActorTest is MockRewardTest {
141142
return swaCaller.registerStream(id, record, writer, initial, uint64(block.number) + SWA_TIMELOCK);
142143
}
143144

144-
/// @dev Bundles a single id/record into the arrays SetWeightRecords batches over.
145+
/// @dev Bundles a single id/record into the batch SetWeightRecords takes.
145146
function _singleWeightRecord(uint64 id, WeightRecord memory record)
146147
internal
147148
pure
148-
returns (uint64[] memory ids, WeightRecord[] memory records)
149+
returns (WeightRecordUpdate[] memory updates)
149150
{
150-
ids = new uint64[](1);
151-
ids[0] = id;
152-
records = new WeightRecord[](1);
153-
records[0] = record;
151+
updates = new WeightRecordUpdate[](1);
152+
updates[0] = WeightRecordUpdate({id: id, record: record});
154153
}
155154

156155
function _setWeightRecords(RewardCaller caller, uint64 id, WeightRecord memory record) internal returns (uint32) {
157-
(uint64[] memory ids, WeightRecord[] memory records) = _singleWeightRecord(id, record);
158-
return caller.setWeightRecords(ids, records);
156+
return caller.setWeightRecords(_singleWeightRecord(id, record));
159157
}
160158

161159
function _stepWeightRecords(RewardCaller caller, uint64 id, WeightRecord memory record) internal returns (uint32) {
162-
(uint64[] memory ids, WeightRecord[] memory records) = _singleWeightRecord(id, record);
163-
return caller.stepWeightRecords(ids, records);
160+
return caller.stepWeightRecords(_singleWeightRecord(id, record));
164161
}
165162

166163
function _registerExplicit(uint64 id, address writer) internal {
@@ -421,27 +418,15 @@ contract FVMRewardActorTest is MockRewardTest {
421418
assertEq(exitCode, USR_NOT_FOUND);
422419
}
423420

424-
// The wire carries one array of [id, record] pairs, so mismatched lengths have no encoding.
425-
// f02 never sees this; the caller is stopped at the boundary.
426-
function test_SetWeightRecords_MismatchedArrayLengths_Reverts() public {
427-
uint64[] memory ids = new uint64[](2);
428-
WeightRecord[] memory records = new WeightRecord[](1);
429-
vm.expectRevert(abi.encodeWithSelector(FVMRewards.ArrayLengthMismatch.selector, uint256(2), uint256(1)));
430-
swaCaller.setWeightRecords(ids, records);
431-
}
432-
433421
function test_SetWeightRecords_DuplicateIdInBatch_IllegalArgument() public {
434422
assertEq(_registerStream(SERVICE_ID, _constantRecord(0.1e18), DistributionKind.IMPLICIT, address(0)), 0);
435423
_warpPastTimelockAndSettle();
436424

437-
uint64[] memory ids = new uint64[](2);
438-
ids[0] = SERVICE_ID;
439-
ids[1] = SERVICE_ID;
440-
WeightRecord[] memory records = new WeightRecord[](2);
441-
records[0] = _constantRecord(0.5e18);
442-
records[1] = _constantRecord(0.5e18);
425+
WeightRecordUpdate[] memory updates = new WeightRecordUpdate[](2);
426+
updates[0] = WeightRecordUpdate({id: SERVICE_ID, record: _constantRecord(0.5e18)});
427+
updates[1] = WeightRecordUpdate({id: SERVICE_ID, record: _constantRecord(0.5e18)});
443428

444-
uint32 exitCode = swaCaller.setWeightRecords(ids, records);
429+
uint32 exitCode = swaCaller.setWeightRecords(updates);
445430
assertEq(exitCode, USR_ILLEGAL_ARGUMENT, "a repeated id queues two PendingKeys for one slot");
446431
}
447432

test/mocks/FVMRewardWire.t.sol

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pragma solidity ^0.8.36;
33

44
import {MockRewardTest} from "./MockRewardTest.sol";
5-
import {WeightRecord, Share, PendingOp} from "../../src/lib/FVMRewardTypes.sol";
5+
import {WeightRecord, WeightRecordUpdate, Share, PendingOp} from "../../src/lib/FVMRewardTypes.sol";
66
import {FVMRewards} from "../../src/lib/FVMRewards.sol";
77

88
/// @dev Tests to match with Rust `fil_actor_reward` tests/types_test.rs `mod serialization`, whose
@@ -48,14 +48,15 @@ contract FVMRewardWireTest is MockRewardTest {
4848
// ]]
4949
function test_SetWeightRecords_WalksEveryIntegerWidth() public {
5050
uint64[8] memory v = [uint64(23), 24, 255, 256, 65_535, 65_536, 4_294_967_295, 4_294_967_296];
51-
uint64[] memory ids = new uint64[](8);
52-
WeightRecord[] memory records = new WeightRecord[](8);
51+
WeightRecordUpdate[] memory updates = new WeightRecordUpdate[](8);
5352
for (uint256 i = 0; i < 8; i++) {
54-
ids[i] = v[i];
55-
records[i] = _record(int256(uint256(v[i])), -int256(uint256(v[i])) - 1, v[i], 0, int256(uint256(v[i])));
53+
updates[i] = WeightRecordUpdate({
54+
id: v[i],
55+
record: _record(int256(uint256(v[i])), -int256(uint256(v[i])) - 1, v[i], 0, int256(uint256(v[i])))
56+
});
5657
}
5758

58-
FVMRewards.trySetWeightRecords(ids, records);
59+
FVMRewards.trySetWeightRecords(updates);
5960
assertEq(
6061
_sent(),
6162
hex"81888217851737170017821818851818381818180018188218ff8518ff38ff18ff0018ff"
@@ -69,28 +70,25 @@ contract FVMRewardWireTest is MockRewardTest {
6970
/// @dev Identical params, different dispatch. The two are separate calls because f02 will not
7071
/// let the discretionary path cancel what the gate produced.
7172
function test_StepWeightRecords_EncodesLikeSetWeightRecords() public {
72-
uint64[] memory ids = new uint64[](1);
73-
WeightRecord[] memory records = new WeightRecord[](1);
74-
ids[0] = 23;
75-
records[0] = _record(23, -24, 23, 0, 23);
73+
WeightRecordUpdate[] memory updates = new WeightRecordUpdate[](1);
74+
updates[0] = WeightRecordUpdate({id: 23, record: _record(23, -24, 23, 0, 23)});
7675

77-
FVMRewards.trySetWeightRecords(ids, records);
76+
FVMRewards.trySetWeightRecords(updates);
7877
bytes memory set = _sent();
79-
FVMRewards.tryStepWeightRecords(ids, records);
78+
FVMRewards.tryStepWeightRecords(updates);
8079
assertEq(_sent(), set);
8180
}
8281

8382
// [[]] and one entry at the widest form. Step encodes identically to Set but is pinned
8483
// separately, so this table stands alone as a description of the method.
8584
function test_StepWeightRecords_EmptyAndBoundaryBatch() public {
86-
FVMRewards.tryStepWeightRecords(new uint64[](0), new WeightRecord[](0));
85+
FVMRewards.tryStepWeightRecords(new WeightRecordUpdate[](0));
8786
assertEq(_sent(), hex"8180");
8887

89-
uint64[] memory ids = new uint64[](1);
90-
WeightRecord[] memory records = new WeightRecord[](1);
91-
ids[0] = 4_294_967_296;
92-
records[0] = _record(4_294_967_296, -4_294_967_297, 65_536, 256, 1e18);
93-
FVMRewards.tryStepWeightRecords(ids, records);
88+
WeightRecordUpdate[] memory updates = new WeightRecordUpdate[](1);
89+
updates[0] =
90+
WeightRecordUpdate({id: 4_294_967_296, record: _record(4_294_967_296, -4_294_967_297, 65_536, 256, 1e18)});
91+
FVMRewards.tryStepWeightRecords(updates);
9492
assertEq(
9593
_sent(),
9694
hex"8181821b0000000100000000851b00000001000000003b0000000100000000" hex"1a000100001901001b0de0b6b3a7640000"

0 commit comments

Comments
 (0)