# Commit Protocol
<div style="font-size:22px;padding-bottom: 12px">
<strong>Commit Protocol</strong> is an accountability protocol designed to incentivize participants to join and commit to various activities such as showing up at events, fitness challenges, product milestones, and more.
</div>
The protocol is built to create accountability by enabling participants to commit to specific goals and receive rewards upon successful completion. It is trustless and permissionless, meaning anyone can create, join, and interact with commitments without needing authorization from a central authority.
Commits are created as ERC721 contracts and participants receive an NFT when they join.
When participants join they transfer a configured amount of tokens, the stake. These stakes become the rewards that are distributed to the participants who have been verified.
## Verifying Participants
Commit Protocol leverages **Verifier Contracts** to authenticate and validate participants **on-chain**. These verifier contracts are external smart contracts that the Commit contract calls to check if participants meet the required conditions (e.g., holding a token, attestation, or valid signature).
**How Verification Works:**
- When participants fulfill the commitment (e.g., complete a fitness goal or attend an event), an **attestation or token is issued to their wallet** by an external service.
- The participant then triggers the `commit.verify()` function on the Commit contract.
- The Commit contract queries the associated Verifier contract, passing the participant’s address and any necessary data.
- The Verifier contract checks if the participant holds the required attestation, token, or signature.
- If the verification passes, the participant becomes eligible to claim rewards.
### Available Verifiers
1. **EASVerifier** (Ethereum Attestation Service)
- **Verification:** Participants must hold an EAS attestation with a specific `schemaUID` and `attester` address.
- **Example Use Case:**
_Participants receive attestations from Event Organizer, a DAO, or another trusted source._
2. **SignatureVerifier**
- **Verification:** Participants provide a backend-issued signature proving attendance or task completion.
- **Example Use Case:**
_Event participants scan a QR code, and the backend issues a signature confirming their presence._
3. **TokenVerifier**
- **Verification:** Participants must hold at least `x` amount of a specified ERC20 or ERC721 token.
- **Example Use Case:**
_Only holders of a DAO governance token or NFT can join a private commit._
### External Services and Verification
Commit Protocol is designed to integrate seamlessly with **external services** (such as event apps or backend servers). These services are responsible for issuing attestations, signatures, or tokens that participants use to verify their commitment.
- **External Services Create Proofs:**
- Attestations, tokens, or signatures are issued **outside** the Commit contract.
- **Commit Contract Performs On-Chain Verification:**
- The `commit.verify()` function checks the participant’s wallet state by calling the Verifier contract on-chain.
### Security Benefits of On-Chain Verifiers:
- **Trustless Verification:** Commit contracts verify directly with Verifier contracts – no manual review is needed.
- **Immutable Records:** Verification results are stored on-chain, preventing tampering or disputes.
- **Interoperable:** Verifier contracts can be updated or swapped, allowing CommitProtocol to adapt to new verification methods.
## Claiming Rewards
Verified participants are able to claim their reward which is calculated as:
`(stakes + funding - fees) / verifiedCount`
### Funder Incentives
Anyone can fund existing Commits to boost participant rewards.
- **Sponsors/Supporters:** Increase participant motivation by funding the reward pool.
- **Grant Programs:** DAOs and foundations can fund public goods or fitness challenges.
- **Speculation on Outcomes:** Funders can back certain Commit pools if they believe in higher verification rates, indirectly influencing outcomes.
## User Flows
- Create Commit
- eg. _Fitness challenge - Run 5K before NYE_
- Join Commit
- https://commit.event/fitness-challenge-run-5k
- Press Commit and transfer $10
- Fund Commit
- eg. _Supporters fund the commit to create further incentives_
- Verification of participants (happens outside of Commit Protocol)
- eg _Strava integration creates an attestation_
- Verify Commit
- https://commit.event/fitness-challenge-run-5k/verify
- See verification status
- Press Verify button
- Claim Rewards
- https://commit.event/fitness-challenge-run-5k/claim
- See eligible rewards
- Press Claim button
- Creator Claim Fees
## SDK
Commit Protocol comes with an SDK for easier integration and building custom apps and frontends.
```ts
import { CommitSDK, Verifiers } from "@commit/sdk"
const commitClient = new CommitSDK({ chainId, walletClient })
const commitAddress = await commitClient.create({
owner: "0x...",
metadataURI: "ipfsCid",
joinBefore: new Date(),
verifyBefore: new Date("2025-01-01"),
verifier: Verifiers.EASVerifier, // TokenVerifier | SignatureVerifier
verifierData: { schemaUID: "0x...", attester: "0x..." },
token: Tokens.USDC,
stake: 10,
fee: 1, // 1 USDC to creator when participants join
maxParticipants: 100,
milestones: [],
client: {
recipient: "0xRecipientAddress",
fee: 1 // 1 USDC to client when participants join
share: 100 // 1% of rewards
},
})
const commit = await commitClient.get(commitAddress)
// Commits can be funded by third-parties
await commit.fund(200) // 200 USDC
// Participant flows
await commit.join()
await commit.verify(verifyData)
await commit.claim()
// Creator claim fees
await commit.claimFees()
// Creator can cancel the commit before verify date (and claiming starts)
await commit.cancel()
// Query Indexer
const [commit] = await commitClient.query({
where: { address: commitAddress }
})
// External service creating an attestation
const participantAddress = commit.participants[0].address
const tx = await eas.attest({
schema: commit.verifierData.schemaUID,
data: {
recipient: participantAddress
}
})
// This participant can now verify and claim their reward
await commit.verify()
await commit.claim()
```
## Contracts
### Building Custom Verifiers
You can easily build your own Verifier contracts to customize how participants are eligible for rewards.
```solidity
contract CustomVerifier is IVerifier {
function verify(
address participant,
bytes calldata data, // Initialized verifierData
bytes calldata userdata // User-provided data from frontend (i.e. a signature)
) external view returns (bool) {
// TODO: Implement logic
return true;
}
}
```
```ts
// Initialize the SDK and extend with your custom verifier (see @commit/sdk/verifiers.index.ts for more details)
const commitClient = new CommitSDK({ chainId, walletClient }).extend({
verifiers: {
CustomVerifier: {
name: "CustomVerifier",
description: "Testing a custom Verifier",
contracts: {
[base.id]: "0xVerifierAddress
},
data: (data) => "0x", // Encode verifierData
userdata: (data) => "0x", // Encode userdata
decode: (data: Hex) => null // Decode verifierData
},
},
});
```
---
### Older sketches below
---
Sketching some ideas for what future versions of the protocol could look like. Mostly focused on:
- A Commit is a deployed ERC721 contract. When participants join, an NFT is minted.
- Participants can verify their commits. Currently three methods:
- TokenVerifier - participant must hold x amount of y tokens (ERC20 or ERC721)
- EASVerifier - checks if participant holds an attestation for schemaUID by attester
- SignatureVerifier - verifies if the signature is valid
- TokenUtils to normalize ETH and ERC20 transfers
This upgrade enables us to create services that verify users. We can query the indexer for all the verified participants and let the Commit creator use these to resolve the winners.
For example:
- Scanning a QR code at an event generates a signature that is verified on-chain
- In a local fitness group, the commit creator marks attendees in the app and creates attestations as proof (which are later resolved in the contract)
- A user can send Strava proofs to a backend service that creates an Attestation
### Proposed Interface
```solidity
interface ICommit {
// Events (TODO)
// Structs
struct Config {
// Commit details
string metadataURI; // { title, image, description, tags } - Use a standard NFT format
// Commit period
uint256 joinBefore; // or startsAt ?
uint256 verifyBefore; // or endsAt ? (is this superseded by milestones?)
// Verifier
address verifier; // Verifier strategy contract (EAS, Signature, Token)
bytes verifierData; // Passed to Verifier contract (schemaUID, attester, tokenAddress etc)
// Stake and fees
address token;
uint256 stake; // Cost to join Commit
uint256 fee; // Creator fee
// Referals
bytes32 clientId; // (Optional) Clients can earn a fee on stakes
address referer; // (Optional) Referer can earn a fee on stakes
uint256 maxParticipants; // (Optional) Limit how many participants can join (this just sets the ERC721 supply)
Milestone[] milestones // (Optional) Define milestones
}
struct Milestone {
uint256 deadline; // Timestamp when participant must verify before
string metadataURI; // (Optional) Details about milestone
}
mapping(address => bool) private participants;
mapping(address => bool) private verified;
// Commits can be funded with additional tokens to be rewarded to verified participants
function fund() external {}
// Participants can join a Commit by staking funds (mints NFT)
function join() external {}
// Participants can verify they've completed a commit (use simulate to check status without paying gas)
// Sets verified[address] = true (or verified[address][milestoneIndex] = true)
function verify() external returns (bool) {}
// Verified participants can claim their share of the rewards (stake + rewards)
// Verifies each milestone?
function claim() external {}
// Creator can claim fees at any point during the Commit cycle
function claimFees() onlyOwner external {}
// Creator can cancel a commitment and return stakeAmounts to participants
function cancel() onlyOwner external {}
}
interface IVerifier {
function verify(
address participant,
bytes calldata data, // Initialized Commit.Config.verifierData
bytes calldata userdata // User-provided data from frontend (i.e. signature)
) external view returns (bool);
}
// Commit ERC721 - each participant receives a minted NFT
contract Commit is ICommit, ERC721 {}
contract CommitRegistry {
// Deploys a Commit ERC721 contract
function create(Commit.Config config) external {}
// Partners / Clients can register their payout address + fee and receive tokens when participants join
function registerClient(ClientConfig config) external {}
function getClient(bytes32 clientId) external {}
}
```
### Use-cases / Commit Archetypes
#### Open Commits
- Participants stake tokens to join
- Participants can Verify they've fulfilled their commitment
- EAS Attestation (some service mints an attestation to their address)
- Signature (some service generates a signature for them)
- Token holdings (Commit contract checks if participant holds a balance over x of ERC20 or 721 token)
`events, one-time fitness activity`
#### Milestone Commits
- Participants can submit multiple proofs (often before defined timestamps)
`grants, multi fitness activities`
*How can we describe in verifier logic: Run 5 times in 7 days?*
- Create a Commit with `commit.milestones = [deadline1, deadline2, ...}`
- With the EASVerifier we can:
- Get all attestations for recipient (by a specific attester and schemaUID) `EASIndexer.getSchemaAttesterRecipientAttestationUIDs`
- Compare each `attestation.time` to the milestones
- `attestation[i].time < milestones[i].deadline`
- Does this happen in the claim rewards function?
- Frontends can query EAS GraphlQL API to show the milestones completed
#### Personal Commits
> one research item is also ‘personal commits’ — this could be applied to personal goals, org/DAO goals, etc. this won’t be ‘group commits’, people won’t be able to join — only creator commits, and perhaps others could speculate on top
Perhaps the simplest solution:
- Creator can create a Commit with the config `maxParticipants: 1`
---
### User Flows
- A Commit is created with a config:
- description (what are participants commiting to?)
- start and end date (during what time period is it active? rewards can be claimed after)
- optional participant limit
- stake token and amount (what's the stake to participate?)
- a verifier contract with rules for how participants prove their commit
- creator reward (optional amount for creator when participants join)
- referer address (optional address to reward referer)
- Commit is shared (via link or QR code)
- Participants can join by sending the stake amount and receive a dynamic NFT
- Participants provide proof during the commit period
- Funders can fund Commit with tokens that will be distributed to the verified participants
- Either via ERC20 direct transfer to contract address or wallet calling `commit.fund`. Both must be supported.
- Keeps track of balance with: `commit.token.balanceOf(address(this))`
- The NFT contains information about verified proofs that has been submitted
- dynamically generates metadata `https://commit.wtf/api/commit/[commitAddress]/[tokenId]`
- At the end of the commit period, successfully committed participants can claim their reward (total stake pool / verified participants count)
- Creator can claim their reward fee at any time during the commit cycle
---
#### Commits as NFTs
- each Commit can have a metadataURI (like NFTs do). This can point to an API that dynamically generates an image of the commit and its resolution status (see above)
- Commits can be listed on NFT marketplaces
- Commits can be bought and sold (and also soulbound to prevent trading)
- Each Commit NFT is a proof of comitting that sits in your account
### Research areas
#### Milestone-based commits
- Fitness milestones: Create commits to run 5 times / week
- Roadmap milestones: Ship x month 1, Ship y month 2, Ship z month 3
##### What if NFTs are minted on Verify instead of Join?
- Each NFT represents a fulfilled milestone
- NFTs for participants can be queried and the timestamps confirm when proof was submitted
- For a month-long commit with daily activities that means 30 NFTs for one commit.
##### Emit Verified events for every proof?
- These can easily be queried in the indexer
- The NFT metadata is dynamic
- https://commit.wtf/api/commit/[commitAddress]/[tokenId]
- Backend API returns `metadata.json`
- Commit name
- Description
- Start/End dates
- Image
- Verifications (`{ date: Date }[]`)
- With this data we can see how many proofs in one week or month and when they happened
### Smart Contracts
This is not final in any way but see it more as a concept to convey ideas.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Clones.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Commit is ERC721 {
using TokenUtils for address;
struct CommitConfig {
uint256 joinDeadline; // Rename to startsAt and endsAt?
uint256 resolveDeadline;
address verifier; // Verifier contract address
bytes verifierData; // Data to be used by the verifier
address token; // Token used for staking (address(0) for native ETH)
uint256 stakeAmount; // Amount to stake
uint256 creatorFee; // Fee set by the creator
string metadataURI; // IPFS hash of metadata
}
CommitConfig public config;
address public protocol;
address public creator;
mapping(address => bool) public participants;
mapping(address => bool) public rewardsClaimed;
uint256 public tokenIdCounter;
address[] public participantList;
address[] public winnerList;
bool public resolved;
bool public creatorFeeClaimed;
event Joined(address indexed participant, uint256 tokenId);
event Resolved(address[] winners);
event RewardClaimed(address indexed participant, uint256 amount);
event CreatorFeeClaimed(uint256 amount);
modifier onlyProtocol() {
require(msg.sender == protocol, "Not authorized");
_;
}
modifier onlyCreator() {
require(msg.sender == creator, "Not authorized");
_;
}
constructor() ERC721("CommitmentNFT", "COMMIT") {}
function initialize(
CommitConfig memory _config,
address _protocol,
address _creator
) external {
require(protocol == address(0), "Already initialized");
config = _config;
protocol = _protocol;
creator = _creator;
}
// Could implement ERC20Permit here to skip the Approve call
function join() external payable {
require(block.timestamp < config.joinDeadline, "Join deadline passed");
require(!participants[msg.sender], "Already joined");
uint256 totalStake = config.stakeAmount + config.creatorFee;
uint256 protocolFee = protocol.getJoinFee();
if (config.token == address(0)) {
require(msg.value == totalStake + protocolFee, "Incorrect ETH amount");
} else {
require(msg.value == protocolFee, "Incorrect ETH amount for protocol fee");
}
// Transfer protocol fee
payable(protocol.feeAddress()).transfer(protocolFee);
// Transfer client fee
uint256 clientShare = 0;
if (config.clientId) {
(address clientAddress, uint256 clientFee) = protocol.getClientFee(config.clientId);
clientShare = totalStake * clientFee / BASIS_POINTS;
config.token.safeTransferFrom(clientShare);
}
// Transfer stake
config.token.safeTransferFrom(msg.sender, address(this), totalStake - clientShare);
participants[msg.sender] = true;
participantList.push(msg.sender);
// Mint NFT to participant
uint256 tokenId = ++tokenIdCounter;
_mint(msg.sender, tokenId);
emit Joined(msg.sender, tokenId);
}
function verify(address participant, bytes32 data) external returns {
require(participants[participant], "Not a participant");
isValid = IVerifier(config.verifier).verify(
participant,
data, // Data provided by the participant (could be signatures or other proofs)
config.verifierData // This includes creator-specific data such as min. token holdings for an erc20 address or EAS schema and attester address
);
require(isValid, "Verification failed");
verified[participant] = true;
emit Verified(participant);
}
function resolve() external onlyCreator {
require(block.timestamp > config.resolveDeadline,"Resolve deadline not reached");
require(!resolved, "Already resolved");
address[] memory winners = _determineWinners();
winnerList = winners;
resolved = true;
emit Resolved(winners);
}
function claimReward() external {
require(resolved, "Not resolved");
require(participants[msg.sender], "Not a participant");
require(verified[msg.sender], "Not verified");
require(!rewardsClaimed[msg.sender], "Reward already claimed");
uint256 reward = _calculateReward();
rewardsClaimed[msg.sender] = true;
config.token.safeTransfer(msg.sender, reward);
emit RewardClaimed(msg.sender, reward);
}
function claimCreatorFee() external onlyCreator {
require(resolved, "Not resolved");
require(!creatorFeeClaimed, "Creator fee already claimed");
uint256 totalFee = config.creatorFee * participantList.length;
creatorFeeClaimed = true;
config.token.safeTransfer(creator, totalFee);
emit CreatorFeeClaimed(totalFee);
}
function _determineWinners() internal view returns (address[] memory) {
uint256 count = participantList.length;
address[] memory winnersTemp = new address[](count);
uint256 index = 0;
for (uint256 i = 0; i < count; i++) {
address participant = participantList[i];
bool isValid = IVerifier(config.verifier).verify(
participant,
config.verifierData
);
if (isValid) {
winnersTemp[index] = participant;
index++;
}
}
// Resize the array to actual winner count
assembly {
mstore(winnersTemp, index)
}
return winnersTemp;
}
function _calculateReward() internal view returns (uint256) {
uint256 totalStake = config.stakeAmount * participantList.length;
uint256 totalCreatorFee = config.creatorFee * participantList.length;
uint256 totalRewardPool = totalStake - totalCreatorFee;
uint256 winnerCount = winnerList.length;
if (winnerCount == 0) {
return 0;
}
return totalRewardPool / winnerCount;
}
}
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Clones.sol";
contract CommitProtocol {
struct ClientConfig {
bytes32 id; // Hashed name - keccak256(abi.encodePacked("base"))
address account; // Where fees will be sent
address owner; // Who can update the config
uint256 share; // Percentage of stake to transfer
}
address public owner;
address public feeAddress;
address public implementation; // Address of the Commit contract implementation
mapping(address => bool) public approvedTokens;
mapping(bytes32 => ClientConfig) public clients;
event CommitCreated(
address indexed commitAddress,
Commit.CommitConfig config
);
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}
constructor(address _implementation, address _feeAddress) {
owner = msg.sender;
implementation = _implementation;
feeAddress = _feeAddress;
}
function createCommitment(
Commit.CommitConfig calldata config
) external payable {
require(approvedTokens[config.token], "Token not approved");
// Transfer protocol creation fee
uint256 protocolFee = getCreateFee();
require(msg.value == protocolFee,"Incorrect ETH amount for protocol fee");
payable(feeAddress).transfer(protocolFee);
address clone = Clones.clone(implementation);
Commit(clone).initialize(config, feeAddress, msg.sender);
emit CommitCreated(clone, config);
}
function setApprovedToken(address token, bool approved) external onlyOwner {
approvedTokens[token] = approved;
}
function setImplementation(address _implementation) external onlyOwner {
implementation = _implementation;
}
function setFeeAddress(address _feeAddress) external onlyOwner {
feeAddress = _feeAddress;
}
function getCreateFee() public pure returns (uint256) {
return 0.001 ether;
}
function getJoinFee() public pure returns (uint256) {
return 0.001 ether;
}
function registerClient(ClientConfig _client) {
// TODO: add functionality for updating (with owner check)
clients[_client.id] = _client;
}
function getClient(bytes32 clientId) public pure returns (address account, uint256 share) {
ClientConfig client = clients[clientId];
return (client.account, client.share);
}
}
interface IVerifier {
function verify(
address account,
bytes calldata userData,
bytes calldata creatorData
) external view returns (bool);
}
// Verifies token holdings
contract TokenBalanceVerifier is IVerifier {
function verify(
address account,
bytes calldata creatorData
) external view override returns (bool) {
(address token, uint256 minBalance) = abi.decode(
creatorData,
(address, uint256)
);
// ERC20 and ERC721 share the same interface for balanceOf
return token.balanceOf(account) >= minBalance;
}
}
// A trusted service can create attestations to users that are verified here
contract EASVerifier is IVerifier {
function verify(
address account,
bytes calldata userData,
bytes calldata creatorData
) external view override returns (bool) {
(bytes32 schemaUID, address attester) = abi.decode(creatorData, (bytes32, address));
// Return true if the account has a valid attestation
return
EASRegistry.getSchemaAttesterRecipientAttestationUIDCount(
schemaUID,
attester,
_account
) > 0;
}
}
// A signature can be created off-chain by a trusted service
contract SignatureVerifier is IVerifier {
function verify(
address account,
bytes calldata userData,
bytes calldata creatorData
) external view override returns (bool) {
(address signer) = abi.decode(creatorData, (address));
(bytes32 hash, bytes signature) = abi.decode(userData, (address));
return isValidERC1271SignatureNow(signer, hash, signature);
}
}
// Normalizes ETH and ERC20 transfers
library TokenUtils {
/**
* @dev Transfers tokens or ETH to a recipient.
* @param token The address of the token to transfer. Use address(0) for native ETH.
* @param to The recipient address.
* @param amount The amount to transfer.
*/
function safeTransfer(address token, address to, uint256 amount) internal {
if (token == address(0)) {
// Transfer native ETH
(bool success, ) = payable(to).call{value: amount}("");
require(success, "ETH transfer failed");
} else {
// Transfer ERC20 token
require(IERC20(token).transfer(to, amount), "Token transfer failed");
}
}
/**
* @dev Transfers tokens or ETH from a sender to a recipient.
* @param token The address of the token to transfer. Use address(0) for native ETH.
* @param from The sender address.
* @param to The recipient address.
* @param amount The amount to transfer.
*/
function safeTransferFrom(address token, address from, address to, uint256 amount) internal {
if (token == address(0)) {
// For ETH, the 'from' address must be msg.sender
require(from == msg.sender, "Sender mismatch for ETH transfer");
require(msg.value == amount, "Incorrect ETH amount sent");
// ETH is already received with the call, so no action is needed
} else {
// Transfer ERC20 token from 'from' to 'to'
require(IERC20(token).transferFrom(from, to, amount), "Token transferFrom failed");
}
}
}
```
### Services
- AttestationService - creates an EAS attestation for a participant
- Enter Venue
- Scan a QR code and receive an attestation (qr needs to be refreshed continuously)
- Github
- Check Commits for repo
- SignatureService - generates a signature to be sent to SignatureVerifier
### Use Cases
#### Events
- Create Commit
- Participants stake to join event
- Page with QR code can be loaded on a device at venue
- Participants scan QR when arriving to the venue
- VerificationPage opens (`/commit/[id]/verify?code=[code]`)
- Connect Wallet
- Sign Auth message
- Send to backend (`POST /api/commit/[id]/verify?code`)
- Return signature
- Call contract `commit.verify(signature)`