Skip to content
Merged
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 3 additions & 41 deletions src/core/PaymentsEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { Errors } from "../libraries/Errors.sol";
import { PaymentLib } from "../libraries/PaymentLib.sol";

/// @title PaymentsEscrow
/// @notice Customer-facing escrow funding and post-termination withdrawal.
/// @notice Customer-facing escrow funding for subscription services.
/// @dev Post-termination withdrawal lives in `PaymentsRefund` and is routed via
/// `TanglePaymentsRewardsFacet` to keep this facet under EIP-170.
abstract contract PaymentsEscrow is PaymentsCore {
using PaymentLib for PaymentLib.ServiceEscrow;

Expand Down Expand Up @@ -41,44 +43,4 @@ abstract contract PaymentsEscrow is PaymentsCore {
emit EscrowFunded(serviceId, token, amount);
_recordPayment(msg.sender, serviceId, token, amount);
}

/// @notice Withdraw remaining escrow balance after service termination.
/// @dev Equivalent to `withdrawRemainingEscrowTo(serviceId, svc.owner)` — the
/// service owner remains the default recipient. Use the `To` variant
/// when the service owner has been blocklisted by the escrow token or
/// otherwise cannot receive on the owner address.
function withdrawRemainingEscrow(uint64 serviceId) external nonReentrant {
_withdrawRemainingEscrow(serviceId, payable(_getService(serviceId).owner));
}

/// @notice Withdraw remaining escrow balance to a service-owner-chosen address.
/// @dev Caller must be the service owner. The recipient is arbitrary, so a
/// service owner blocklisted by the escrow token (or operating from a
/// smart-account that cannot receive directly) can still recover funds
/// by routing to a fresh address.
function withdrawRemainingEscrowTo(uint64 serviceId, address payable to) external nonReentrant {
if (to == address(0)) revert Errors.ZeroAddress();
_withdrawRemainingEscrow(serviceId, to);
}

function _withdrawRemainingEscrow(uint64 serviceId, address payable to) private {
Types.Service storage svc = _getService(serviceId);
if (svc.owner != msg.sender) {
revert Errors.NotServiceOwner(serviceId, msg.sender);
}
if (svc.status != Types.ServiceStatus.Terminated) {
revert Errors.ServiceNotTerminated(serviceId);
}

PaymentLib.ServiceEscrow storage escrow = _serviceEscrows[serviceId];
uint256 remaining = escrow.balance;
if (remaining == 0) revert Errors.ZeroAmount();

address token = escrow.token;
escrow.balance = 0;
escrow.totalReleased += remaining;

PaymentLib.transferPayment(to, token, remaining);
emit EscrowRefunded(serviceId, to, token, remaining);
}
}
55 changes: 55 additions & 0 deletions src/core/PaymentsRefund.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import { PaymentsCore } from "./PaymentsCore.sol";
import { Types } from "../libraries/Types.sol";
import { Errors } from "../libraries/Errors.sol";
import { PaymentLib } from "../libraries/PaymentLib.sol";

/// @title PaymentsRefund
/// @notice Post-termination escrow refund path.
/// @dev Extracted from `PaymentsEscrow` so the customer-facing funding + billing
/// facet stays under the EIP-170 24576-byte runtime limit. Withdrawal is a
/// rare-path operation conceptually closer to the rewards/admin surface and
/// therefore lives on `TanglePaymentsRewardsFacet`.
abstract contract PaymentsRefund is PaymentsCore {
/// @notice Withdraw remaining escrow balance after service termination.
/// @dev Equivalent to `withdrawRemainingEscrowTo(serviceId, svc.owner)` — the
/// service owner remains the default recipient. Use the `To` variant
/// when the service owner has been blocklisted by the escrow token or
/// otherwise cannot receive on the owner address.
function withdrawRemainingEscrow(uint64 serviceId) external nonReentrant {
_withdrawRemainingEscrow(serviceId, payable(_getService(serviceId).owner));
}

/// @notice Withdraw remaining escrow balance to a service-owner-chosen address.
/// @dev Caller must be the service owner. The recipient is arbitrary, so a
/// service owner blocklisted by the escrow token (or operating from a
/// smart-account that cannot receive directly) can still recover funds
/// by routing to a fresh address.
function withdrawRemainingEscrowTo(uint64 serviceId, address payable to) external nonReentrant {
if (to == address(0)) revert Errors.ZeroAddress();
_withdrawRemainingEscrow(serviceId, to);
}

function _withdrawRemainingEscrow(uint64 serviceId, address payable to) private {
Types.Service storage svc = _getService(serviceId);
if (svc.owner != msg.sender) {
revert Errors.NotServiceOwner(serviceId, msg.sender);
}
if (svc.status != Types.ServiceStatus.Terminated) {
revert Errors.ServiceNotTerminated(serviceId);
}

PaymentLib.ServiceEscrow storage escrow = _serviceEscrows[serviceId];
uint256 remaining = escrow.balance;
if (remaining == 0) revert Errors.ZeroAmount();

address token = escrow.token;
escrow.balance = 0;
escrow.totalReleased += remaining;

PaymentLib.transferPayment(to, token, remaining);
emit EscrowRefunded(serviceId, to, token, remaining);
}
}
13 changes: 6 additions & 7 deletions src/facets/tangle/TanglePaymentsFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,19 @@ import { PaymentsBilling } from "../../core/PaymentsBilling.sol";
import { IFacetSelectors } from "../../interfaces/IFacetSelectors.sol";

