The on-chain component of Opentip. Written in Solidity, deployed on Base.
Solidity
^0.8.26
License
MIT
Tokens
USDC · ETH · OAR
Fee
5% (500 bps)
The contract inherits from four OpenZeppelin libraries:
Ownable2Step
Two-step ownership transfer for safety
Pausable
Emergency pause/unpause mechanism
ReentrancyGuard
Protection against reentrancy attacks
EIP712
Off-chain typed signature verification
| Constant | Value | Description |
|---|---|---|
| MAX_FEE_BPS | 1000 | Maximum fee: 10% |
| MAX_REPO_ID_LENGTH | 200 | Maximum characters for a repo ID |
| Function | Parameters | Returns | Modifier | Description |
|---|---|---|---|---|
| registerRepo | repoId, payoutAddress, expiry, nonce, signature | — | whenNotPaused | Register a repo with an EIP-712 signed permit. Validates format, expiry, and signature. |
| updatePayoutAddress | repoId, newAddress | — | whenNotPaused | Rotate the payout wallet for a registered repo. Only callable by current payout address. |
| receiveTipEth | repoId | — | payable, nonReentrant, whenNotPaused | Accept an ETH tip. Splits fee, credits pending balance in ETH. |
| receiveTip | repoId, token, amount | — | nonReentrant, whenNotPaused | Accept an ERC-20 tip (USDC, OAR, etc). Pulls tokens from msg.sender, splits fee, credits pending balance. |
| claimAll | repoId | — | nonReentrant, whenNotPaused | Withdraw all pending tokens (ETH + ERC-20) in a single transaction. Only callable by the payout address. |
| Function | Parameters | Returns | Modifier | Description |
|---|---|---|---|---|
| pause | — | — | onlyOwner | Emergency pause. Blocks register, update, tip, and claim. |
| unpause | — | — | onlyOwner | Resume operations after a pause. |
| setFeeBps | newFeeBps | — | onlyOwner | Update platform fee. Capped at MAX_FEE_BPS (1000 = 10%). |
| setTreasuryAddress | newTreasury | — | onlyOwner | Change the treasury address. |
| withdrawTreasury | token, amount | — | onlyOwner, nonReentrant | Withdraw accumulated fees for a specific token. |
| setRegistrarSigner | newSigner | — | onlyOwner | Rotate the EIP-712 registrar signer. |
| adminReassignPayout | repoId, newAddress | — | onlyOwner | Emergency recovery: reassign payout address. Developer must first link and verify the new wallet in their dashboard. |
| adminMigrateRepo | repoId, payoutAddress | — | onlyOwner | Migrate a v1 repo to v2. Disabled after migrationDeadline. |
| addToken | token, decimals | — | onlyOwner | Register a new ERC-20 token for tipping. |
| removeToken | token | — | onlyOwner | Remove a token from accepting new tips. Existing pending balances remain claimable via everAcceptedTokens. |
| sweepStrayEth | to | — | onlyOwner, nonReentrant | Recover ETH sent directly to the contract outside receiveTipEth. |
| setMigrationDeadline | deadline | — | onlyOwner | Set a deadline after which adminMigrateRepo is disabled. Pass 0 to re-enable. |
| Function | Parameters | Returns | Modifier | Description |
|---|---|---|---|---|
| getPendingBalance | repoId, token | uint256 | view | Amount of a specific token available to claim for a repo. |
| getPayoutAddress | repoId | address | view | The wallet that can claim tips for a repo. |
| getTotalTipped | repoId, token | uint256 | view | Lifetime gross tip amount for a repo in a specific token. |
| getTotalTipCount | repoId | uint256 | view | Lifetime tip count for a repo. |
| feeBps | — | uint256 | view | Current platform fee in basis points. |
| treasuryBalances | token | uint256 | view | Accumulated unwithdrawn fees for a token. |
| isRegistered | repoId | bool | view | Whether a repo has been registered. |
| Event | Parameters | Description |
|---|---|---|
| RepoRegistered | repoId, payoutAddress (indexed), timestamp | Emitted when a new repo is registered. |
| PayoutAddressUpdated | repoId, oldAddress, newAddress (indexed) | Emitted when a payout wallet is rotated. |
| TipReceived | tipper (indexed), repoId, token, amount, feeAmount, timestamp | Emitted on every tip (ETH or ERC-20). |
| Claimed | repoId, payoutAddress (indexed), token, amount, timestamp | Emitted when a developer claims tips. |
| TreasuryWithdrawn | to (indexed), token, amount, timestamp | Emitted when treasury funds are withdrawn. |
| FeeUpdated | oldFeeBps, newFeeBps | Emitted when the platform fee changes. |
| TreasuryAddressUpdated | oldTreasury (indexed), newTreasury (indexed) | Emitted when the treasury address changes. |
| RegistrarSignerUpdated | oldSigner (indexed), newSigner (indexed) | Emitted when the registrar signer is rotated. |
| TokenAdded | token (indexed), decimals | Emitted when a new token is registered for tipping. |
| TokenRemoved | token (indexed) | Emitted when a token is removed. |
The platform fee is expressed in basis points (bps). 500 bps = 5%. The fee is capped at 1000 bps (10%) and enforced in both the constructor and setFeeBps.
receiveTipEth() (ETH) or receiveTip() (ERC-20)amount * feeBps / 10000treasuryBalance[token]pendingBalance[token]claimAll() to withdraw all tokenswithdrawTreasury(token, amount) to collect fees per tokenRegistration uses EIP-712 typed signatures to prevent squatting. The process:
Register typed struct with the registrar keyregisterRepo()The signature expires after 5 minutes (backend) with a hard cap of 10 minutes enforced on-chain. This limits the window for replay attacks while giving ample time for normal transaction submission.
/ separator/{
name: "Opentip",
version: "2",
chainId: 8453,
verifyingContract: "0x..."
}All state-changing external functions (receiveTip, receiveTipEth, claimAll, withdrawTreasury) use nonReentrant.
The owner can pause the contract in an emergency. Paused contract blocks registration, tip updates, tips, and claims.
Uses Ownable2Step — ownership transfer requires the new owner to accept. Prevents accidental transfer to a wrong address.
All functions update state before making external calls (USDC transfers), following the CEI pattern.
ETH sent outside receiveTipEth() is tracked separately as strayEth and can be swept by the owner via sweepStrayEth().
Removing a token stops new tips but existing pending balances remain claimable. claimAll() iterates over everAcceptedTokens, a permanent record of all tokens that were ever added.
Registration signatures are capped at 10 minutes maximum validity. The backend signs with a 5-minute expiry.
The full contract source is available at contracts/src/OpentipV2.sol in the repository.