RewardClaim
Inherits: IRewardClaim, Initializable, UUPSUpgradeable, PausableUpgradeable, AccessControlUpgradeable, ReentrancyGuardUpgradeable
Title: RewardClaim - Espresso Reward Claim Contract
Allows validators and delegators to claim ESP token rewards based on cryptographic proofs from the Espresso network.
Daily Limit Fairness: This contract enforces daily claim limits on a first-come, first-served basis. Once the limit is reached, remaining claimers must wait until the next day. In unlikely but not impossible scenarios they may be unable to claim for multiple days. The limit (default 1%, max 5% of supply) is set high enough that it should never be reached under normal operation. This is a simple defense-in-depth mechanism to limit potential damage in the unlikely case an attacker is able to circumvent authentication of reward claims. It is not a mechanism to throttle rate of withdrawals under normal operation. Stakers are encouraged to claim their rewards periodically and take advantage of staking their rewards.
Governance Architecture: This contract uses ONLY AccessControlUpgradeable.
- DEFAULT_ADMIN_ROLE: Can upgrade contract, manage roles, update daily limits
- PAUSER_ROLE: Can pause/unpause user facing methods in the contract during emergencies
Governance: This contract enforces a single-admin model.
currentAdminandDEFAULT_ADMIN_ROLEalways reference the same address and can only be changed viagrantRole(DEFAULT_ADMIN_ROLE, ...). Any attempt to revoke or renounce the default admin role reverts so that there is always a single admin.
State Variables
espToken
The ESP token contract
EspTokenV2 public espToken
lightClient
The light client contract
LightClientV3 public lightClient
claimedRewards
Tracks total lifetime rewards claimed by each address
mapping(address claimer => uint256 claimed) public claimedRewards
dailyLimitWei
Maximum amount (in Wei) that can be claimed per day across all claimers
Daily limits provide defense-in-depth security: in the unlikely event an exploit for the merkle proof verification is discovered, at most the daily limit can be minted before the contract is paused by the PAUSER_ROLE. This offers a second layer of protection beyond cryptographic verification.
This parameter is intentionally kept non-dynamic such that inflating the token
totalSupply will not inflate the value of this limiting parameter.
uint256 public dailyLimitWei
lastSetDailyLimitBasisPoints
Basis points used when daily limit was last set (for reference only)
This is a snapshot of the basis points parameter from the last setDailyLimit call. As total supply changes, this value becomes outdated and no longer represents the actual percentage that dailyLimitWei represents relative to current supply.
uint256 public lastSetDailyLimitBasisPoints
MAX_DAILY_LIMIT_BASIS_POINTS
Maximum daily limit as percentage of total supply in basis points (500 = 5%)
Hardcoded to prevent setting dangerously high limits without a contract upgrade. Increasing this value further would require upgrading the contract, which is intentional to ensure careful consideration and governance of security parameters.
uint256 public constant MAX_DAILY_LIMIT_BASIS_POINTS = 500
BPS_DENOMINATOR
Basis points denominator (100% = 10000 bps)
uint256 public constant BPS_DENOMINATOR = 10000
_currentDay
Current day number (days since epoch)
uint256 private _currentDay
_claimedToday
Amount claimed today across all claimers
No view functions provided for _currentDay or _claimedToday to avoid race conditions. Clients should use call/estimateGas on claimRewards() to check if a claim would succeed. Honest claims should never hit rate limits under normal operation.
It may be potentially useful to add a getter for when the daily limit will reset. We don’t expect to hit the daily limits, therefore implementation in the contract and in clients is not part of the initial release.
uint256 private _claimedToday
PAUSER_ROLE
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE")
currentAdmin
Current admin address with DEFAULT_ADMIN_ROLE
Tracks the single admin to enforce single-admin invariant
address public currentAdmin
totalClaimed
Total amount of rewards claimed across all users
Enables convenient monitoring of unclaimed rewards by subtracting totalClaimed
from total_reward_distributed in the Espresso block header. As long as the total unclaimed
rewards is less than the daily limit, honest claims are guaranteed to never exceed the daily
limit.
uint256 public totalClaimed
Functions
constructor
constructor() ;
initialize
Initializes the RewardClaim contract
Sets daily limit to 1% of total ESP token supply
function initialize(address _admin, address _espToken, address _lightClient, address _pauser)
external
virtual
initializer;
Parameters
| Name | Type | Description |
|---|---|---|
_admin | address | Address that will be granted DEFAULT_ADMIN_ROLE for contract administration |
_espToken | address | Address of the ESP token contract |
_lightClient | address | Address of the light client contract |
_pauser | address | Address to be granted the pauser role |
pause
function pause() external virtual onlyRole(PAUSER_ROLE);
unpause
function unpause() external virtual onlyRole(PAUSER_ROLE);
setDailyLimit
Updates the daily limit
This function computes an absolute daily limit in Wei by multiplying the supplied basis points with the current total supply of ESP tokens.
nonReentrant protects against reentrancy during the external call to totalSupply.
Unlikely to be exploited: we are calling our token, but the token is upgradable.
DO NOT REMOVE: Added for defense-in-depth.
function setDailyLimit(uint256 basisPoints)
external
virtual
onlyRole(DEFAULT_ADMIN_ROLE)
nonReentrant;
Parameters
| Name | Type | Description |
|---|---|---|
basisPoints | uint256 | Daily limit as basis points of current total supply (1-500 for 0.01%-5%) |
claimRewards
Claim all unclaimed staking rewards
nonReentrant is not strictly necessary:
- claimedRewards updated before external call
- re-entrancy would change msg.sender making proof verification fail
- we are calling our token
The token is upgradable, the modifier makes re-entrancy simpler to reason about.
DO NOT REMOVE: added for defense-in-depth and clarity.
See RewardClaim.Reentrancy.Unit.t.sol for regression test.
function claimRewards(uint256 lifetimeRewards, bytes calldata authData)
external
virtual
whenNotPaused
nonReentrant;
Parameters
| Name | Type | Description |
|---|---|---|
lifetimeRewards | uint256 | Total earned lifetime rewards for the user |
authData | bytes | Authentication data from Espresso query service |
getVersion
function getVersion()
external
pure
virtual
returns (uint8 majorVersion, uint8 minorVersion, uint8 patchVersion);
_enforceDailyLimit
See “Daily Limit Fairness” in contract docs.
function _enforceDailyLimit(uint256 amount) internal virtual;
_authorizeUpgrade
only the timelock can authorize an upgrade
function _authorizeUpgrade(address newImplementation)
internal
virtual
override
onlyRole(DEFAULT_ADMIN_ROLE);
grantRole
Override grantRole to enforce single-admin invariant for DEFAULT_ADMIN_ROLE
When granting DEFAULT_ADMIN_ROLE, automatically revokes it from the current admin. This ensures only one address has DEFAULT_ADMIN_ROLE at any time, atomically. This is intentionally not pausable for emergency governance access.
function grantRole(bytes32 role, address account) public virtual override;
renounceRole
Prevent renouncing DEFAULT_ADMIN_ROLE to preserve governance control
Override renounceRole() to revert when attempting to renounce DEFAULT_ADMIN_ROLE, preventing accidental or malicious admin role renunciation
Revokes role from the calling account.
Roles are often managed via {grantRole} and {revokeRole}: this function’s
purpose is to provide a mechanism for accounts to lose their privileges
if they are compromised (such as when a trusted device is misplaced).
If the calling account had been revoked role, emits a {RoleRevoked}
event.
Requirements:
- the caller must be
callerConfirmation. May emit a {RoleRevoked} event.
function renounceRole(bytes32 role, address callerConfirmation) public virtual override;
revokeRole
Prevent revoking DEFAULT_ADMIN_ROLE to preserve the single-admin invariant.
Revokes role from account.
If account had been granted role, emits a {RoleRevoked} event.
Requirements:
- the caller must have
role’s admin role. May emit a {RoleRevoked} event.
function revokeRole(bytes32 role, address account) public virtual override;
_verifyAuthRoot
function _verifyAuthRoot(uint256 lifetimeRewards, bytes calldata authData)
internal
view
virtual
returns (bool);
Events
DailyLimitUpdated
The daily limit is updated
event DailyLimitUpdated(uint256 oldLimit, uint256 newLimit);
Errors
ZeroDailyLimit
Attempting to set daily limit to zero
error ZeroDailyLimit();
DailyLimitTooHigh
Attempting to set daily limit above the maximum allowed percentage
error DailyLimitTooHigh();
NoChangeRequired
Attempting to set daily limit to the current value
error NoChangeRequired();
ZeroTotalSupply
Total ESP token supply is zero during initialization
error ZeroTotalSupply();
ZeroPauserAddress
Pauser address is zero during initialization
error ZeroPauserAddress();
ZeroAdminAddress
Admin address is zero during initialization
error ZeroAdminAddress();
ZeroLightClientAddress
Light client address is zero during initialization
error ZeroLightClientAddress();
ZeroTokenAddress
ESP token address is zero during initialization
error ZeroTokenAddress();
DefaultAdminCannotBeRenounced
Attempted to renounce DEFAULT_ADMIN_ROLE which would break governance
error DefaultAdminCannotBeRenounced();
DefaultAdminCannotBeRevoked
Attempted to revoke DEFAULT_ADMIN_ROLE which would break governance
error DefaultAdminCannotBeRevoked();