Repository navigation
feat(tipping): Nodle Tipping contract (v1 deployed, v2 on branch) #134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
7a73cd9
feat(tipping): immutable Tipping contract with on-chain recipient res…
Douglasacost 0e8d8bd
fix(tipping): zero-amount guard, single registration, fee-recipient s…
Douglasacost f2354a9
test(tipping): full suite (31 tests, 97% line coverage), local only
Douglasacost 18889a8
feat(tipping): fork test, deploy and seed scripts, EraVM gas check, c…
Douglasacost 79662b7
feat(tipping): agnostic signature-authorized tipping (any ERC-20, any…
Douglasacost 9f6438c
test(tipping): rename mock field for spell check
Douglasacost 6d7416b
Merge branch 'main' into feat/tipping
Douglasacost 9a9bfa7
ops(tipping): zksync deploy wrapper that moves the L1-only files asid…
Douglasacost f97b7d4
ops(verify): inline Foundry remappings into imports; the explorer now…
Douglasacost 4fd9816
feat(tipping): drop Pausable (review decision 2026-10-28)
Douglasacost f1a2649
feat(tipping): v2 resolves the recipient on-chain; no backend signature
Douglasacost 9850026
test(tipping): rename for the spell check
Douglasacost c8aa7a9
feat(tipping): tip() takes the recipient and verifies it against the NFT
Douglasacost e56bee7
feat(tipping): fees accrue in the contract; withdrawFees(token) sends…
Douglasacost f3399d6
docs(tipping): contract title and description for v2
Douglasacost 779c63a
refactor(tipping): review renames: NFTStandard, isValidTipRecipient p…
Douglasacost 73a6f93
refactor(tipping): _pay → _transferTipWithFee; fuzz asserts the balan…
Douglasacost 4eabf23
fix(tipping): review fixes from Copilot
Douglasacost 0a78683
test(tipping): cover the ERC-165 refusal and the no-proof fallthrough…
Douglasacost File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| #!/usr/bin/env bash | ||
| # Deploys the immutable Tipping contract on zkSync (Sepolia or mainnet) with forge --zksync. | ||
| # zksolc cannot compile the L1-only SwarmRegistryL1Upgradeable (EXTCODECOPY), so the L1-only | ||
| # files are moved aside for the compile and restored on exit, exactly as | ||
| # ops/deploy_collection_factory_zksync.sh and ops/upgrade_collection_factory_zksync.sh do. | ||
| # | ||
| # Usage: | ||
| # DEPLOYER_PRIVATE_KEY=0x… N_TIPPING_ADMIN=0x… \ | ||
| # ops/deploy_tipping_zksync.sh <testnet|mainnet> [--broadcast] | ||
| # | ||
| # Without --broadcast the script simulates only. The broadcast JSON lands in | ||
| # broadcast/DeployTippingZkSync.s.sol/<chainId>/run-latest.json; verify afterwards with | ||
| # python3 ops/verify_zksync_contracts.py --broadcast <that file>. | ||
| set -euo pipefail | ||
| cd "$(dirname "$0")/.." | ||
|
|
||
| NETWORK="${1:-}"; BROADCAST="${2:-}" | ||
| case "$BROADCAST" in | ||
| ""|--broadcast) ;; | ||
| *) echo "usage: $0 <testnet|mainnet> [--broadcast] (second argument must be exactly --broadcast or omitted)"; exit 1 ;; | ||
| esac | ||
| case "$NETWORK" in | ||
| testnet) RPC_URL="${L2_RPC:-https://sepolia.era.zksync.dev}" ;; | ||
| mainnet) RPC_URL="${L2_RPC:-https://mainnet.era.zksync.io}" ;; | ||
| *) echo "usage: $0 <testnet|mainnet> [--broadcast]"; exit 1 ;; | ||
| esac | ||
| : "${DEPLOYER_PRIVATE_KEY:?}" "${N_TIPPING_ADMIN:?}" | ||
| [[ "$DEPLOYER_PRIVATE_KEY" == 0x* ]] || export DEPLOYER_PRIVATE_KEY="0x${DEPLOYER_PRIVATE_KEY}" | ||
| if [[ "$NETWORK" == "mainnet" && "$BROADCAST" == "--broadcast" && "${CONFIRM_MAINNET:-}" != "YES" ]]; then | ||
| echo "mainnet broadcast requires CONFIRM_MAINNET=YES"; exit 1 | ||
| fi | ||
|
|
||
| L1_BACKUP_DIR="/tmp/rollup-l1-backup-tipping" | ||
| restore_l1_contracts() { | ||
| [ -d "$L1_BACKUP_DIR" ] || return 0 | ||
| [ -f "$L1_BACKUP_DIR/SwarmRegistryL1Upgradeable.sol" ] && mv "$L1_BACKUP_DIR/SwarmRegistryL1Upgradeable.sol" src/swarms/ | ||
| [ -f "$L1_BACKUP_DIR/SwarmRegistryL1.t.sol" ] && mv "$L1_BACKUP_DIR/SwarmRegistryL1.t.sol" test/ | ||
| [ -d "$L1_BACKUP_DIR/upgrade-demo" ] && mv "$L1_BACKUP_DIR/upgrade-demo" test/ | ||
| [ -f "$L1_BACKUP_DIR/DeploySwarmUpgradeable.s.sol" ] && mv "$L1_BACKUP_DIR/DeploySwarmUpgradeable.s.sol" script/ | ||
| [ -f "$L1_BACKUP_DIR/UpgradeSwarm.s.sol" ] && mv "$L1_BACKUP_DIR/UpgradeSwarm.s.sol" script/ | ||
| rm -rf "$L1_BACKUP_DIR" | ||
| echo "L1-only contracts restored" | ||
| } | ||
| move_l1_contracts() { | ||
| [ -d "$L1_BACKUP_DIR" ] && restore_l1_contracts | ||
| mkdir -p "$L1_BACKUP_DIR" | ||
| [ -f src/swarms/SwarmRegistryL1Upgradeable.sol ] && mv src/swarms/SwarmRegistryL1Upgradeable.sol "$L1_BACKUP_DIR/" | ||
| [ -f test/SwarmRegistryL1.t.sol ] && mv test/SwarmRegistryL1.t.sol "$L1_BACKUP_DIR/" | ||
| [ -d test/upgrade-demo ] && mv test/upgrade-demo "$L1_BACKUP_DIR/" | ||
| [ -f script/DeploySwarmUpgradeable.s.sol ] && mv script/DeploySwarmUpgradeable.s.sol "$L1_BACKUP_DIR/" | ||
| [ -f script/UpgradeSwarm.s.sol ] && mv script/UpgradeSwarm.s.sol "$L1_BACKUP_DIR/" | ||
| echo "L1-only contracts moved aside for the zksync compile" | ||
| } | ||
| trap restore_l1_contracts EXIT | ||
| move_l1_contracts | ||
|
|
||
| echo "network=$NETWORK rpc=$RPC_URL admin=$N_TIPPING_ADMIN broadcast=${BROADCAST:-no}" | ||
| forge script script/DeployTippingZkSync.s.sol --rpc-url "$RPC_URL" --zksync --skip test ${BROADCAST:+--broadcast --slow} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // SPDX-License-Identifier: BSD-3-Clause-Clear | ||
| pragma solidity ^0.8.26; | ||
|
|
||
| import {Script, console} from "forge-std/Script.sol"; | ||
|
|
||
| import {Tipping} from "../src/tipping/Tipping.sol"; | ||
|
|
||
| /** | ||
| * @title DeployTippingZkSync | ||
| * @notice Deploys the immutable `Tipping` contract (nft-feed-indexer design §6.2, meta#242). | ||
| * No proxy, no initializer: the constructor sets everything, so a mistaken input | ||
| * means a new deployment, not an upgrade. Fee starts at 0 with the admin as recipient. | ||
| * | ||
| * Usage: ops/deploy_tipping_zksync.sh <testnet|mainnet> [--broadcast] | ||
| * (moves the L1-only swarm files aside; plain `forge script --zksync` fails to compile) | ||
| * | ||
| * Environment Variables: | ||
| * - DEPLOYER_PRIVATE_KEY: Private key with ETH for gas (gets no role). | ||
| * - N_TIPPING_ADMIN: Multisig that will hold DEFAULT_ADMIN_ROLE (fee, fee recipient). | ||
| */ | ||
| contract DeployTippingZkSync is Script { | ||
| address public tipping; | ||
|
|
||
| function run() external { | ||
| uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); | ||
| address admin = vm.envAddress("N_TIPPING_ADMIN"); | ||
| require(admin != address(0), "N_TIPPING_ADMIN is zero"); | ||
|
|
||
| console.log("=== Deploying Tipping on ZkSync ==="); | ||
| console.log("Admin:", admin); | ||
| console.log(""); | ||
|
|
||
| vm.startBroadcast(deployerPrivateKey); | ||
| tipping = address(new Tipping(admin)); | ||
| vm.stopBroadcast(); | ||
|
|
||
| console.log("Tipping:", tipping); | ||
| console.log(""); | ||
| console.log("Next: record the address and deploy block in the indexer (chains.ts `tipping`)."); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| // SPDX-License-Identifier: BSD-3-Clause-Clear | ||
| pragma solidity ^0.8.26; | ||
|
|
||
| /** | ||
| * @title ITipping | ||
| * @notice Interface of Nodle Tipping v2: tip an NFT post or an account with any ERC-20. | ||
| * A post tip names its recipient and the contract verifies the claim against the NFT | ||
| * (ERC-721 owner; ERC-1155 collection owner role, Ownable owner or ERC-2981 receiver). | ||
| * No backend signature, no allowlist, no pause. Keep the event surface stable: the | ||
| * Nodle feed indexer consumes it. | ||
| */ | ||
| interface ITipping { | ||
| enum NFTStandard { | ||
| ERC721, | ||
| ERC1155 | ||
| } | ||
|
|
||
| /// @notice A post was tipped. `amount` is gross (what the tipper gave); `fee` went to the fee recipient. | ||
| event Tipped( | ||
| address indexed tipper, | ||
| address indexed collection, | ||
| uint256 indexed tokenId, | ||
| address token, | ||
| address recipient, | ||
| uint256 amount, | ||
| uint256 fee | ||
| ); | ||
| /// @notice An account was tipped directly, not tied to a post. | ||
| event AccountTipped(address indexed tipper, address indexed to, address token, uint256 amount, uint256 fee); | ||
| event FeeUpdated(uint16 feeBps); | ||
| event FeeRecipientUpdated(address indexed feeRecipient); | ||
| /// @notice Accrued fees in `token` were sent to the fee recipient. | ||
| event FeesWithdrawn(address indexed token, address indexed to, uint256 amount); | ||
|
|
||
| error ZeroAddress(); | ||
| error ZeroAmount(); | ||
| error SelfTip(); | ||
| error NotAContract(address collection); | ||
| /// @notice The NFT does not exist for the given standard (ownerOf reverted, or an ERC-1155 id with zero supply). | ||
| error InvalidToken(address collection, uint256 tokenId); | ||
| /// @notice The named recipient is not the NFT's owner (ERC-721) nor the collection's owner / royalty receiver (ERC-1155). | ||
| error NotRecipient(address collection, uint256 tokenId, address recipient); | ||
| error FeeTooHigh(uint16 feeBps); | ||
| error InvalidFeeRecipient(address recipient); | ||
|
|
||
| /// @notice Tip the NFT `tokenId` of `collection`: `recipient` is verified against the NFT, then paid `amount` of `token`. | ||
| function tip( | ||
| address collection, | ||
| NFTStandard standard, | ||
| uint256 tokenId, | ||
| address recipient, | ||
| address token, | ||
| uint256 amount | ||
| ) external; | ||
| /// @notice Tip an account the caller names. | ||
| function tipAccount(address token, address to, uint256 amount) external; | ||
|
|
||
| /// @notice Whether `tip` would accept `recipient` for this NFT; reverts for a non-contract or an invalid token, like `tip`. | ||
| function isValidTipRecipient(address collection, NFTStandard standard, uint256 tokenId, address recipient) | ||
| external | ||
| view | ||
| returns (bool); | ||
| /// @notice Send everything this contract holds in `token` (accrued fees) to the fee recipient. Anyone may call. | ||
| function withdrawFees(address token) external; | ||
|
|
||
| function feeBps() external view returns (uint16); | ||
| function feeRecipient() external view returns (address); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid, fixed:
Tippingadded toCONTRACT_SOURCE_MAPandDeployTippingZkSync.s.soltoBROADCAST_CONTRACT_SEQUENCE.