/// @title TanglePaymentsFacet
/// @notice Customer-facing escrow funding/withdrawal + subscription billing entry points.
/// @notice Customer-facing escrow funding + subscription billing entry points.
/// @dev Distribution self-call selectors (`distributePayment`, `depositToEscrow`,
/// `initSubscriptionBaseline`, `distributeBillWithKeeper`) live on
/// `TanglePaymentsDistributionFacet`. Rewards claims + admin/views live on
/// `TanglePaymentsDistributionFacet`. Rewards claims + admin/views + post-
/// termination escrow refund (`withdrawRemainingEscrow{,To}`) live on
/// `TanglePaymentsRewardsFacet`. Subscription billing reaches the distribution
/// path via a diamond self-call so the heavy distribution machinery does not
/// contribute bytecode to this facet.
contract TanglePaymentsFacet is PaymentsEscrow, PaymentsBilling, IFacetSelectors {
function selectors() external pure returns (bytes4[] memory selectorList) {
selectorList = new bytes4[](5);
selectorList = new bytes4[](3);
selectorList[0] = this.fundService.selector;
selectorList[1] = this.withdrawRemainingEscrow.selector;
selectorList[2] = this.withdrawRemainingEscrowTo.selector;
selectorList[3] = this.billSubscription.selector;
selectorList[4] = this.billSubscriptionBatch.selector;
selectorList[1] = this.billSubscription.selector;
selectorList[2] = this.billSubscriptionBatch.selector;
}
}
12 changes: 9 additions & 3 deletions src/facets/tangle/TanglePaymentsRewardsFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
pragma solidity ^0.8.26;

import { PaymentsRewards } from "../../core/PaymentsRewards.sol";
import { PaymentsRefund } from "../../core/PaymentsRefund.sol";
import { IFacetSelectors } from "../../interfaces/IFacetSelectors.sol";

/// @title TanglePaymentsRewardsFacet
/// @notice Rewards claim, payment-split / treasury admin, escrow view.
/// @notice Rewards claim, payment-split / treasury admin, escrow view, post-
/// termination escrow refund.
/// @dev Hosted on its own facet so the bytecode footprint stays small. The billing,
/// escrow funding, and distribution selectors live on `TanglePaymentsFacet`.
contract TanglePaymentsRewardsFacet is PaymentsRewards, IFacetSelectors {
/// Refund (`withdrawRemainingEscrow{,To}`) was relocated here from
/// `TanglePaymentsFacet` to keep that facet under EIP-170.
contract TanglePaymentsRewardsFacet is PaymentsRewards, PaymentsRefund, IFacetSelectors {
function selectors() external pure returns (bytes4[] memory selectorList) {
selectorList = new bytes4[](14);
selectorList = new bytes4[](16);
selectorList[0] = bytes4(keccak256("claimRewards()"));
selectorList[1] = bytes4(keccak256("claimRewards(address)"));
selectorList[2] = bytes4(keccak256("claimRewardsBatch(address[])"));
Expand All @@ -25,5 +29,7 @@ contract TanglePaymentsRewardsFacet is PaymentsRewards, IFacetSelectors {
selectorList[11] = this.getServiceEscrow.selector;
selectorList[12] = this.getBillableServices.selector;
selectorList[13] = this._claimRewardsTokenSafe.selector;
selectorList[14] = this.withdrawRemainingEscrow.selector;
selectorList[15] = this.withdrawRemainingEscrowTo.selector;
}
}
Loading