From 9021799104162bfa81424357f4f5ca4d4ad95cf9 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Wed, 3 Jun 2026 19:29:39 +0300 Subject: [PATCH 1/2] fix(openclaw): ship Sardis skill pack files --- .gitignore | 2 ++ .../sardis-openclaw/skills/audit/SKILL.md | 26 +++++++++++++++ .../sardis-openclaw/skills/payments/SKILL.md | 32 +++++++++++++++++++ .../skills/spending-policy/SKILL.md | 26 +++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 packages/sardis-openclaw/skills/audit/SKILL.md create mode 100644 packages/sardis-openclaw/skills/payments/SKILL.md create mode 100644 packages/sardis-openclaw/skills/spending-policy/SKILL.md diff --git a/.gitignore b/.gitignore index 6916be933..1b0f12832 100644 --- a/.gitignore +++ b/.gitignore @@ -296,6 +296,8 @@ gv-team.md .windsurf/ .zencoder/ skills/ +!packages/sardis-openclaw/skills/ +!packages/sardis-openclaw/skills/** skills-lock.json AGENTS.md diff --git a/packages/sardis-openclaw/skills/audit/SKILL.md b/packages/sardis-openclaw/skills/audit/SKILL.md new file mode 100644 index 000000000..716acad0f --- /dev/null +++ b/packages/sardis-openclaw/skills/audit/SKILL.md @@ -0,0 +1,26 @@ +--- +name: sardis-audit +description: Teach an OpenClaw agent to inspect Sardis transactions, preserve evidence, and verify authority before reporting payment state. +homepage: https://sardis.sh +user-invocable: false +--- + +# Sardis Audit + +Use this skill only when `SARDIS_API_KEY` is present. If it is missing, state that Sardis audit data is unavailable. + +## Audit Flow + +Use `sardis_list_transactions` to inspect the activity ledger before summarizing payment history, card activity, or budget usage. + +Use `sardis_check_balance` before claiming available spend capacity. + +Use `sardis_check_policy` before saying a future spend is allowed. + +## Reporting Rules + +Report Sardis transaction identifiers, status, merchant, amount, currency, and evidence references when available. + +Distinguish `pending`, `requires_approval`, `denied`, `settled`, and `revoked` states. + +Never claim a payment completed from intent text alone. The ledger status is the source of truth. diff --git a/packages/sardis-openclaw/skills/payments/SKILL.md b/packages/sardis-openclaw/skills/payments/SKILL.md new file mode 100644 index 000000000..ba62f9cc7 --- /dev/null +++ b/packages/sardis-openclaw/skills/payments/SKILL.md @@ -0,0 +1,32 @@ +--- +name: sardis-payments +description: Teach an OpenClaw agent to use Sardis payment verbs with policy checks, reversibility awareness, and fail-closed handling. +homepage: https://sardis.sh +user-invocable: false +--- + +# Sardis Payments + +Use this skill only when `SARDIS_API_KEY` is present. If it is missing, do not create wallets, issue cards, or attempt payments. + +## Payment Verbs + +Use `sardis_give_wallet` to provision the agent payment identity. + +Use `sardis_spend` only after a successful `sardis_check_policy` result. + +Use `sardis_pay_invoice` for invoice-shaped requests instead of manually constructing a payment when invoice metadata is available. + +Use `sardis_issue_card` only when the user has explicitly requested a card and the policy result permits it. + +Use `sardis_freeze_card` when a card is revoked, compromised, outside mandate, or no longer needed. + +## Fail-Closed Rules + +If Sardis returns `requires_approval`, surface the approval requirement and wait. + +If Sardis returns `deny`, stop the payment path. + +If Sardis is unavailable, treat the action as denied until the service is reachable again. + +Never expose raw card data, private keys, API keys, signing secrets, or provider tokens. diff --git a/packages/sardis-openclaw/skills/spending-policy/SKILL.md b/packages/sardis-openclaw/skills/spending-policy/SKILL.md new file mode 100644 index 000000000..399bfc7b1 --- /dev/null +++ b/packages/sardis-openclaw/skills/spending-policy/SKILL.md @@ -0,0 +1,26 @@ +--- +name: sardis-spending-policy +description: Teach an OpenClaw agent to set budgets, check policy before spend, and honor Sardis allow / requires_approval / deny outcomes. +homepage: https://sardis.sh +user-invocable: false +--- + +# Sardis Spending Policy + +Use this skill only when `SARDIS_API_KEY` is present. If it is missing, stop and ask the operator to configure Sardis before attempting any money movement. + +## Required Flow + +1. Create or update a budget with `sardis_set_budget` before the first spend. +2. Run `sardis_check_policy` before every `sardis_spend`. +3. Continue only when the policy result is `allow`. +4. Pause when the result is `requires_approval`. +5. Refuse when the result is `deny`. + +## Guardrails + +Never infer authority from user wording alone. The Sardis policy result is the authority boundary. + +Never split one payment into smaller payments to bypass a budget, approval threshold, merchant block, token block, or time window. + +Never retry a denied action with changed fields unless the operator explicitly changes the budget or mandate first. From ed9686d60dddd1130429cf779df279bf1ed23a0c Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Sun, 14 Jun 2026 15:52:42 +0300 Subject: [PATCH 2/2] chore(contracts): remove deprecated SardisVerifyingPaymaster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SardisVerifyingPaymaster.sol is marked DEPRECATED / "do NOT deploy to mainnet" in-file — superseded by Circle Paymaster (permissionless, no deployment needed). No contract, script, or test references it. Remove it and its test, and drop the stale mention from the ReentrantToken mock comment. forge build clean. --- contracts/src/SardisVerifyingPaymaster.sol | 157 ---------------- contracts/test/SardisVerifyingPaymaster.t.sol | 173 ------------------ contracts/test/mocks/ReentrantToken.sol | 2 +- 3 files changed, 1 insertion(+), 331 deletions(-) delete mode 100644 contracts/src/SardisVerifyingPaymaster.sol delete mode 100644 contracts/test/SardisVerifyingPaymaster.t.sol diff --git a/contracts/src/SardisVerifyingPaymaster.sol b/contracts/src/SardisVerifyingPaymaster.sol deleted file mode 100644 index 293655ffa..000000000 --- a/contracts/src/SardisVerifyingPaymaster.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MIT -// DEPRECATED: This contract has been superseded by Circle Paymaster (permissionless, no deployment needed). -// Circle Paymaster address (all chains): 0x0578cFB241215b77442a541325d6A4E6dFE700Ec -// Kept for reference only — do NOT deploy to mainnet. -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/utils/Pausable.sol"; -import "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; - -/** - * @title SardisVerifyingPaymaster - * @notice ERC-4337 paymaster with wallet allowlist and sponsor caps. - * @dev Validation supports optional off-chain verifier signatures in paymasterData. - */ -contract SardisVerifyingPaymaster is IPaymaster, Ownable, Pausable { - using ECDSA for bytes32; - - IEntryPoint public immutable entryPoint; - - /// @notice Optional verifier signer for sponsorship approvals. - address public verifier; - - /// @notice Max cost allowed per sponsored operation. - uint256 public maxSponsoredCostPerOp; - - /// @notice Daily sponsorship cap for this paymaster. - uint256 public dailySponsorCap; - - /// @notice Amount spent today in wei-equivalent units. - uint256 public spentToday; - - /// @notice Last day index for daily cap accounting. - uint256 public lastSpendDay; - - mapping(address => bool) public allowedWallets; - mapping(address => bool) public allowedTokens; - - event WalletAllowlistUpdated(address indexed wallet, bool allowed); - event TokenAllowlistUpdated(address indexed token, bool allowed); - event SponsorshipCapsUpdated(uint256 maxPerOp, uint256 dailyCap); - event VerifierUpdated(address indexed oldVerifier, address indexed newVerifier); - - error NotEntryPoint(); - error WalletNotAllowed(address wallet); - error CostLimitExceeded(uint256 maxCost, uint256 limit); - error DailyCapExceeded(uint256 maxCost, uint256 remaining); - error InvalidVerifierSignature(); - - constructor(address entryPoint_, address owner_, uint256 maxSponsoredCostPerOp_, uint256 dailySponsorCap_) - Ownable(owner_) - { - require(entryPoint_ != address(0), "entrypoint=0"); - entryPoint = IEntryPoint(entryPoint_); - maxSponsoredCostPerOp = maxSponsoredCostPerOp_; - dailySponsorCap = dailySponsorCap_; - lastSpendDay = block.timestamp / 1 days; - } - - modifier onlyEntryPoint() { - if (msg.sender != address(entryPoint)) revert NotEntryPoint(); - _; - } - - function setWalletAllowed(address wallet, bool allowed) external onlyOwner { - allowedWallets[wallet] = allowed; - emit WalletAllowlistUpdated(wallet, allowed); - } - - function setTokenAllowed(address token, bool allowed) external onlyOwner { - allowedTokens[token] = allowed; - emit TokenAllowlistUpdated(token, allowed); - } - - function setCaps(uint256 maxPerOp, uint256 dailyCap) external onlyOwner { - maxSponsoredCostPerOp = maxPerOp; - dailySponsorCap = dailyCap; - emit SponsorshipCapsUpdated(maxPerOp, dailyCap); - } - - function setVerifier(address newVerifier) external onlyOwner { - address old = verifier; - verifier = newVerifier; - emit VerifierUpdated(old, newVerifier); - } - - function withdrawTo(address payable to, uint256 amount) external onlyOwner { - entryPoint.withdrawTo(to, amount); - } - - function deposit() external payable { - entryPoint.depositTo{ value: msg.value }(address(this)); - } - - /** - * @inheritdoc IPaymaster - */ - function validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) - external - override - onlyEntryPoint - whenNotPaused - returns (bytes memory context, uint256 validationData) - { - if (!allowedWallets[userOp.sender]) revert WalletNotAllowed(userOp.sender); - if (maxCost > maxSponsoredCostPerOp) revert CostLimitExceeded(maxCost, maxSponsoredCostPerOp); - - _rollDayIfNeeded(); - uint256 remaining = dailySponsorCap > spentToday ? (dailySponsorCap - spentToday) : 0; - if (maxCost > remaining) revert DailyCapExceeded(maxCost, remaining); - - // Optional paymasterData format: abi.encode(address token, bytes verifierSignature) - if (userOp.paymasterAndData.length >= 52 + 32 + 32) { - bytes calldata paymasterData = ERC4337Utils.paymasterData(userOp); - (address token, bytes memory sig) = abi.decode(paymasterData, (address, bytes)); - if (token != address(0)) { - require(allowedTokens[token], "token_not_allowed"); - } - if (verifier != address(0)) { - bytes32 innerHash = keccak256( - abi.encodePacked(address(this), block.chainid, userOp.sender, token, maxCost, userOpHash) - ); - bytes32 digest = keccak256(abi.encodePacked("\\x19Ethereum Signed Message:\\n32", innerHash)); - if (digest.recover(sig) != verifier) revert InvalidVerifierSignature(); - } - } - - context = abi.encode(userOp.sender, maxCost); - validationData = ERC4337Utils.SIG_VALIDATION_SUCCESS; - } - - /** - * @inheritdoc IPaymaster - */ - function postOp(PostOpMode, bytes calldata context, uint256 actualGasCost, uint256) - external - override - onlyEntryPoint - { - (address sender, uint256 reservedMaxCost) = abi.decode(context, (address, uint256)); - sender; // silence unused var warning while preserving context shape - reservedMaxCost; - - _rollDayIfNeeded(); - spentToday += actualGasCost; - } - - function _rollDayIfNeeded() internal { - uint256 today = block.timestamp / 1 days; - if (today != lastSpendDay) { - lastSpendDay = today; - spentToday = 0; - } - } -} diff --git a/contracts/test/SardisVerifyingPaymaster.t.sol b/contracts/test/SardisVerifyingPaymaster.t.sol deleted file mode 100644 index f97baeba7..000000000 --- a/contracts/test/SardisVerifyingPaymaster.t.sol +++ /dev/null @@ -1,173 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "forge-std/Test.sol"; -import "../src/SardisVerifyingPaymaster.sol"; -import "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; - -contract SardisVerifyingPaymasterTest is Test { - SardisVerifyingPaymaster internal paymaster; - - address internal wallet = address(0xAAA1); - - function setUp() public { - // this test contract acts as mock EntryPoint. - paymaster = new SardisVerifyingPaymaster(address(this), address(this), 1 ether, 10 ether); - } - - function _buildUserOp(address sender) internal pure returns (PackedUserOperation memory op) { - op.sender = sender; - op.nonce = 0; - op.initCode = ""; - op.callData = ""; - op.accountGasLimits = bytes32(0); - op.preVerificationGas = 0; - op.gasFees = bytes32(0); - op.paymasterAndData = ""; - op.signature = ""; - } - - function testValidatePaymasterUserOpSuccess() public { - paymaster.setWalletAllowed(wallet, true); - - PackedUserOperation memory op = _buildUserOp(wallet); - (bytes memory context, uint256 validationData) = paymaster.validatePaymasterUserOp(op, bytes32("op"), 0.1 ether); - - assertGt(context.length, 0); - assertEq(validationData, 0); - } - - function testValidatePaymasterUserOpRevertsWhenWalletNotAllowed() public { - PackedUserOperation memory op = _buildUserOp(wallet); - vm.expectRevert(); - paymaster.validatePaymasterUserOp(op, bytes32("op"), 0.1 ether); - } - - function testValidatePaymasterUserOpRevertsOnCapExceeded() public { - paymaster.setWalletAllowed(wallet, true); - - PackedUserOperation memory op = _buildUserOp(wallet); - vm.expectRevert(); - paymaster.validatePaymasterUserOp(op, bytes32("op"), 2 ether); - } - - function testPostOpTracksSpend() public { - paymaster.setWalletAllowed(wallet, true); - bytes memory context = abi.encode(wallet, 0.1 ether); - - paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 0.05 ether, 0); - - assertEq(paymaster.spentToday(), 0.05 ether); - } - - // ============ Fuzz: Gas Calculation ============ - - function testFuzz_gasCalculation(uint256 maxCost) public { - // Bound maxCost to reasonable range - maxCost = bound(maxCost, 0, 100 ether); - - paymaster.setWalletAllowed(wallet, true); - - PackedUserOperation memory op = _buildUserOp(wallet); - - if (maxCost > 1 ether) { - // Exceeds maxSponsoredCostPerOp (set to 1 ether in setUp) - vm.expectRevert(); - paymaster.validatePaymasterUserOp(op, bytes32("op"), maxCost); - } else { - // Should succeed; cap check passes - (bytes memory context, uint256 validationData) = - paymaster.validatePaymasterUserOp(op, bytes32("op"), maxCost); - assertGt(context.length, 0); - assertEq(validationData, 0); - } - } - - function testFuzz_postOpAccumulatesSpend(uint256 gasCost1, uint256 gasCost2) public { - gasCost1 = bound(gasCost1, 0, 5 ether); - gasCost2 = bound(gasCost2, 0, 5 ether); - - paymaster.setWalletAllowed(wallet, true); - - bytes memory context = abi.encode(wallet, 1 ether); - - paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, gasCost1, 0); - paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, gasCost2, 0); - - assertEq(paymaster.spentToday(), gasCost1 + gasCost2); - } - - function testFuzz_dailyCapExhaustion(uint256 maxCost) public { - // Exhaust most of the daily cap, then try one more - maxCost = bound(maxCost, 0.01 ether, 1 ether); - - paymaster.setWalletAllowed(wallet, true); - - // Spend up to the cap via postOp - bytes memory context = abi.encode(wallet, 1 ether); - paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 9.5 ether, 0); - - PackedUserOperation memory op = _buildUserOp(wallet); - - uint256 remaining = 10 ether - 9.5 ether; // 0.5 ether - if (maxCost > remaining) { - vm.expectRevert(); - paymaster.validatePaymasterUserOp(op, bytes32("op"), maxCost); - } else { - (bytes memory ctx,) = paymaster.validatePaymasterUserOp(op, bytes32("op"), maxCost); - assertGt(ctx.length, 0); - } - } - - // ============ Reentrancy: postOp ============ - - function test_reentrancy_postOpDoubleCall() public { - // The paymaster's postOp only tracks spend, no external calls. - // Verify that calling postOp twice doesn't create inconsistency. - paymaster.setWalletAllowed(wallet, true); - - bytes memory context = abi.encode(wallet, 0.5 ether); - - // First call - paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 0.3 ether, 0); - assertEq(paymaster.spentToday(), 0.3 ether); - - // Second call (simulates what would happen in reentrancy) - paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 0.2 ether, 0); - assertEq(paymaster.spentToday(), 0.5 ether); - } - - function test_reentrancy_validateAndPostOpSequence() public { - // Ensure validate followed by postOp correctly tracks state - paymaster.setWalletAllowed(wallet, true); - - PackedUserOperation memory op = _buildUserOp(wallet); - (bytes memory context,) = paymaster.validatePaymasterUserOp(op, bytes32("op"), 0.5 ether); - - // postOp with actual cost less than maxCost - paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 0.3 ether, 0); - assertEq(paymaster.spentToday(), 0.3 ether); - - // Another validate should still have remaining cap - (bytes memory context2,) = paymaster.validatePaymasterUserOp(op, bytes32("op2"), 0.5 ether); - assertGt(context2.length, 0); - } - - // ============ Daily Reset ============ - - function testDailyCapResets() public { - paymaster.setWalletAllowed(wallet, true); - - bytes memory context = abi.encode(wallet, 1 ether); - paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 9 ether, 0); - assertEq(paymaster.spentToday(), 9 ether); - - // Warp to next day - vm.warp(block.timestamp + 1 days); - - // spentToday should reset on next interaction - PackedUserOperation memory op = _buildUserOp(wallet); - (bytes memory ctx,) = paymaster.validatePaymasterUserOp(op, bytes32("op"), 0.5 ether); - assertGt(ctx.length, 0); - } -} diff --git a/contracts/test/mocks/ReentrantToken.sol b/contracts/test/mocks/ReentrantToken.sol index f717e41be..89b41f66c 100644 --- a/contracts/test/mocks/ReentrantToken.sol +++ b/contracts/test/mocks/ReentrantToken.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; /// @title ReentrantToken /// @notice Mock ERC20 that calls back into the caller during transfer/transferFrom. -/// @dev Used to test reentrancy resistance in RefundProtocol and SardisVerifyingPaymaster. +/// @dev Used to test reentrancy resistance in RefundProtocol. /// The callback target and calldata are configurable so the same mock can attack /// any external function. contract ReentrantToken {