@@ -382,4 +382,201 @@ contract MembershipRenewTest is MembershipBaseSetup, IERC5643Base {
382382 assertGt (membership.expiresAt (tokenId), originalExpiration);
383383 assertEq (IERC20 (riverAirdrop).balanceOf (alice), currentPoints + points);
384384 }
385+
386+ function test_renewMembershipAfterPriceDropToFree () external {
387+ // Setup: Create a paid town with initial price
388+ uint256 initialPrice = MEMBERSHIP_PRICE;
389+ uint256 platformMinFee = platformReqs.getMembershipFee ();
390+ _setupMembershipPricing (1 , initialPrice);
391+
392+ // Alice joins at the higher price
393+ vm.deal (alice, initialPrice);
394+ vm.prank (alice);
395+ membership.joinSpace {value: initialPrice}(alice);
396+
397+ uint256 tokenId = _getAliceTokenId ();
398+ uint256 originalExpiration = membership.expiresAt (tokenId);
399+
400+ // Verify Alice paid the initial price
401+ uint256 lockedRenewalPrice = _getRenewalPrice (tokenId);
402+ assertEq (lockedRenewalPrice, initialPrice, "Initial renewal price should be locked " );
403+
404+ // Town transitions to minimum price (effectively free except for protocol fee)
405+ vm.prank (founder);
406+ membership.setMembershipPrice (platformMinFee);
407+
408+ // Verify current price is now just the platform minimum fee
409+ uint256 newMembershipPrice = membership.getMembershipPrice ();
410+ assertEq (newMembershipPrice, platformMinFee, "New price should be platform minimum " );
411+
412+ // Warp to expiration
413+ vm.warp (originalExpiration);
414+
415+ // Get updated renewal price - should be the lower free price, not the locked higher price
416+ uint256 renewalPrice = _getRenewalPrice (tokenId);
417+ assertEq (
418+ renewalPrice,
419+ platformMinFee,
420+ "Renewal price should use lower current price, not locked price "
421+ );
422+ assertLt (renewalPrice, initialPrice, "Renewal price should be less than original " );
423+
424+ // Track balances
425+ (address protocol , uint256 protocolBalanceBefore ) = _getProtocolFeeData ();
426+ uint256 spaceBalanceBefore = address (membership).balance;
427+ uint256 currentPoints = IERC20 (riverAirdrop).balanceOf (alice);
428+
429+ // Alice renews at the new lower price
430+ _renewMembershipWithValue (alice, tokenId, renewalPrice);
431+
432+ // Verify protocol fee (all goes to protocol since price equals min fee)
433+ assertEq (
434+ protocol.balance - protocolBalanceBefore,
435+ platformMinFee,
436+ "Protocol should receive the minimum fee "
437+ );
438+
439+ // Verify space receives nothing (entire amount is protocol fee)
440+ assertEq (address (membership).balance, spaceBalanceBefore, "Space should receive nothing " );
441+
442+ // Verify membership was renewed
443+ assertGt (membership.expiresAt (tokenId), originalExpiration, "Membership should be renewed " );
444+
445+ // Verify points were awarded based on renewal price
446+ uint256 points = _getPoints (renewalPrice);
447+ assertEq (
448+ IERC20 (riverAirdrop).balanceOf (alice),
449+ currentPoints + points,
450+ "Points should be awarded "
451+ );
452+ }
453+
454+ function test_renewMembershipAfterPriceDropToLower () external {
455+ // Setup: Create a paid town with high initial price
456+ uint256 initialPrice = MEMBERSHIP_PRICE;
457+ uint256 lowerPrice = MEMBERSHIP_PRICE / 2 ;
458+ _setupMembershipPricing (1 , initialPrice);
459+
460+ // Alice joins at the higher price
461+ vm.deal (alice, initialPrice);
462+ vm.prank (alice);
463+ membership.joinSpace {value: initialPrice}(alice);
464+
465+ uint256 tokenId = _getAliceTokenId ();
466+ uint256 originalExpiration = membership.expiresAt (tokenId);
467+
468+ // Verify Alice's renewal price is locked at initial price
469+ assertEq (_getRenewalPrice (tokenId), initialPrice, "Initial renewal price should be locked " );
470+
471+ // Town reduces price to lower amount
472+ vm.prank (founder);
473+ membership.setMembershipPrice (lowerPrice);
474+
475+ // Verify new membership price
476+ assertEq (
477+ membership.getMembershipPrice (),
478+ lowerPrice,
479+ "New membership price should be lower "
480+ );
481+
482+ // Warp to expiration
483+ vm.warp (originalExpiration);
484+
485+ // Get renewal price - should be the lower current price
486+ uint256 renewalPrice = _getRenewalPrice (tokenId);
487+ assertEq (renewalPrice, lowerPrice, "Renewal should use lower current price " );
488+ assertLt (renewalPrice, initialPrice, "Renewal price should be less than original " );
489+
490+ // Track balances
491+ (address protocol , uint256 protocolBalanceBefore ) = _getProtocolFeeData ();
492+ uint256 spaceBalanceBefore = address (membership).balance;
493+ uint256 currentPoints = IERC20 (riverAirdrop).balanceOf (alice);
494+
495+ // Alice renews at the lower price
496+ _renewMembershipWithValue (alice, tokenId, renewalPrice);
497+
498+ // Calculate expected fees
499+ uint256 protocolFee = _calculateProtocolFee (renewalPrice);
500+ uint256 points = _getPoints (renewalPrice);
501+
502+ // Verify balances
503+ assertEq (
504+ protocol.balance - protocolBalanceBefore,
505+ protocolFee,
506+ "Protocol should receive correct fee "
507+ );
508+ assertEq (
509+ address (membership).balance,
510+ spaceBalanceBefore + renewalPrice - protocolFee,
511+ "Space should receive net amount "
512+ );
513+
514+ // Verify membership was renewed
515+ assertGt (membership.expiresAt (tokenId), originalExpiration, "Membership should be renewed " );
516+
517+ // Verify points
518+ assertEq (
519+ IERC20 (riverAirdrop).balanceOf (alice),
520+ currentPoints + points,
521+ "Points should be awarded based on renewal price "
522+ );
523+ }
524+
525+ function test_renewMembershipAfterPriceIncrease () external {
526+ // Setup: Create a paid town with low initial price
527+ uint256 initialPrice = MEMBERSHIP_PRICE / 2 ;
528+ uint256 higherPrice = MEMBERSHIP_PRICE;
529+ _setupMembershipPricing (1 , initialPrice);
530+
531+ // Alice joins at the lower price
532+ vm.deal (alice, initialPrice);
533+ vm.prank (alice);
534+ membership.joinSpace {value: initialPrice}(alice);
535+
536+ uint256 tokenId = _getAliceTokenId ();
537+ uint256 originalExpiration = membership.expiresAt (tokenId);
538+
539+ // Verify Alice's renewal price is locked at initial lower price
540+ assertEq (_getRenewalPrice (tokenId), initialPrice, "Initial renewal price should be locked " );
541+
542+ // Town increases price
543+ vm.prank (founder);
544+ membership.setMembershipPrice (higherPrice);
545+
546+ // Verify new membership price is higher
547+ assertEq (
548+ membership.getMembershipPrice (),
549+ higherPrice,
550+ "New membership price should be higher "
551+ );
552+
553+ // Warp to expiration
554+ vm.warp (originalExpiration);
555+
556+ // Get renewal price - should still be the locked lower price
557+ uint256 renewalPrice = _getRenewalPrice (tokenId);
558+ assertEq (
559+ renewalPrice,
560+ initialPrice,
561+ "Renewal should maintain locked lower price despite increase "
562+ );
563+ assertLt (renewalPrice, higherPrice, "Renewal price should be less than new higher price " );
564+
565+ // Track balances
566+ uint256 currentPoints = IERC20 (riverAirdrop).balanceOf (alice);
567+
568+ // Alice renews at her locked-in lower price
569+ _renewMembershipWithValue (alice, tokenId, renewalPrice);
570+
571+ // Verify membership was renewed
572+ assertGt (membership.expiresAt (tokenId), originalExpiration, "Membership should be renewed " );
573+
574+ // Verify points based on the lower renewal price
575+ uint256 points = _getPoints (renewalPrice);
576+ assertEq (
577+ IERC20 (riverAirdrop).balanceOf (alice),
578+ currentPoints + points,
579+ "Points should be awarded based on locked lower price "
580+ );
581+ }
385582}
0 commit comments