3. Live Contracts — Mainnet & Fuji
Mainnet C-Chain · chainId 431140xa116261Ed3a848A9E1cd34923D5A0442D1455F710x01BEEA13A485c7bAD58f926E345325e9e3773bEeFuji Testnet · chainId 43113
0xa3Bc5564A18e107807aF41fF2a5215Db050b22dD0xcFDdeA5482baE9A6733B58F6a39FC36BCe6164cFThe deployed ComplianceGate contracts (Fuji and Mainnet C-Chain) check tier >= 2, the old single-ladder rule, so businesses (Tier 4) and agents (Tier 5) pass them. They are immutable; a category-aware gate is planned for mainnet hardening.
Solidity Integration
Block non-compliant users at the smart contract level — one line of code
pragma solidity ^0.8.28;
interface IAttestationStore {
function verify(address subject)
external view
returns (bool verified, uint32 tier,
uint64 timestamp, uint64 expiry);
}
contract MyDeFiProtocol {
IAttestationStore public immutable kumply;
constructor() {
kumply = IAttestationStore(
0xa116261Ed3a848A9E1cd34923D5A0442D1455F71
);
}
function deposit(uint256 amount) external {
(bool verified, uint32 tier,,) = kumply.verify(msg.sender);
require(verified, "KYC required");
require(tier >= 2, "Standard tier needed");
}
}
Smart Accounts and Paymasters (ERC-4337)
Gating gas sponsorship or smart-account actions on a KUMPLY attestation, from another contract.
verify() is free by design
verify() is a view function with no fee logic, and the deployed AttestationStore is not upgradeable, so it never charges a fee, whoever calls it and whether or not the caller is subscribed (callers only pay normal gas when calling it inside a transaction). The per-call fee (verificationFee, currently 0) and subscribedCallers only apply to checkCompliance() and to ComplianceGate.
What verify() returns
(verified, tier, timestamp, expiry). timestamp is the issuance time and expiry the expiration time, both in UNIX seconds. verified is false if the attestation expired, was revoked, or was never issued, and then every other field is 0, so you never compare expiry yourself. It keeps working while the contract is paused.
Attest and check the smart account address
Attestations are keyed by the exact address that signs or is sponsored. For a smart account that is userOp.sender, the account contract itself, not the EOA that owns it. Checking the owner EOA when only the smart account was attested (or the other way round) returns not verified.
Inside validatePaymasterUserOp: don't call verify()
ERC-7562 (rule OP-011) blocks the TIMESTAMP opcode during UserOperation validation for every entity, staked or not, and verify() reads block.timestamp, so bundlers that enforce ERC-7562 will reject it. Read the raw attestations(sender) record instead and return its expiry as validUntil: the EntryPoint enforces the time check for you. The record sits in a mapping keyed by the sender, which ERC-7562 treats as storage associated with the account (STO-021). Outside validation (postOp, your own contracts, off-chain), verify() is fine.
Re-issuing overwrites
issueAttestation() on an address that already has a record replaces it (tier, timestamp and expiry) without an error. Read the current record; don't assume the first one you saw still holds.
totalAttestations() counts issuances
It is a historical counter of every issuance, including re-issues. It does not go down on revocation or expiry, so it is not the number of currently valid attestations.
Example: validation helper without TIMESTAMP
Example code, not audited. It compiles and is tested in the KUMPLY repo (contracts/examples/KumplyPaymasterCheck.sol), including a trace check that it never executes TIMESTAMP. It requires an exact tier (5 for agents), because tiers 4 and 5 are categories, not higher levels. Adapt requiredTier and the rest of your paymaster logic to your own policy.
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.28;
/// @notice Read-only view of AttestationStore's public attestations getter.
interface IKumplyAttestations {
function attestations(address subject) external view returns (
bool verified,
uint32 tier,
uint64 timestamp,
uint64 expiry,
address verifier
);
}
/// @title KumplyPaymasterCheck - example ERC-4337 paymaster gate on a KUMPLY attestation
/// @notice ERC-7562 (rule OP-011) blocks the TIMESTAMP opcode during UserOperation
/// validation, and AttestationStore.verify() reads block.timestamp. This helper
/// reads the raw attestations(sender) record instead and hands the expiry to
/// the EntryPoint as validUntil, so the time check happens outside validation.
/// @dev Call kumplyValidationData(userOp.sender) from validatePaymasterUserOp and return
/// the result as validationData. The record lives in a mapping keyed by the sender,
/// so it is storage associated with the account (ERC-7562 STO-021).
contract KumplyPaymasterCheck {
/// @notice ERC-4337 validationData value meaning "reject this UserOperation"
uint256 internal constant SIG_VALIDATION_FAILED = 1;
/// @notice KUMPLY AttestationStore this paymaster reads from
IKumplyAttestations public immutable kumply;
/// @notice Exact tier required for sponsorship (5 = agent, KYA)
/// @dev Tiers are not a single ladder: 1-3 are levels for people, 4 (business) and
/// 5 (agent) are separate categories. An exact match keeps a business from
/// passing an agent check and the other way round.
uint32 public immutable requiredTier;
/// @param _kumply AttestationStore address (same address on the network you sponsor on)
/// @param _requiredTier Exact tier to sponsor (use 5 for agents)
constructor(IKumplyAttestations _kumply, uint32 _requiredTier) {
kumply = _kumply;
requiredTier = _requiredTier;
}
/// @notice ERC-4337 validationData for sponsoring sender
/// @param sender The smart account address (userOp.sender), not its owner EOA
/// @return validationData SIG_VALIDATION_FAILED if there is no attestation (never issued
/// or revoked) or the tier is not requiredTier; otherwise validUntil = expiry and
/// validAfter = 0, packed as validUntil << 160
function kumplyValidationData(address sender) public view returns (uint256 validationData) {
(bool verified, uint32 tier, , uint64 expiry, ) = kumply.attestations(sender);
if (!verified || tier != requiredTier) return SIG_VALIDATION_FAILED;
uint48 validUntil = expiry > type(uint48).max ? type(uint48).max : uint48(expiry);
return uint256(validUntil) << 160;
}
}