git clone https://github.com/vibeforge1111/vibeship-spawner-skills
blockchain/account-abstraction/skill.yamlAccount Abstraction Skill
Expert guidance for ERC-4337, smart wallets, and paymasters
version: 1.0.0 skill_id: account-abstraction name: Account Abstraction Engineer category: blockchain description: | Comprehensive expertise in ERC-4337 account abstraction, smart contract wallets, paymasters, bundlers, and user operation handling. Covers social recovery, session keys, gas sponsorship, and wallet SDKs.
triggers:
- account abstraction
- ERC-4337
- smart wallet
- paymaster
- bundler
- user operation
- gasless transaction
- session keys
- social recovery
- smart account
expertise_areas:
- ERC-4337 specification
- Smart account development
- Paymaster implementation
- Bundler integration
- Session keys
- Social recovery
- Wallet SDK integration
- Gas abstraction
patterns:
-
id: erc4337-account name: ERC-4337 Smart Account description: | Standard smart contract wallet compatible with ERC-4337 bundler infrastructure when_to_use:
- Building smart wallet product
- Gasless user experience
- Advanced wallet features implementation: | // SPDX-License-Identifier: MIT pragma solidity ^0.8.19;
import "@account-abstraction/contracts/core/BaseAccount.sol"; import "@account-abstraction/contracts/samples/callback/TokenCallbackHandler.sol";
contract SmartAccount is BaseAccount, TokenCallbackHandler { address public owner; IEntryPoint private immutable _entryPoint;
constructor(IEntryPoint anEntryPoint, address _owner) { _entryPoint = anEntryPoint; owner = _owner; } function entryPoint() public view override returns (IEntryPoint) { return _entryPoint; } function _validateSignature( UserOperation calldata userOp, bytes32 userOpHash ) internal view override returns (uint256 validationData) { bytes32 hash = MessageHashUtils.toEthSignedMessageHash(userOpHash); address signer = ECDSA.recover(hash, userOp.signature); if (signer != owner) { return SIG_VALIDATION_FAILED; } return 0; // Valid } function execute( address dest, uint256 value, bytes calldata data ) external { _requireFromEntryPoint(); (bool success, bytes memory result) = dest.call{value: value}(data); if (!success) { assembly { revert(add(result, 32), mload(result)) } } } function executeBatch( address[] calldata dests, uint256[] calldata values, bytes[] calldata datas ) external { _requireFromEntryPoint(); require(dests.length == values.length && values.length == datas.length); for (uint i = 0; i < dests.length; i++) { (bool success,) = dests[i].call{value: values[i]}(datas[i]); require(success); } } receive() external payable {}}
User Operation Flow:
- User creates UserOperation (calldata, gas limits, signature)
- Sends to Bundler (via RPC or API)
- Bundler validates and bundles with others
- Bundler calls EntryPoint.handleOps()
- EntryPoint validates and executes each UserOp
- Gas paid from account or Paymaster security_notes:
- Validate all signatures carefully
- Handle replay protection (nonces)
- Consider upgrade mechanisms
-
id: paymaster name: Gas Sponsorship Paymaster description: | Contract that pays gas fees on behalf of users for gasless transaction experience when_to_use:
- Onboarding users without ETH
- Sponsored transactions
- Subscription-based gas implementation: | // SPDX-License-Identifier: MIT pragma solidity ^0.8.19;
import "@account-abstraction/contracts/core/BasePaymaster.sol";
contract SponsorPaymaster is BasePaymaster { mapping(address => bool) public sponsoredAccounts; uint256 public maxGasCost = 0.01 ether;
constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {} function addSponsoredAccount(address account) external onlyOwner { sponsoredAccounts[account] = true; } function _validatePaymasterUserOp( UserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 maxCost ) internal view override returns (bytes memory context, uint256 validationData) { // Check if account is sponsored require(sponsoredAccounts[userOp.sender], "Not sponsored"); // Check gas limit require(maxCost <= maxGasCost, "Gas too high"); // Return context for postOp (if needed) return (abi.encode(userOp.sender), 0); } function _postOp( PostOpMode mode, bytes calldata context, uint256 actualGasCost ) internal override { // Optional: Track gas usage per user address sender = abi.decode(context, (address)); emit GasSponsored(sender, actualGasCost); } // Deposit ETH for gas sponsorship function deposit() external payable { entryPoint().depositTo{value: msg.value}(address(this)); }}
Paymaster Types:
- Verifying Paymaster: Requires off-chain signature
- Deposit Paymaster: Users pre-deposit tokens
- Sponsor Paymaster: Free gas for approved accounts
- Token Paymaster: Pay gas in ERC20 tokens security_notes:
- Prevent DoS by rate limiting
- Validate userOp thoroughly
- Monitor deposit balance
-
id: session-keys name: Session Keys for Delegated Access description: | Temporary keys with limited permissions for improved UX without compromising security when_to_use:
- Gaming with frequent transactions
- Automated DeFi strategies
- Mobile app sessions implementation: | // SPDX-License-Identifier: MIT pragma solidity ^0.8.19;
contract SessionKeyAccount is SmartAccount { struct SessionKey { address key; uint256 validAfter; uint256 validUntil; address[] allowedTargets; uint256 spendLimit; uint256 spent; }
mapping(bytes32 => SessionKey) public sessionKeys; function createSessionKey( address key, uint256 duration, address[] calldata targets, uint256 spendLimit ) external onlyOwner returns (bytes32 keyId) { keyId = keccak256(abi.encode(key, block.timestamp)); sessionKeys[keyId] = SessionKey({ key: key, validAfter: block.timestamp, validUntil: block.timestamp + duration, allowedTargets: targets, spendLimit: spendLimit, spent: 0 }); } function _validateSignature( UserOperation calldata userOp, bytes32 userOpHash ) internal view override returns (uint256 validationData) { // Try owner signature first bytes32 hash = MessageHashUtils.toEthSignedMessageHash(userOpHash); address signer = ECDSA.recover(hash, userOp.signature); if (signer == owner) { return 0; } // Check session keys bytes32 keyId = _extractKeyId(userOp.signature); SessionKey storage sk = sessionKeys[keyId]; if (signer != sk.key) return SIG_VALIDATION_FAILED; if (!_isValidTarget(userOp.callData, sk.allowedTargets)) { return SIG_VALIDATION_FAILED; } // Return validity window return _packValidationData( false, uint48(sk.validUntil), uint48(sk.validAfter) ); }} security_notes:
- Limit session key permissions strictly
- Short validity windows (hours, not days)
- Revocation mechanism required
anti_patterns:
-
id: no-nonce-validation name: Missing UserOperation nonce validation severity: critical description: | Not properly validating nonces allows replay attacks where the same UserOperation is executed multiple times consequence: | Attackers can replay valid UserOperations
-
id: unlimited-paymaster name: Paymaster without spending limits severity: high description: | Paymaster that sponsors unlimited gas is vulnerable to draining attacks consequence: | Attackers can drain paymaster's deposit
commands: deploy_account: description: Deploy ERC-4337 smart account steps: - Choose account implementation (Safe, Kernel, etc.) - Deploy factory contract or use existing - Create account via factory - Fund account or set up paymaster - Test UserOperation submission
integrate_sdk: description: Integrate wallet SDK steps: - Choose SDK (Biconomy, ZeroDev, Alchemy, Pimlico) - Configure bundler endpoint - Set up paymaster if using - Build UserOperations - Handle transaction status