Skip to content

feat(cards): SavingsFacility pays out swept shares in cash - #58

Merged
Slokh merged 2 commits into
cardsfrom
pyramid/ddfc1655-dfdb-4ba0-aef7-d35d59ab945d
Nov 12, 2026
Merged

Slokh merged 2 commits into
cardsfrom
pyramid/ddfc1655-dfdb-4ba0-aef7-d35d59ab945d

Conversation

@Slokh

@Slokh Slokh commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator

Adds SavingsFacility.payOut(owner): when a card-records sweep (TIP-1169 escheat with a via facility) pays an account's Earn shares into the facility, anyone can call payOut, which pays the recipient the precompile's payoutOf record names the shares' previewRedeem value in the base asset, once per share, and redeems them as settle does. This lets an unclaimed-property authority receive a dormant account's savings as cash.

  • ICards interface (payoutOf) and a MockCards test harness, like MockReservations.
  • Tests: payout to the recipient, only new sweeps, no recipient.
  • docs/cards/design.md: a Payouts bullet under Spend from savings.

Checks: forge fmt --check, forge build, forge lint --deny warnings src/core, forge build --sizes src, FOUNDRY_PROFILE=ci forge test (669 passed). Used by the tempoxyz/cards dormant account scenario on localnet.

Pyramid thread: https://pyramid.tehq.net/threads/3700e62c-ee77-49a8-9e5f-4692bb1682b3

@tempoxyz-bot

tempoxyz-bot commented Nov 12, 2026 •

Copy link
Copy Markdown

👁️ Cyclops Security Review

✅ Reviewed 4907d05 (latest) · Coverage: partial · ⚠️ 1 open finding · report
⚠️ Not checked: Whether TIP-1169 intends swept shares to have no recovery path when immediate redemption is permanently unavailable (an async-only engine, or a recipient whose asset transfers are blocked by policy). Without that, it is unclear whether inde
⚠️ Not checked: The supplied repository contains an ICards interface and a freely writable test double, not the production TIP-1169 implementation. Its ABI, atomic transfer-to-record relationship, cumulative-counter lifetime, reset behavior, and interactio
ℹ️ Not verifiable from the source alone (needs running code or deployed state): Whether the production PropAMM pool fills redemptions below PropAMMEngine.previewRedeem (oracle price), which would make every payOut revert under that engine. The swept shares would then stay locked, (1 more in the report)

# Severity Finding Introduced Status
1 🟢 Low SavingsFacility.payOut finalizes zero-quote sweeps without paying the recipient 162be00 ✅ Fixed in 4907d05
2 🟢 Low SavingsFacility payOut lets any vault user force float-funded deferred redemption 162be00 ✅ Fixed in 4907d05
3 🟡 Medium SavingsFacility funder can redeem or withdraw swept shares awaiting payOut 162be00 ✅ Fixed in 4907d05
4 ⚪ Needs review SavingsFacility.payOut pays earlier sweeps' shares to the record's latest recipient 162be00 🟠 Open

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread cards/src/core/SavingsFacility.sol Resolved
Comment thread cards/src/core/SavingsFacility.sol Resolved
Comment thread cards/src/core/SavingsFacility.sol Resolved
Comment thread cards/src/core/SavingsFacility.sol Resolved
Comment thread cards/src/core/SavingsFacility.sol Resolved
Comment thread cards/src/core/SavingsFacility.sol Outdated
Comment on lines +119 to +128
(address to, uint128 swept) = CARDS.payoutOf(owner, SHARE, address(this));
if (to == address(0)) revert ZeroAddress();
uint256 done = paidOut[owner];
if (swept <= done) revert NothingToSettle();
shares = swept - done;
paidOut[owner] = swept;
// As in `settle`: priced by the vault's quote, and redeemed for at least what is paid.
assets = VAULT.previewRedeem(shares);
_redeem(shares, assets);
SafeTransferLib.safeTransfer(ASSET, to, assets);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs review: SavingsFacility.payOut pays earlier sweeps' shares to the record's latest recipient

payOut reads one recipient from the card records precompile at call time and pays it the whole unpaid delta against a high-water mark keyed by owner alone. If a later sweep of the same owner names a different recipient, the earlier recipient's unpaid shares are paid to the later one. Whether TIP-1169 permits that recipient change is not established.

Valid? React with 👍 / 👎.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new blocking issues. 2 optional suggestions (nits or notes on pre-existing code) were found and not posted.

@Slokh
Slokh merged commit 1dbfabf into cards Nov 12, 2026
36 checks passed
@Slokh
Slokh deleted the pyramid/ddfc1655-dfdb-4ba0-aef7-d35d59ab945d branch November 12, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants