Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ The Filecoin Beam (FilBeamOperator) contract is responsible for managing CDN (ca
- Skips datasets without valid rail configuration
- Continues processing even if some datasets cannot be settled
- **Partial Settlement**: Supports partial settlements when `accumulated_amount > lockupFixed`
- **Events**: Emits `CDNSettlement` event with actual settled amount (may be less than accumulated)
- **Events**: Emits `CDNSettlement` event with settled amount and remaining lockup after settlement
- Event is emitted whenever a valid rail is configured, even if `settledAmount` is 0
- No event is emitted for uninitialized datasets or when no rail is configured
- **Independent Operation**: Can be called independently of cache-miss settlement

**Method**: `settleCacheMissPaymentRails(uint256[] dataSetIds)`
Expand All @@ -106,7 +108,9 @@ The Filecoin Beam (FilBeamOperator) contract is responsible for managing CDN (ca
- Skips datasets without valid rail configuration
- Continues processing even if some datasets cannot be settled
- **Partial Settlement**: Supports partial settlements when `accumulated_amount > lockupFixed`
- **Events**: Emits `CacheMissSettlement` event with actual settled amount (may be less than accumulated)
- **Events**: Emits `CacheMissSettlement` event with settled amount and remaining lockup after settlement
- Event is emitted whenever a valid rail is configured, even if `settledAmount` is 0
- No event is emitted for uninitialized datasets or when no rail is configured
- **Independent Operation**: Can be called independently of CDN settlement

#### Payment Rail Termination
Expand Down Expand Up @@ -160,8 +164,8 @@ The Filecoin Beam (FilBeamOperator) contract is responsible for managing CDN (ca

#### Events
- `UsageReported(uint256 indexed dataSetId, uint256 indexed fromEpoch, uint256 indexed toEpoch, uint256 cdnBytesUsed, uint256 cacheMissBytesUsed)`
- `CDNSettlement(uint256 indexed dataSetId, uint256 fromEpoch, uint256 toEpoch, uint256 cdnAmount)`
- `CacheMissSettlement(uint256 indexed dataSetId, uint256 fromEpoch, uint256 toEpoch, uint256 cacheMissAmount)`
- `CDNSettlement(uint256 indexed dataSetId, uint256 settledAmount, uint256 remainingLockup)` - Emitted after CDN settlement attempt. `settledAmount` is the amount transferred (may be 0 if no usage or zero lockup). `remainingLockup` is the lockup available after settlement.
- `CacheMissSettlement(uint256 indexed dataSetId, uint256 settledAmount, uint256 remainingLockup)` - Emitted after cache-miss settlement attempt. `settledAmount` is the amount transferred (may be 0 if no usage or zero lockup). `remainingLockup` is the lockup available after settlement.
- `PaymentRailsTerminated(uint256 indexed dataSetId)`
- `FilBeamOperatorControllerUpdated(address indexed oldController, address indexed newController)`
- `FwssFilBeamControllerMigrated(address indexed previousController, address indexed newController)`
Expand Down
58 changes: 28 additions & 30 deletions src/FilBeamOperator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ contract FilBeamOperator is Ownable2Step {
uint256 cacheMissBytesUsed
);

event CDNSettlement(uint256 indexed dataSetId, uint256 cdnAmount);
event CDNSettlement(uint256 indexed dataSetId, uint256 settledAmount, uint256 remainingLockup);

event CacheMissSettlement(uint256 indexed dataSetId, uint256 cacheMissAmount);
event CacheMissSettlement(uint256 indexed dataSetId, uint256 settledAmount, uint256 remainingLockup);

event PaymentRailsTerminated(uint256 indexed dataSetId);

Expand Down Expand Up @@ -182,11 +182,8 @@ contract FilBeamOperator is Ownable2Step {
function _settlePaymentRail(uint256 dataSetId, bool isCDN) internal {
DataSetUsage storage usage = dataSetUsage[dataSetId];

// Get the appropriate amount based on rail type
uint256 amount = isCDN ? usage.cdnAmount : usage.cacheMissAmount;

// Early return if data set not initialized or no usage to settle
if (usage.maxReportedEpoch == 0 || amount == 0) {
// Early return if data set not initialized (no event)
if (usage.maxReportedEpoch == 0) {
return;
}

Expand All @@ -195,38 +192,39 @@ contract FilBeamOperator is Ownable2Step {
FilecoinWarmStorageServiceStateView(fwssStateViewContractAddress).getDataSet(dataSetId);
uint256 railId = isCDN ? dsInfo.cdnRailId : dsInfo.cacheMissRailId;

// Early return if no rail configured
// Early return if no rail configured (no event)
if (railId == 0) {
return;
}

// Get the actual amount we can settle based on rail lockup
uint256 amountToSettle = _getSettleableAmount(railId, amount);
// Get the appropriate amount based on rail type
uint256 amount = isCDN ? usage.cdnAmount : usage.cacheMissAmount;

// Early return if nothing can be settled (no lockup available)
if (amountToSettle == 0) {
return;
// Get rail info for lockup amount
FilecoinPayV1.RailView memory rail = FilecoinPayV1(paymentsContractAddress).getRail(railId);

// Calculate the amount we can settle (limited by lockup)
uint256 amountToSettle = amount > rail.lockupFixed ? rail.lockupFixed : amount;

// Calculate remaining lockup after settlement
uint256 remainingLockup = rail.lockupFixed - amountToSettle;

// Settle through FWSS only if there's something to settle
if (amountToSettle > 0) {
if (isCDN) {
FilecoinWarmStorageService(fwssContractAddress).settleFilBeamPaymentRails(dataSetId, amountToSettle, 0);
usage.cdnAmount -= amountToSettle;
} else {
FilecoinWarmStorageService(fwssContractAddress).settleFilBeamPaymentRails(dataSetId, 0, amountToSettle);
usage.cacheMissAmount -= amountToSettle;
}
}

// Settle the amount through FWSS
// Always emit event (even if settledAmount is 0)
if (isCDN) {
FilecoinWarmStorageService(fwssContractAddress).settleFilBeamPaymentRails(dataSetId, amountToSettle, 0);
usage.cdnAmount -= amountToSettle;
emit CDNSettlement(dataSetId, amountToSettle);
emit CDNSettlement(dataSetId, amountToSettle, remainingLockup);
} else {
FilecoinWarmStorageService(fwssContractAddress).settleFilBeamPaymentRails(dataSetId, 0, amountToSettle);
usage.cacheMissAmount -= amountToSettle;
emit CacheMissSettlement(dataSetId, amountToSettle);
emit CacheMissSettlement(dataSetId, amountToSettle, remainingLockup);
}
}

/// @dev Internal helper to get the settleable amount based on rail lockup
/// @param railId The payment rail ID
/// @param requestedAmount The amount requested to settle
/// @return The amount that can be settled (limited by lockupFixed)
function _getSettleableAmount(uint256 railId, uint256 requestedAmount) internal view returns (uint256) {
FilecoinPayV1.RailView memory rail = FilecoinPayV1(paymentsContractAddress).getRail(railId);
// Return the minimum of requested amount and available lockup
return requestedAmount > rail.lockupFixed ? rail.lockupFixed : requestedAmount;
}
}
5 changes: 4 additions & 1 deletion src/mocks/MockFWSS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ contract MockFWSS {
{
settlements.push(
Settlement({
dataSetId: dataSetId, cdnAmount: cdnAmount, cacheMissAmount: cacheMissAmount, timestamp: block.timestamp
dataSetId: dataSetId,
cdnAmount: cdnAmount,
cacheMissAmount: cacheMissAmount,
timestamp: block.timestamp
})
);

Expand Down
5 changes: 4 additions & 1 deletion test/DeployFilBeamDecimalPricing.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ contract DeployFilBeamDecimalPricingTest is Test {
}
}

function testFuzz_calculateUsdfcPerByte(uint128 scaledPrice, uint8 priceDecimals, uint8 tokenDecimals) public view {
function testFuzz_calculateUsdfcPerByte(uint128 scaledPrice, uint8 priceDecimals, uint8 tokenDecimals)
public
view
{
// Bound inputs to reasonable ranges
scaledPrice = uint128(bound(scaledPrice, 1, type(uint64).max)); // Reasonable price range
priceDecimals = uint8(bound(priceDecimals, 0, 12)); // Limit to reasonable decimal range
Expand Down
200 changes: 191 additions & 9 deletions test/FilBeamOperator.t.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import {Test, Vm} from "forge-std/Test.sol";
import {FilBeamOperator} from "../src/FilBeamOperator.sol";
import {FilecoinWarmStorageService} from "@filecoin-services/FilecoinWarmStorageService.sol";
import {MockFWSS} from "../src/mocks/MockFWSS.sol";
Expand Down Expand Up @@ -33,9 +33,9 @@ contract FilBeamOperatorTest is Test {
uint256 cacheMissBytesUsed
);

event CDNSettlement(uint256 indexed dataSetId, uint256 cdnAmount);
event CDNSettlement(uint256 indexed dataSetId, uint256 settledAmount, uint256 remainingLockup);

event CacheMissSettlement(uint256 indexed dataSetId, uint256 cacheMissAmount);
event CacheMissSettlement(uint256 indexed dataSetId, uint256 settledAmount, uint256 remainingLockup);

event FwssFilBeamControllerChanged(address indexed previousController, address indexed newController);

Expand Down Expand Up @@ -345,7 +345,7 @@ contract FilBeamOperatorTest is Test {
vm.stopPrank();

vm.expectEmit(true, false, false, true);
emit CDNSettlement(DATA_SET_ID_1, 300000);
emit CDNSettlement(DATA_SET_ID_1, 300000, 700000);

vm.prank(user1);
filBeam.settleCDNPaymentRails(_singleUint256Array(DATA_SET_ID_1));
Expand Down Expand Up @@ -374,7 +374,7 @@ contract FilBeamOperatorTest is Test {
vm.stopPrank();

vm.expectEmit(true, false, false, true);
emit CacheMissSettlement(DATA_SET_ID_1, 300000);
emit CacheMissSettlement(DATA_SET_ID_1, 300000, 700000);

vm.prank(user1);
filBeam.settleCacheMissPaymentRails(_singleUint256Array(DATA_SET_ID_1));
Expand Down Expand Up @@ -813,9 +813,9 @@ contract FilBeamOperatorTest is Test {
dataSetIds[1] = DATA_SET_ID_2;

vm.expectEmit(true, false, false, true);
emit CDNSettlement(DATA_SET_ID_1, 300000);
emit CDNSettlement(DATA_SET_ID_1, 300000, 700000);
vm.expectEmit(true, false, false, true);
emit CDNSettlement(DATA_SET_ID_2, 150000);
emit CDNSettlement(DATA_SET_ID_2, 150000, 850000);

vm.prank(user1);
filBeam.settleCDNPaymentRails(dataSetIds);
Expand Down Expand Up @@ -862,9 +862,9 @@ contract FilBeamOperatorTest is Test {
dataSetIds[1] = DATA_SET_ID_2;

vm.expectEmit(true, false, false, true);
emit CacheMissSettlement(DATA_SET_ID_1, 300000);
emit CacheMissSettlement(DATA_SET_ID_1, 300000, 700000);
vm.expectEmit(true, false, false, true);
emit CacheMissSettlement(DATA_SET_ID_2, 150000);
emit CacheMissSettlement(DATA_SET_ID_2, 150000, 850000);

vm.prank(user1);
filBeam.settleCacheMissPaymentRails(dataSetIds);
Expand Down Expand Up @@ -1486,4 +1486,186 @@ contract FilBeamOperatorTest is Test {
vm.expectRevert(MockFWSS.UnauthorizedCaller.selector);
filBeam.settleCDNPaymentRails(_singleUint256Array(DATA_SET_ID_1));
}

// ============ Settlement Event Tests ============

function test_SettlementEmitsEventWhenNoUsage() public {
// Record and settle all usage first
vm.prank(filBeamOperatorController);
filBeam.recordUsageRollups(
1, _singleUint256Array(DATA_SET_ID_1), _singleUint256Array(1000), _singleUint256Array(500)
);

// Settle CDN
filBeam.settleCDNPaymentRails(_singleUint256Array(DATA_SET_ID_1));
assertEq(mockFWSS.getSettlementsCount(), 1);

// Second settlement should emit event with settledAmount=0 and remainingLockup=lockupFixed
vm.expectEmit(true, false, false, true);
emit CDNSettlement(DATA_SET_ID_1, 0, 1000000);

filBeam.settleCDNPaymentRails(_singleUint256Array(DATA_SET_ID_1));

// No new FWSS settlement should occur
assertEq(mockFWSS.getSettlementsCount(), 1);

// Same for cache miss
filBeam.settleCacheMissPaymentRails(_singleUint256Array(DATA_SET_ID_1));
assertEq(mockFWSS.getSettlementsCount(), 2);

vm.expectEmit(true, false, false, true);
emit CacheMissSettlement(DATA_SET_ID_1, 0, 1000000);

filBeam.settleCacheMissPaymentRails(_singleUint256Array(DATA_SET_ID_1));
assertEq(mockFWSS.getSettlementsCount(), 2);
}

function test_SettlementEmitsEventWhenZeroLockup() public {
// Set lockup to 0
mockPayments.setLockupFixed(1, 0);
mockPayments.setLockupFixed(2, 0);

// Record usage
vm.prank(filBeamOperatorController);
filBeam.recordUsageRollups(
1, _singleUint256Array(DATA_SET_ID_1), _singleUint256Array(1000), _singleUint256Array(500)
);

// Settlement should emit event with settledAmount=0, remainingLockup=0
vm.expectEmit(true, false, false, true);
emit CDNSettlement(DATA_SET_ID_1, 0, 0);

filBeam.settleCDNPaymentRails(_singleUint256Array(DATA_SET_ID_1));

// No FWSS settlement should occur (zero lockup)
assertEq(mockFWSS.getSettlementsCount(), 0);

// Amount should still be accumulated
(uint256 cdnAmount,,) = filBeam.dataSetUsage(DATA_SET_ID_1);
assertEq(cdnAmount, 100000);

// Same for cache miss
vm.expectEmit(true, false, false, true);
emit CacheMissSettlement(DATA_SET_ID_1, 0, 0);

filBeam.settleCacheMissPaymentRails(_singleUint256Array(DATA_SET_ID_1));
assertEq(mockFWSS.getSettlementsCount(), 0);

(, uint256 cacheMissAmount,) = filBeam.dataSetUsage(DATA_SET_ID_1);
assertEq(cacheMissAmount, 100000);
}

function test_NoEventForUninitializedDataset() public {
// Try to settle an uninitialized dataset - should not emit any event
// We can't easily test for absence of events, but we can verify no state changes
vm.recordLogs();

filBeam.settleCDNPaymentRails(_singleUint256Array(DATA_SET_ID_1));
filBeam.settleCacheMissPaymentRails(_singleUint256Array(DATA_SET_ID_1));

// Get logs and verify no CDNSettlement or CacheMissSettlement events
Vm.Log[] memory logs = vm.getRecordedLogs();
for (uint256 i = 0; i < logs.length; i++) {
// CDNSettlement and CacheMissSettlement event signatures should not appear
bytes32 cdnSettlementTopic = keccak256("CDNSettlement(uint256,uint256,uint256)");
bytes32 cacheMissSettlementTopic = keccak256("CacheMissSettlement(uint256,uint256,uint256)");
assertTrue(
logs[i].topics[0] != cdnSettlementTopic,
"CDNSettlement event should not be emitted for uninitialized dataset"
);
assertTrue(
logs[i].topics[0] != cacheMissSettlementTopic,
"CacheMissSettlement event should not be emitted for uninitialized dataset"
);
}

// No settlements should occur
assertEq(mockFWSS.getSettlementsCount(), 0);
}

function test_NoEventForNoRailConfigured() public {
// Set up a dataset with no rails
FilecoinWarmStorageService.DataSetInfoView memory dsInfo = FilecoinWarmStorageService.DataSetInfoView({
pdpRailId: 0,
cacheMissRailId: 0,
cdnRailId: 0,
payer: user1,
payee: user2,
serviceProvider: address(0),
commissionBps: 0,
clientDataSetId: 0,
pdpEndEpoch: 0,
providerId: 0,
dataSetId: 3
});
uint256 dataSetId3 = 3;
mockStateView.setDataSetInfo(dataSetId3, dsInfo);

// Record usage (this initializes the dataset)
vm.prank(filBeamOperatorController);
filBeam.recordUsageRollups(
1, _singleUint256Array(dataSetId3), _singleUint256Array(1000), _singleUint256Array(500)
);

// Try to settle - should not emit events since no rail is configured
vm.recordLogs();

filBeam.settleCDNPaymentRails(_singleUint256Array(dataSetId3));
filBeam.settleCacheMissPaymentRails(_singleUint256Array(dataSetId3));

// Get logs and verify no settlement events
Vm.Log[] memory logs = vm.getRecordedLogs();
for (uint256 i = 0; i < logs.length; i++) {
bytes32 cdnSettlementTopic = keccak256("CDNSettlement(uint256,uint256,uint256)");
bytes32 cacheMissSettlementTopic = keccak256("CacheMissSettlement(uint256,uint256,uint256)");
assertTrue(
logs[i].topics[0] != cdnSettlementTopic,
"CDNSettlement event should not be emitted when no rail configured"
);
assertTrue(
logs[i].topics[0] != cacheMissSettlementTopic,
"CacheMissSettlement event should not be emitted when no rail configured"
);
}

// No settlements should occur
assertEq(mockFWSS.getSettlementsCount(), 0);

// Amount should still be accumulated
(uint256 cdnAmount, uint256 cacheMissAmount,) = filBeam.dataSetUsage(dataSetId3);
assertEq(cdnAmount, 100000);
assertEq(cacheMissAmount, 100000);
}

function test_PartialSettlementWithLimitedLockupEmitsCorrectRemainingLockup() public {
// Set limited lockup for CDN rail
mockPayments.setLockupFixed(1, 50000); // CDN rail has 50k lockup
mockPayments.setLockupFixed(2, 30000); // Cache miss rail has 30k lockup

// Record usage that will exceed the lockup limits
vm.prank(filBeamOperatorController);
filBeam.recordUsageRollups(
1,
_singleUint256Array(DATA_SET_ID_1),
_singleUint256Array(1000), // 100k CDN amount (1000 * 100)
_singleUint256Array(500) // 100k cache miss amount (500 * 200)
);

// First CDN settlement - should settle 50k, remaining lockup = 0 (all lockup used)
vm.expectEmit(true, false, false, true);
emit CDNSettlement(DATA_SET_ID_1, 50000, 0);

filBeam.settleCDNPaymentRails(_singleUint256Array(DATA_SET_ID_1));

// First cache miss settlement - should settle 30k, remaining lockup = 0
vm.expectEmit(true, false, false, true);
emit CacheMissSettlement(DATA_SET_ID_1, 30000, 0);

filBeam.settleCacheMissPaymentRails(_singleUint256Array(DATA_SET_ID_1));

// Verify partial settlements
(uint256 cdnAmount, uint256 cacheMissAmount,) = filBeam.dataSetUsage(DATA_SET_ID_1);
assertEq(cdnAmount, 50000, "Should have 50k CDN remaining");
assertEq(cacheMissAmount, 70000, "Should have 70k cache miss remaining");
}
}
Loading