|
2 | 2 | pragma solidity 0.8.27;
|
3 | 3 |
|
4 | 4 | import { IRecurringCollector } from "../../../../contracts/interfaces/IRecurringCollector.sol";
|
| 5 | +import { IGraphPayments } from "../../../../contracts/interfaces/IGraphPayments.sol"; |
5 | 6 | import { IHorizonStakingTypes } from "../../../../contracts/interfaces/internal/IHorizonStakingTypes.sol";
|
6 | 7 |
|
7 | 8 | import { RecurringCollectorSharedTest } from "./shared.t.sol";
|
@@ -309,5 +310,138 @@ contract RecurringCollectorCollectTest is RecurringCollectorSharedTest {
|
309 | 310 | uint256 collected = _recurringCollector.collect(_paymentType(fuzzy.unboundedPaymentType), data);
|
310 | 311 | assertEq(collected, tokens);
|
311 | 312 | }
|
| 313 | + |
| 314 | + function test_Collect_RevertWhen_ExceedsMaxSlippage() public { |
| 315 | + // Setup: Create agreement with known parameters |
| 316 | + IRecurringCollector.RecurringCollectionAgreement memory rca; |
| 317 | + rca.deadline = uint64(block.timestamp + 1000); |
| 318 | + rca.endsAt = uint64(block.timestamp + 2000); |
| 319 | + rca.payer = address(0x123); |
| 320 | + rca.dataService = address(0x456); |
| 321 | + rca.serviceProvider = address(0x789); |
| 322 | + rca.maxInitialTokens = 0; // No initial tokens to keep calculation simple |
| 323 | + rca.maxOngoingTokensPerSecond = 1 ether; // 1 token per second |
| 324 | + rca.minSecondsPerCollection = 60; // 1 minute |
| 325 | + rca.maxSecondsPerCollection = 3600; // 1 hour |
| 326 | + rca.nonce = 1; |
| 327 | + rca.metadata = ""; |
| 328 | + |
| 329 | + // Accept the agreement |
| 330 | + _recurringCollectorHelper.authorizeSignerWithChecks(rca.payer, 1); |
| 331 | + IRecurringCollector.SignedRCA memory signedRCA = _recurringCollectorHelper.generateSignedRCA(rca, 1); |
| 332 | + bytes16 agreementId = _accept(signedRCA); |
| 333 | + |
| 334 | + // Do a first collection to use up initial tokens allowance |
| 335 | + skip(rca.minSecondsPerCollection); |
| 336 | + IRecurringCollector.CollectParams memory firstCollection = IRecurringCollector.CollectParams({ |
| 337 | + agreementId: agreementId, |
| 338 | + collectionId: keccak256("first"), |
| 339 | + tokens: 1 ether, // Small amount |
| 340 | + dataServiceCut: 0, |
| 341 | + receiverDestination: rca.serviceProvider, |
| 342 | + maxSlippage: type(uint256).max |
| 343 | + }); |
| 344 | + vm.prank(rca.dataService); |
| 345 | + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, _generateCollectData(firstCollection)); |
| 346 | + |
| 347 | + // Wait minimum collection time again for second collection |
| 348 | + skip(rca.minSecondsPerCollection); |
| 349 | + |
| 350 | + // Calculate expected narrowing: max allowed is 60 tokens (60 seconds * 1 token/second) |
| 351 | + uint256 maxAllowed = rca.maxOngoingTokensPerSecond * rca.minSecondsPerCollection; // 60 tokens |
| 352 | + uint256 requested = maxAllowed + 50 ether; // Request 110 tokens |
| 353 | + uint256 expectedSlippage = requested - maxAllowed; // 50 tokens |
| 354 | + uint256 maxSlippage = expectedSlippage - 1; // Allow up to 49 tokens slippage |
| 355 | + |
| 356 | + // Create collect params with slippage protection |
| 357 | + IRecurringCollector.CollectParams memory collectParams = IRecurringCollector.CollectParams({ |
| 358 | + agreementId: agreementId, |
| 359 | + collectionId: keccak256("test"), |
| 360 | + tokens: requested, |
| 361 | + dataServiceCut: 0, |
| 362 | + receiverDestination: rca.serviceProvider, |
| 363 | + maxSlippage: maxSlippage |
| 364 | + }); |
| 365 | + |
| 366 | + bytes memory data = _generateCollectData(collectParams); |
| 367 | + |
| 368 | + // Expect revert due to excessive slippage (50 > 49) |
| 369 | + vm.expectRevert( |
| 370 | + abi.encodeWithSelector( |
| 371 | + IRecurringCollector.RecurringCollectorExcessiveSlippage.selector, |
| 372 | + requested, |
| 373 | + maxAllowed, |
| 374 | + maxSlippage |
| 375 | + ) |
| 376 | + ); |
| 377 | + vm.prank(rca.dataService); |
| 378 | + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); |
| 379 | + } |
| 380 | + |
| 381 | + function test_Collect_OK_WithMaxSlippageDisabled() public { |
| 382 | + // Setup: Create agreement with known parameters |
| 383 | + IRecurringCollector.RecurringCollectionAgreement memory rca; |
| 384 | + rca.deadline = uint64(block.timestamp + 1000); |
| 385 | + rca.endsAt = uint64(block.timestamp + 2000); |
| 386 | + rca.payer = address(0x123); |
| 387 | + rca.dataService = address(0x456); |
| 388 | + rca.serviceProvider = address(0x789); |
| 389 | + rca.maxInitialTokens = 0; // No initial tokens to keep calculation simple |
| 390 | + rca.maxOngoingTokensPerSecond = 1 ether; // 1 token per second |
| 391 | + rca.minSecondsPerCollection = 60; // 1 minute |
| 392 | + rca.maxSecondsPerCollection = 3600; // 1 hour |
| 393 | + rca.nonce = 1; |
| 394 | + rca.metadata = ""; |
| 395 | + |
| 396 | + // Accept the agreement |
| 397 | + _recurringCollectorHelper.authorizeSignerWithChecks(rca.payer, 1); |
| 398 | + IRecurringCollector.SignedRCA memory signedRCA = _recurringCollectorHelper.generateSignedRCA(rca, 1); |
| 399 | + bytes16 agreementId = _accept(signedRCA); |
| 400 | + |
| 401 | + // Do a first collection to use up initial tokens allowance |
| 402 | + skip(rca.minSecondsPerCollection); |
| 403 | + IRecurringCollector.CollectParams memory firstCollection = IRecurringCollector.CollectParams({ |
| 404 | + agreementId: agreementId, |
| 405 | + collectionId: keccak256("first"), |
| 406 | + tokens: 1 ether, // Small amount |
| 407 | + dataServiceCut: 0, |
| 408 | + receiverDestination: rca.serviceProvider, |
| 409 | + maxSlippage: type(uint256).max |
| 410 | + }); |
| 411 | + vm.prank(rca.dataService); |
| 412 | + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, _generateCollectData(firstCollection)); |
| 413 | + |
| 414 | + // Wait minimum collection time again for second collection |
| 415 | + skip(rca.minSecondsPerCollection); |
| 416 | + |
| 417 | + // Calculate expected narrowing: max allowed is 60 tokens (60 seconds * 1 token/second) |
| 418 | + uint256 maxAllowed = rca.maxOngoingTokensPerSecond * rca.minSecondsPerCollection; // 60 tokens |
| 419 | + uint256 requested = maxAllowed + 50 ether; // Request 110 tokens (will be narrowed to 60) |
| 420 | + |
| 421 | + // Create collect params with slippage disabled (type(uint256).max) |
| 422 | + IRecurringCollector.CollectParams memory collectParams = IRecurringCollector.CollectParams({ |
| 423 | + agreementId: agreementId, |
| 424 | + collectionId: keccak256("test"), |
| 425 | + tokens: requested, |
| 426 | + dataServiceCut: 0, |
| 427 | + receiverDestination: rca.serviceProvider, |
| 428 | + maxSlippage: type(uint256).max |
| 429 | + }); |
| 430 | + |
| 431 | + bytes memory data = _generateCollectData(collectParams); |
| 432 | + |
| 433 | + // Should succeed despite slippage when maxSlippage is disabled |
| 434 | + _expectCollectCallAndEmit( |
| 435 | + rca, |
| 436 | + agreementId, |
| 437 | + IGraphPayments.PaymentTypes.IndexingFee, |
| 438 | + collectParams, |
| 439 | + maxAllowed // Will collect the narrowed amount |
| 440 | + ); |
| 441 | + |
| 442 | + vm.prank(rca.dataService); |
| 443 | + uint256 collected = _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); |
| 444 | + assertEq(collected, maxAllowed); |
| 445 | + } |
312 | 446 | /* solhint-enable graph/func-name-mixedcase */
|
313 | 447 | }
|
0 commit comments