CreditModule
Inherits: ReentrancyGuard, IExecutableModule, IRegistrableModule, IAuthorizationModule, EIP712, IInitializableModule, IMigratableModule
Title: CreditModule
Credit-based RAI module using off-chain vouchers (Envelopes) to trigger JIT refills.
State Variables
_initialized
bool private _initialized
merchant
address public merchant
paymentProcessor
address public paymentProcessor
_nextPlanId
uint256 private _nextPlanId
_nextEnvId
uint256 private _nextEnvId
_plans
mapping(uint256 => PlanPacked) internal _plans
_envelopes
mapping(uint256 => EnvelopePacked) internal _envelopes
envelopeOf
mapping(address => mapping(uint256 => uint256)) public envelopeOf
ENVELOPE_TYPEHASH
EIP-712 typehash for credit envelope settlement vouchers.
chainId is included as an explicit field in the signed struct to prevent cross-chain replay even if two deployments share the same contract address (e.g. CREATE2 with identical salt across chains). OpenZeppelin's EIP712 domain separator also includes chainId, but this provides defence-in-depth.
bytes32 public constant ENVELOPE_TYPEHASH = keccak256(
"CreditEnvelope(uint256 id,uint64 sequence,uint64 creditsUsed,bytes32 manifestHash,uint256 chainId)"
)
MAX_BATCH_QUERY
Maximum number of plan IDs accepted by batch view helpers (OOG guard).
uint256 internal constant MAX_BATCH_QUERY = 256
Functions
constructor
Note: oz-upgrades-unsafe-allow: constructor
constructor() EIP712("amser_CreditModule", "1");
initialize
function initialize(address _merchant, address _paymentProcessor) external;
setPaymentProcessor
Updates the PaymentProcessor address (migration path).
Only the current PaymentProcessor can call this, via migrateModule(). This enables protocol-level upgrades without breaking existing modules.
function setPaymentProcessor(address _newProcessor) external;
Parameters
| Name | Type | Description |
|---|---|---|
_newProcessor | address | Address of the new PaymentProcessor |
createPlan
function createPlan(
uint256 price,
uint64 batchAmount,
address token,
bytes32 metadataHash
)
external
returns (uint256 planId);
togglePlanActive
Merchant toggles plan active/inactive.
function togglePlanActive(uint256 planId) external;
onlySubscriber
Modifier to ensure only the subscriber can call pause/resume functions
modifier onlySubscriber(uint256 envId) ;
pauseEnvelope
Pauses an envelope.
Only the subscriber can pause their envelope.
function pauseEnvelope(uint256 envId) external onlySubscriber(envId);
Parameters
| Name | Type | Description |
|---|---|---|
envId | uint256 | Envelope ID |
resumeEnvelope
Resumes a paused envelope.
Only the subscriber can resume their envelope.
function resumeEnvelope(uint256 envId) external onlySubscriber(envId);
Parameters
| Name | Type | Description |
|---|---|---|
envId | uint256 | Envelope ID |
updateAllowanceExpiry
Updates the Permit2 allowance expiry for an envelope.
Only the subscriber can extend their envelope's allowance expiry. This allows recovery when an envelope's original expiry has passed but the subscriber has renewed their Permit2 allowance off-chain. Without this, remaining batches would be permanently stranded once the expiry timestamp is exceeded.
function updateAllowanceExpiry(
uint256 envId,
uint256 newExpiry
)
external
onlySubscriber(envId);
Parameters
| Name | Type | Description |
|---|---|---|
envId | uint256 | Envelope ID |
newExpiry | uint256 | New allowance expiry timestamp (must be in the future and fit in uint48) |
updateAuthorizedAgent
Rotates the authorized agent address for an envelope.
Callable by the subscriber (envelope owner) or the merchant. Updates the envelopeOf mapping atomically:
- Old agent address is removed from envelopeOf
- New agent address is mapped to this envelope The envelope's sequence, creditsConsumed, remainingBatches, and all other state is preserved. Only the agent address changes. Reverts if newAgent is already the authorized agent (no-op guard). Reverts if newAgent already has an envelope for this plan (would create a mapping collision).
function updateAuthorizedAgent(uint256 envId, address newAgent) external;
Parameters
| Name | Type | Description |
|---|---|---|
envId | uint256 | Envelope ID to update |
newAgent | address | New authorized agent address |
registerCreditsWithPermit
function registerCreditsWithPermit(
address agent,
uint256 planId,
uint256 totalBatches,
uint256 allowanceExpiry,
bytes calldata permitData,
bytes calldata signature
)
external
nonReentrant
returns (uint256 envId);
settleEnvelope
Submit co-signed proof of credit usage (checkpoint or exhaustion).
Allows cumulative checkpoint updates. Only triggers payment when fully exhausted.
PERMISSIONLESS BY DESIGN This function is callable by anyone who holds valid co-signed proofs (userSig + merchantSig). This is intentional: restricting to authorised callers would create a liveness risk where disputed or abandoned envelopes become permanently unresolvable if no authorised party is willing to pay gas. FRONT-RUNNING RISK (accepted): A mempool observer could front-run a settlement transaction by replaying the same signatures. The impact is limited to timing manipulation — an attacker cannot redirect funds, only control when isSettled becomes true and therefore when keeper execution is triggered. FUTURE ENHANCEMENT: Consider a timelock pattern where authorised parties (subscriber, merchant, authorizedAgent) can settle immediately, but any other caller must wait a SETTLEMENT_DISPUTE_WINDOW (e.g. 7 days) after the last checkpoint. This provides a neutral resolution path for abandoned envelopes without removing the liveness guarantee.
PAYMENT TRIGGER: Payment (keeper execution) is triggered only when creditsConsumed reaches plan.batchAmount (full batch exhaustion). Intermediate settlement checkpoints update creditsConsumed for accounting and audit purposes but do NOT trigger payment. IMPLICATION: If an agent consumes a partial batch and stops (e.g. 80 of 100 credits), the merchant has delivered service but does not receive payment until the full batch is consumed. The merchant co-signs each checkpoint and is therefore aware of consumption state. FUTURE: Proportional payment on settlement (amount * creditsConsumed / batchAmount) is a planned enhancement that would resolve this incentive asymmetry. Track in roadmap.
PAUSED ENVELOPES: Settlement is intentionally permitted on paused envelopes. Pausing controls keeper execution (billing), not settlement (recording already-consumed credits). Preventing settlement on paused envelopes would create a liveness risk where off-chain credit consumption could never be recorded on-chain, permanently blocking the merchant from receiving payment for delivered service.
function settleEnvelope(
uint256 id,
uint64 sequence,
uint64 creditsUsed,
bytes32 manifestHash,
bytes calldata userSig,
bytes calldata merchantSig
)
external
nonReentrant;
Parameters
| Name | Type | Description |
|---|---|---|
id | uint256 | Envelope ID |
sequence | uint64 | Current batch sequence (must match on-chain state) |
creditsUsed | uint64 | Cumulative credits used in this batch (must be > current creditsConsumed) |
manifestHash | bytes32 | Hash of work manifest for this checkpoint |
userSig | bytes | User's EIP-712 signature |
merchantSig | bytes | Merchant's EIP-712 signature |
registrationContext
function registrationContext(
uint256 planId,
address
)
external
view
override
returns (address token, uint256 minAmount);
registerExecution
function registerExecution(
address subscriber,
uint256 planId,
uint256 executionBudget,
uint256 allowanceExpiry
)
external
override
returns (uint256 id);
quoteExecution
Quotes whether execution is allowed and returns execution parameters.
Single entry point replacing canExecute, getExecutionFailureReason, and executionContext. Maps envelope-specific failure states to standardized ExecutionFailureReason enum. Failure reasons are checked in priority order:
- Envelope existence (permanent failure)
- Paused state (user-controlled)
- Plan active state (merchant-controlled)
- Remaining batches (user-controlled)
- Settlement status (credits exhausted triggers refill) When reason == None, the remaining return values contain valid execution context. When reason != None, context values are zeroed out.
function quoteExecution(uint256 id)
external
view
override
returns (
ExecutionFailureReason reason,
address payer,
address recipient,
address token,
uint256 amount,
uint256 executionTime,
uint256 windowId
);
Parameters
| Name | Type | Description |
|---|---|---|
id | uint256 | Envelope ID |
Returns
| Name | Type | Description |
|---|---|---|
reason | ExecutionFailureReason | Standardized failure reason (None if executable) |
payer | address | Address that will pay (subscriber) |
recipient | address | Address that will receive payment (merchant) |
token | address | Token address to transfer |
amount | uint256 | Amount to transfer (plan price) |
executionTime | uint256 | Canonical execution timestamp (block.timestamp for credits) |
windowId | uint256 | Idempotency window identifier (sequence number) |
onExecute
function onExecute(uint256 id, uint256) external override;
isActive
function isActive(address subject, uint256 planId) external view override returns (bool);
isActiveAny
Optional batch helper: true if any scope is active under current CreditModule semantics.
Array length is bounded by MAX_BATCH_QUERY to prevent OOG griefing. Non-existent plan IDs are safely skipped (_isActive returns false).
function isActiveAny(
address subject,
uint32[] calldata scopes
)
external
view
override
returns (bool);
isActiveBatch
Optional batch helper: returns isActive for each scope in order.
Array length is bounded by MAX_BATCH_QUERY to prevent OOG griefing. Non-existent plan IDs are safely skipped (_isActive returns false).
function isActiveBatch(
address subject,
uint32[] calldata scopes
)
external
view
returns (bool[] memory states);
_isActive
Returns false if the associated plan has been deactivated by the merchant.
function _isActive(address subject, uint256 planId) internal view returns (bool);
getEnvelope
Get envelope details by ID
function getEnvelope(uint256 id)
external
view
returns (
uint64 sequence,
bool isSettled,
uint32 planId,
uint32 remainingBatches,
address authorizedAgent,
address subscriber,
uint64 creditsConsumed,
bool paused,
uint48 allowanceExpiry
);
Parameters
| Name | Type | Description |
|---|---|---|
id | uint256 | Envelope ID |
Returns
| Name | Type | Description |
|---|---|---|
sequence | uint64 | Current batch epoch |
isSettled | bool | True if exhausted and waiting for PaymentProcessor |
planId | uint32 | The credit plan ID |
remainingBatches | uint32 | Total batches remaining |
authorizedAgent | address | The AI Agent public key |
subscriber | address | The payer address |
creditsConsumed | uint64 | Cumulative credits consumed in current batch |
paused | bool | True if envelope is paused by subscriber |
allowanceExpiry | uint48 |
getPlan
Get plan details by ID
function getPlan(uint256 planId)
external
view
returns (
uint160 price,
uint64 batchAmount,
bool active,
address token,
bytes32 metadataHash
);
Parameters
| Name | Type | Description |
|---|---|---|
planId | uint256 | Plan ID |
Returns
| Name | Type | Description |
|---|---|---|
price | uint160 | Cost per refill |
batchAmount | uint64 | Credits granted per refill |
active | bool | Whether the plan is active |
token | address | Payment token address |
metadataHash | bytes32 | Metadata hash |
Events
PlanCreated
event PlanCreated(
uint256 indexed planId, uint256 price, uint256 batchAmount, bytes32 metadataHash
);
EnvelopeCreated
event EnvelopeCreated(
uint256 indexed id,
address indexed subscriber,
address indexed agent,
uint256 planId,
uint256 remainingBatches
);
EnvelopeSettled
event EnvelopeSettled(
uint256 indexed id, uint64 sequence, bytes32 manifestHash, uint64 creditsUsed
);
CreditsRefilled
event CreditsRefilled(uint256 indexed id, uint64 newSequence, uint256 remainingBatches);
PlanActiveToggled
event PlanActiveToggled(uint256 indexed planId, bool active);
EnvelopePaused
event EnvelopePaused(uint256 indexed id);
EnvelopeUnpaused
event EnvelopeUnpaused(uint256 indexed id);
AllowanceExpiryUpdated
event AllowanceExpiryUpdated(uint256 indexed envId, uint256 newExpiry);
AuthorizedAgentUpdated
event AuthorizedAgentUpdated(
uint256 indexed envId, address indexed oldAgent, address indexed newAgent
);
PaymentProcessorUpdated
event PaymentProcessorUpdated(address indexed oldProcessor, address indexed newProcessor);
Errors
AlreadyInitialized
error AlreadyInitialized();
OnlyMerchant
error OnlyMerchant();
OnlyProcessor
error OnlyProcessor();
OnlySubscriber
error OnlySubscriber();
InvalidSignatures
error InvalidSignatures();
AlreadySettled
error AlreadySettled();
SequenceMismatch
error SequenceMismatch();
UsageMustIncrease
error UsageMustIncrease();
ExceedsBatchLimit
error ExceedsBatchLimit();
PlanDoesNotExist
error PlanDoesNotExist();
PlanNotActive
error PlanNotActive();
EnvelopeAlreadyPaused
error EnvelopeAlreadyPaused();
EnvelopeNotPaused
error EnvelopeNotPaused();
InvalidToken
error InvalidToken();
InvalidPrice
error InvalidPrice();
InvalidBatchAmount
error InvalidBatchAmount();
EnvelopeAlreadyExistsForPlan
error EnvelopeAlreadyExistsForPlan();
InvalidExecutionBudget
error InvalidExecutionBudget();
EnvelopeDoesNotExist
error EnvelopeDoesNotExist();
ArrayTooLong
error ArrayTooLong();
InvalidProcessor
error InvalidProcessor();
InvalidMerchant
error InvalidMerchant();
InvalidAgent
error InvalidAgent();
NoBatchesRemaining
error NoBatchesRemaining();
InvalidAllowanceExpiry
error InvalidAllowanceExpiry();
AgentAlreadyAuthorized
error AgentAlreadyAuthorized();
OnlySubscriberOrMerchant
error OnlySubscriberOrMerchant();
Structs
PlanPacked
Plan configuration for credit refills. Note: metadataHash is stored as a fixed-size value for compact storage.
struct PlanPacked {
uint160 price; // Cost per refill
uint64 batchAmount; // Credits granted per refill
bool active;
address token;
bytes32 metadataHash;
}
EnvelopePacked
Slot 0: sequence (64 bits) | isSettled (1 bit) | paused (1 bit) | planId (32 bits) | remainingBatches (32 bits) | allowanceExpiry (48 bits) Slot 1: authorizedAgent (160 bits) Slot 2: subscriber (160 bits) Slot 3: creditsConsumed (64 bits) - cumulative usage in current batch
struct EnvelopePacked {
uint64 sequence; // Current batch epoch
bool isSettled; // True if exhausted and waiting for PaymentProcessor
bool paused; // True if envelope is paused by subscriber
uint32 planId; // The credit plan
uint32 remainingBatches; // Total batches allowed (execution guardrail)
uint48 allowanceExpiry; // When the delegated Permit2 allowance expires
address authorizedAgent; // The AI Agent public key
address subscriber; // The payer
uint64 creditsConsumed; // Cumulative usage in current batch (checkpoint tracking)
}