FeeSchedule
Inherits: Ownable2Step, IFeeSchedule
Title: FeeSchedule
Author: amser
Tier-based fee registry for the amser protocol.
This contract manages fee rates independently of PaymentProcessor, enabling fee policy changes without upgrading the execution engine. Architecture:
- Fee tiers define (protocolFeeBps, keeperFeeBps) pairs.
- Modules are assigned to tiers by admins (DAO-ready via ownership transfer).
- Unassigned modules (tier 0) fall back to global defaults.
- PaymentProcessor calls resolveFees(module) once per execution.
Tier 0 is reserved as the "no tier assigned" sentinel. It cannot be
configured as a tier — modules on tier 0 always use the global defaults.
Each FeeTier has a
configuredboolean that tracks whether it has ever been set via setFeeTier(). This replaces the previous (0,0) sentinel pattern and allows zero-fee tiers for promotional or partner merchants. Security: - Only the owner (admin / DAO) can create tiers and assign modules.
- Protocol fee rates are bounded by MAX_PROTOCOL_FEE_BPS (30%).
- Keeper fee rates are bounded:
keeperFeeBps <= 10000. - Deactivating a tier causes all modules on that tier to fall back to defaults.
- This contract holds no funds and has no transfer authority.
- RECOMMENDATION: Deploy behind a TimelockController so that fee increases have a notice period (e.g. 24–48 h), giving merchants time to react. Gas Profile (per resolveFees call):
- Module with tier: 2 SLOADs (~4200 gas cold, ~200 gas warm)
- Module without tier: 1 SLOAD + 2 SLOADs (~6300 gas cold, ~300 gas warm)
Note: security-contact: security@amser.io
State Variables
defaultProtocolFeeBps
Global default protocol fee (basis points, 0-10000)
uint16 public defaultProtocolFeeBps
defaultKeeperFeeBps
Global default keeper fee (basis points of protocol fee, 0-10000)
uint16 public defaultKeeperFeeBps
_tiers
Tier definitions: tierId => fee rates
Tier 0 is reserved as "no tier assigned" and cannot be configured.
mapping(uint8 => FeeTier) private _tiers
moduleTier
Module tier assignments: module address => tierId
0 means no tier assigned (falls back to global defaults).
mapping(address => uint8) public moduleTier
MAX_PROTOCOL_FEE_BPS
Maximum protocol fee: 30% (3000 basis points).
Caps the protocol's cut to prevent governance attacks that divert 100% of merchant payments. Keeper fee is uncapped (it's a share of the already-capped protocol fee).
uint16 public constant MAX_PROTOCOL_FEE_BPS = 3000
MAX_BATCH_TIER_SIZE
Maximum number of modules in a single setModuleTierBatch call.
uint256 public constant MAX_BATCH_TIER_SIZE = 100
Functions
constructor
Deploys the FeeSchedule with global default fee rates.
constructor(uint16 _defaultProtocolFeeBps, uint16 _defaultKeeperFeeBps) Ownable(msg.sender);
Parameters
| Name | Type | Description |
|---|---|---|
_defaultProtocolFeeBps | uint16 | Default protocol fee (basis points, 0-10000) |
_defaultKeeperFeeBps | uint16 | Default keeper fee share (basis points of protocol fee, 0-10000) |
resolveFees
Resolves the effective fee rates for a module.
Called by PaymentProcessor once per execution. Resolution order:
- Check if module has a tier assigned (moduleTier != 0)
- If tier is active, return tier rates
- Otherwise, return global defaults
function resolveFees(address module)
external
view
override
returns (uint16 protocolFeeBps, uint16 keeperFeeBps);
Parameters
| Name | Type | Description |
|---|---|---|
module | address | Address of the module being executed |
Returns
| Name | Type | Description |
|---|---|---|
protocolFeeBps | uint16 | Effective protocol fee (basis points) |
keeperFeeBps | uint16 | Effective keeper fee (basis points) |
setDefaultFees
Updates the global default fee rates.
These rates apply to any module that has no tier assigned or whose tier has been deactivated.
function setDefaultFees(uint16 _protocolFeeBps, uint16 _keeperFeeBps) external onlyOwner;
Parameters
| Name | Type | Description |
|---|---|---|
_protocolFeeBps | uint16 | New default protocol fee (basis points, 0-10000) |
_keeperFeeBps | uint16 | New default keeper fee (basis points, 0-10000) |
setFeeTier
Creates or updates a fee tier.
Tier 0 is reserved and cannot be configured. Creating a tier that already exists will update its rates.
function setFeeTier(
uint8 tierId,
uint16 _protocolFeeBps,
uint16 _keeperFeeBps
)
external
onlyOwner;
Parameters
| Name | Type | Description |
|---|---|---|
tierId | uint8 | Tier identifier (1-255) |
_protocolFeeBps | uint16 | Protocol fee for this tier (basis points, 0-10000) |
_keeperFeeBps | uint16 | Keeper fee for this tier (basis points, 0-10000) |
deactivateFeeTier
Deactivates a fee tier.
All modules assigned to this tier will fall back to global defaults until the tier is reactivated. Module assignments are preserved.
function deactivateFeeTier(uint8 tierId) external onlyOwner;
Parameters
| Name | Type | Description |
|---|---|---|
tierId | uint8 | Tier identifier (1-255) |
activateFeeTier
Reactivates a previously deactivated fee tier.
Modules still assigned to this tier will resume using tier rates.
function activateFeeTier(uint8 tierId) external onlyOwner;
Parameters
| Name | Type | Description |
|---|---|---|
tierId | uint8 | Tier identifier (1-255) |
setModuleTier
Assigns a module to a fee tier.
Setting tierId to 0 removes the tier assignment (reverts to defaults). Non-zero tierId must reference an active tier.
function setModuleTier(address module, uint8 tierId) external onlyOwner;
Parameters
| Name | Type | Description |
|---|---|---|
module | address | Address of the module |
tierId | uint8 | Tier identifier (0 = remove assignment, 1-255 = assign to tier) |
setModuleTierBatch
Batch-assigns multiple modules to a fee tier.
Convenience function for DAO proposals that assign many modules at once.
function setModuleTierBatch(address[] calldata modules, uint8 tierId) external onlyOwner;
Parameters
| Name | Type | Description |
|---|---|---|
modules | address[] | Array of module addresses |
tierId | uint8 | Tier identifier (0 = remove assignment, 1-255 = assign to tier) |
getFeeTier
Returns the full configuration of a fee tier.
function getFeeTier(uint8 tierId)
external
view
returns (uint16 protocolFeeBps, uint16 keeperFeeBps, bool active, bool configured);
Parameters
| Name | Type | Description |
|---|---|---|
tierId | uint8 | Tier identifier |
Returns
| Name | Type | Description |
|---|---|---|
protocolFeeBps | uint16 | Protocol fee (basis points) |
keeperFeeBps | uint16 | Keeper fee (basis points) |
active | bool | Whether the tier is currently active |
configured | bool | Whether the tier has ever been set via setFeeTier |
getModuleFees
Returns the effective fee rates for a module (same as resolveFees).
Convenience function with explicit naming for dashboard/SDK use.
function getModuleFees(address module)
external
view
returns (uint16 protocolFeeBps, uint16 keeperFeeBps, uint8 tierId, bool isUsingDefault);
Parameters
| Name | Type | Description |
|---|---|---|
module | address | Module address |
Returns
| Name | Type | Description |
|---|---|---|
protocolFeeBps | uint16 | Effective protocol fee (basis points) |
keeperFeeBps | uint16 | Effective keeper fee (basis points) |
tierId | uint8 | The tier the module is on (0 = default) |
isUsingDefault | bool | Whether the module is using default rates |
Events
DefaultFeesUpdated
event DefaultFeesUpdated(uint16 protocolFeeBps, uint16 keeperFeeBps);
FeeTierCreated
event FeeTierCreated(uint8 indexed tierId, uint16 protocolFeeBps, uint16 keeperFeeBps);
FeeTierUpdated
event FeeTierUpdated(uint8 indexed tierId, uint16 protocolFeeBps, uint16 keeperFeeBps);
FeeTierDeactivated
event FeeTierDeactivated(uint8 indexed tierId);
FeeTierActivated
event FeeTierActivated(uint8 indexed tierId);
ModuleTierAssigned
event ModuleTierAssigned(address indexed module, uint8 indexed tierId);
Errors
InvalidFees
error InvalidFees();
InvalidTier
error InvalidTier();
TierNotActive
error TierNotActive();
InvalidModule
error InvalidModule();
BatchTooLarge
error BatchTooLarge();
Structs
FeeTier
Fee rates for a specific tier.
Packed into a single 32-byte storage slot. protocolFeeBps (16 bits) + keeperFeeBps (16 bits) + active (1 bit) + configured (1 bit) = 34 bits. configured = false means "fall through to global default" in activateFeeTier.
struct FeeTier {
uint16 protocolFeeBps; // Protocol's cut of each payment (0-10000)
uint16 keeperFeeBps; // Keeper's share of protocol fee (0-10000)
bool active; // Whether this tier is currently active
bool configured; // Whether this tier has ever been set via setFeeTier
}