diff --git a/ADMINISTRATION.md b/ADMINISTRATION.md index 08b98756..228cebed 100644 --- a/ADMINISTRATION.md +++ b/ADMINISTRATION.md @@ -47,7 +47,7 @@ ## Operational notes - Validate game timings, tier setup, fee routing, and attestation settings before launch. -- If `JBProjects` has a creation fee, include the exact native-token fee when calling `launchGameWith()`. +- If `JBProjects` has a creation fee, include the exact native-token fee when calling `launchGameWith()` directly or through the configured trusted forwarder. - Treat `launchGameWith()` as the real admin commitment. - During scoring, follow the submission, attestation, and ratification flow rather than looking for discretionary overrides. - Use `triggerNoContestFor()` only when the game has actually entered the documented no-contest condition. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5ca5edc5..f2f4a5a3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -21,7 +21,7 @@ | Module | Responsibility | Notes | | --- | --- | --- | -| `DefifaDeployer` | Launches games, sets phased rulesets, clones hooks, initializes governance, and fulfills commitments | Launch-time and completion-time runtime surface | +| `DefifaDeployer` | Launches games, sets phased rulesets, clones hooks, resolves ERC-2771 launch callers, initializes governance, and fulfills commitments | Launch-time and completion-time runtime surface | | `DefifaHook` | NFT minting, delegation, game-phase-aware cash-out behavior, and completion claims | Main game-facing runtime hook | | `DefifaGovernor` | Scorecard submission, attestation weighting, quorum, grace periods, and ratification | Governance surface | | `DefifaHookLib` | Shared validation and weight math extracted from the hook | Bytecode-management helper | @@ -41,9 +41,10 @@ ```text creator -> deployer validates mint/refund/start timings + -> deployer resolves the ERC-2771 caller and advertises that account as the creation-fee payer -> deployer predicts the game project ID and clones a game hook deterministically -> deployer builds phased rulesets and optional fee splits - -> deployer advertises the resolved fee payer, then reserves the game project via createFor + -> deployer reserves the game project via createFor -> controller launches the project -> governor is initialized for the game -> hook ownership and project ownership are transferred into the intended long-term shape diff --git a/CHANGELOG.md b/CHANGELOG.md index 77af8348..5043dffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,5 +103,5 @@ Shared ABI artifacts checked with no ABI item changes: - Replace every `DefifaDelegate` reference with `DefifaHook` and regenerate ABI types. - Re-check any scorecard, attestation, or cash-out indexing code against the V6 events. V5 scorecard assumptions are not selector- or payload-stable. - Do not depend on `DefifaProjectOwner` in V6 deployments. -- `DefifaDeployer` now implements `IJBPayerTracker`. While forwarding a project-creation fee to `JBProjects.createFor`, it advertises the resolved fee payer (the `launchGameWith` caller) through the transient `originalPayer` getter, so a `pay`-routing fee receiver credits the player who paid rather than the deployer. Regenerate ABI types to pick up the added `originalPayer()` getter. +- `DefifaDeployer` now implements `IJBPayerTracker` and `ERC2771Context`. Its constructor accepts a `trustedForwarder`, and while forwarding a project-creation fee to `JBProjects.createFor`, it advertises the resolved fee payer (the `launchGameWith` ERC-2771 caller) through the transient `originalPayer` getter, so a `pay`-routing fee receiver credits the player who paid rather than the deployer or forwarder. Regenerate ABI types to pick up the constructor and ABI changes. - `DefifaDeployer` caches its controller's `PROJECTS`, `RULESETS`, and `DIRECTORY` as constructor immutables (resolved once from `CONTROLLER`) instead of reading `CONTROLLER.PROJECTS()` / `CONTROLLER.RULESETS()` / `CONTROLLER.DIRECTORY()` at each use. They are exposed as `IDefifaDeployer` getters alongside `CONTROLLER`; regenerate ABI types to pick them up. diff --git a/INVARIANTS.md b/INVARIANTS.md index 9c3628a9..90712719 100644 --- a/INVARIANTS.md +++ b/INVARIANTS.md @@ -57,7 +57,7 @@ NO_CONTEST is reported by the view as soon as the condition is met; the on-chain - **Reserve mints blocked in NO_CONTEST.** Prevents a malicious reserve mint from inflating `totalMintCost` past `minParticipation` after the game has already failed the participation check, which would otherwise revive the game from NO_CONTEST → SCORING (`DefifaHook.sol:577-579`). - **Delegate changes locked after MINT.** `setTierDelegateTo` / `setTierDelegatesTo` revert outside the MINT phase. This freezes voting power before scoring begins, so attestation can't be hot-swapped to a colluding delegate after scorecards drop (`DefifaHook.sol:817-819, 830-832`). - **`addToBalanceOf` cannot inflate participation.** `minParticipation` is checked against `totalMintCost` (incremented only by paid mints and reserve mints), not terminal balance — donations to the terminal cannot artificially satisfy the participation threshold (`DefifaDeployer.sol:256-260`). -- **Front-run-resistant hook clone.** `cloneDeterministic` salts on `keccak256(msg.sender, nonce)` so a different caller produces a different address; a watcher cannot front-run `launchGameWith` to deploy a hook at the predicted address and DoS initialization (`DefifaDeployer.sol:582-591`). +- **Front-run-resistant hook clone.** `cloneDeterministic` salts on `keccak256(_msgSender(), nonce)` so a different resolved caller produces a different address; a watcher cannot front-run `launchGameWith` to deploy a hook at the predicted address and DoS initialization. For trusted-forwarder calls, `_msgSender()` is the ERC-2771 signer, not the forwarder (`DefifaDeployer.sol:582-591`). - **Commitment fulfillment is single-shot.** `commitmentsFulfilledFor[gameId]` set BEFORE external calls; a re-entrant call returns early (`DefifaDeployer.sol:319-321`). - **NO_CONTEST trigger is single-shot.** `noContestTriggeredFor[gameId]` set BEFORE queuing the refund ruleset; a re-entrant call reverts `NoContestAlreadyTriggered` (`DefifaDeployer.sol:662-670`). @@ -67,7 +67,7 @@ NO_CONTEST is reported by the view as soon as the condition is met; the on-chain ### B.1 DefifaDeployer bindings -- **Dependency bindings are constructor `immutable`s, not a runtime setter.** `HOOK_CODE_ORIGIN`, `TOKEN_URI_RESOLVER`, `GOVERNOR`, `CONTROLLER`, `REGISTRY`, `DEFIFA_PROJECT_ID`, `BASE_PROTOCOL_PROJECT_ID`, and `HOOK_STORE` are all fixed at construction (`DefifaDeployer.sol:288-308`). These dependencies share unified CREATE2 addresses / canonical project IDs across chains, so nothing chain-specific remains to wire post-deploy — no address can mutate them after deployment. +- **Dependency bindings are constructor `immutable`s, not a runtime setter.** `HOOK_CODE_ORIGIN`, `TOKEN_URI_RESOLVER`, `GOVERNOR`, `CONTROLLER`, `REGISTRY`, `DEFIFA_PROJECT_ID`, `BASE_PROTOCOL_PROJECT_ID`, `HOOK_STORE`, and the ERC-2771 `trustedForwarder` are all fixed at construction (`DefifaDeployer.sol:288-308`). These dependencies share unified CREATE2 addresses / canonical project IDs across chains, so nothing chain-specific remains to wire post-deploy — no address can mutate them after deployment. No caller can retro-edit any existing game's rulesets, splits, fee divisors, or tier configuration. The protocol-fee divisor (`BASE_PROTOCOL_FEE_DIVISOR = 40` ⇒ 2.5%) and Defifa-fee divisor (`DEFIFA_FEE_DIVISOR = 20` ⇒ 5%) are `constant` (`DefifaDeployer.sol:72, 76`). @@ -93,7 +93,7 @@ Owns every game's project NFT (`PROJECTS.createFor(this)` in `launchGameWith`). **Permissionless game launch:** -- **`launchGameWith(DefifaLaunchProjectData)` payable → gameId** — anyone. Forwards `msg.value` to `JBProjects.createFor` for the creation fee. Validates timing/tier/currency/timeout consistency; clones the Defifa hook via `cloneDeterministic` salted with `msg.sender || nonce`; queues MINT (optional REFUND) and SCORING rulesets via `controller.launchRulesetsFor`; calls `governor.initializeGame`; transfers hook ownership to the governor; registers the clone in the address registry. (`DefifaDeployer.sol:381-642`) +- **`launchGameWith(DefifaLaunchProjectData)` payable → gameId** — anyone, including through the configured ERC-2771 trusted forwarder. Forwards `msg.value` to `JBProjects.createFor` for the creation fee and advertises the resolved `_msgSender()` as `originalPayer` while `createFor` runs. Validates timing/tier/currency/timeout consistency; clones the Defifa hook via `cloneDeterministic` salted with `_msgSender() || nonce`; queues MINT (optional REFUND) and SCORING rulesets via `controller.launchRulesetsFor`; calls `governor.initializeGame`; transfers hook ownership to the governor; registers the clone in the address registry. (`DefifaDeployer.sol:381-642`) - **Invariant:** game ID reserved before hook deployment so an interleaving `createFor` cannot invalidate the salt. Project NFT permanently held by this contract. **Permissionless lifecycle triggers:** @@ -105,7 +105,7 @@ Owns every game's project NFT (`PROJECTS.createFor(this)` in `launchGameWith`). **Construction-time bindings:** -- All Defifa dependencies (hook origin, URI resolver, governor, controller, registry, fee project IDs, hook store) are constructor `immutable`s — there is no post-deploy setter to bind or rebind them. (`DefifaDeployer.sol:288-308`) +- All Defifa dependencies (hook origin, URI resolver, governor, controller, registry, fee project IDs, hook store, and trusted forwarder) are constructor `immutable`s — there is no post-deploy setter to bind or rebind them. (`DefifaDeployer.sol:288-308`) **ERC-721 receipt:** @@ -185,7 +185,7 @@ Pure rendering surface — no privileged surface that affects game outcome or fu 11. **State-before-external-call ordering.** `commitmentsFulfilledFor`, `noContestTriggeredFor`, `ratifiedScorecardIdOf`, and `cashOutWeightIsSet` are all written BEFORE the external call that consumes them — re-entrancy cannot replay the action. 12. **Permissionless settlement triggers extract no value beyond canonical allocation.** `fulfillCommitmentsOf`, `triggerNoContestFor`, `mintReservesFor`, `submitScorecardFor`, `attestToScorecardFrom`, `ratifyScorecardFrom` — caller's reward is exactly the gas-funded service to the game, never a redirected payout. 13. **One-shot bindings.** `DefifaHook.initialize`, `DefifaHook.setTierCashOutWeightsTo`, `DefifaGovernor.initializeGame` — all irreversible. `DefifaDeployer`'s own dependencies are constructor `immutable`s (no setter at all). -14. **Front-run-resistant clone deployment.** `cloneDeterministic` salt includes `msg.sender`; a different caller produces a different address (`DefifaDeployer.sol:589-592`). +14. **Front-run-resistant clone deployment.** `cloneDeterministic` salt includes `_msgSender()`; a different resolved caller produces a different address (`DefifaDeployer.sol:589-592`). 15. **Participation immune to balance inflation.** `minParticipation` checks `hook.totalMintCost`, not terminal balance — `addToBalanceOf` donations cannot satisfy the threshold (`DefifaDeployer.sol:256-260`). 16. **NFT-only cash-out path.** `beforeCashOutRecordedWith` reverts if fungible project tokens are cashed out (`DefifaHook.sol:289`). The hook is the sole cash-out surface for Defifa games. @@ -197,7 +197,7 @@ For the underlying parimutuel game mechanics, pot-formation math, fee pipeline, These are NOT third-party attack vectors but are powers held by privileged addresses: -- **`DefifaDeployer` dependency wiring** (governor, controller, registry, fee project IDs, hook origin, URI resolver, hook store) is fixed at construction as `immutable`s — there is no privileged post-deploy setter. Misconfiguration would require deploying with wrong constructor args (wrong governor, wrong fee project IDs, etc.) — operationally caught by deploy script validation. +- **`DefifaDeployer` dependency wiring** (governor, controller, registry, fee project IDs, hook origin, URI resolver, hook store, trusted forwarder) is fixed at construction as `immutable`s — there is no privileged post-deploy setter. Misconfiguration would require deploying with wrong constructor args (wrong governor, wrong fee project IDs, wrong forwarder, etc.) — operationally caught by deploy script validation. - **`DefifaGovernor` Ownable owner** can call `initializeGame`. In production deployment this owner is the `DefifaDeployer` (called during `launchGameWith`). If the governor's owner were ever rotated to a non-deployer address, that address could bootstrap rogue scorecards for games it didn't deploy — but only games whose hook ownership it also controls, which would require breaking `DefifaDeployer.launchGameWith`'s `hook.transferOwnership(governor)` flow. - **`DefifaGovernor` as `DefifaHook` owner** is the **single ratifier** of every game's scorecard. The governor itself doesn't decide outcomes — it only enforces the BWA quorum + grace + timelock state machine. But the governor's *bytecode* is the source of truth for ratification rules; replacing the governor (via a controller-level migration or hook-ownership transfer) would change the rules. The deploy script intentionally leaves the governor in place and the hooks owned by it — there is no path in this codebase to rotate hook ownership away from the original governor. - **`DefifaDeployer` as `JBProjects` NFT holder** is the sole `ownerMustSendPayouts` invoker during SCORING (the SCORING ruleset sets `ownerMustSendPayouts=true`). `fulfillCommitmentsOf` is the deployer's `sendPayoutsOf` invocation — and it's permissionless. No human address has owner power over a Defifa game's payouts post-launch. diff --git a/README.md b/README.md index dea4d338..58528ecb 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Then read the upstream repos this package depends on: | Contract | Role | | --- | --- | -| `DefifaDeployer` | Launches games, clones hooks, initializes governance, and fulfills post-game fee commitments. | +| `DefifaDeployer` | Launches games, clones hooks, resolves ERC-2771 callers for launch attribution, initializes governance, and fulfills post-game fee commitments. | | `DefifaHook` | ERC-721 game-piece hook that tracks tiers, delegation, pending reserves, and cash-out weights for settlement. | | `DefifaGovernor` | Scorecard governance surface that accepts submissions, attestations, quorum checks, grace periods, and ratification. | | `DefifaHookLib` | Shared validation and weight logic extracted from the hook. | @@ -155,7 +155,7 @@ references/ ## Deployment notes -Deployments are handled through Sphinx. The deployer composes Juicebox core, the 721 hook stack, Defifa-specific governance, and metadata rendering into one game-launch surface. +Deployments are handled through Sphinx. The deployer composes Juicebox core, the 721 hook stack, Defifa-specific governance, metadata rendering, and the core trusted forwarder into one ERC-2771-aware game-launch surface. ## Where state lives diff --git a/USER_JOURNEYS.md b/USER_JOURNEYS.md index aee2c355..a14024bc 100644 --- a/USER_JOURNEYS.md +++ b/USER_JOURNEYS.md @@ -31,13 +31,13 @@ This repo turns a Juicebox project into a prediction-game lifecycle with fixed p - the creator knows the game start time, mint duration, optional refund duration, and scoring-timeout assumptions - tier count, tier names, tier price, and split commitments are finalized - the chosen terminal and payment token are correct because the launch path is intentionally one-way -- if `JBProjects` has a creation fee, the launch caller sends that exact native-token amount +- if `JBProjects` has a creation fee, the direct caller or trusted forwarder sends that exact native-token amount **Main Flow** 1. Prepare launch data with timing, tiers, splits, fee-project settings, terminal, and governance params. 2. Call `DefifaDeployer.launchGameWith(...)`. -3. The deployer launches the JB project, clones and initializes `DefifaHook`, initializes the governor state, and stores the game's immutable ops data. +3. The deployer resolves the ERC-2771 caller, advertises that account as the creation-fee payer while reserving the JB project, clones and initializes `DefifaHook`, initializes the governor state, and stores the game's immutable ops data. 4. The game advances through its documented phase sequence. **Failure Modes** diff --git a/package-lock.json b/package-lock.json index b372d08d..34f32188 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ballkidz/defifa", - "version": "0.0.61", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ballkidz/defifa", - "version": "0.0.61", + "version": "1.0.2", "license": "MIT", "dependencies": { "@bananapus/721-hook-v6": "^0.0.76", diff --git a/package.json b/package.json index 2db987b3..cfbafaaf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ballkidz/defifa", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "engines": { "node": ">=20.0.0" diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 63e213ce..e7d48871 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -120,7 +120,8 @@ contract DeployMainnet is Script, Sphinx { registry: registry.registry, defifaProjectId: _defifaProjectId, baseProtocolProjectId: _baseProtocolProjectId, - hookStore: hookStore + hookStore: hookStore, + trustedForwarder: core.trustedForwarder }); governor.transferOwnership(address(deployer)); diff --git a/src/DefifaDeployer.sol b/src/DefifaDeployer.sol index 492304d3..701b0eab 100644 --- a/src/DefifaDeployer.sol +++ b/src/DefifaDeployer.sol @@ -27,6 +27,7 @@ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol"; import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol"; import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol"; import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol"; +import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -53,6 +54,7 @@ import {DefifaTierParams} from "./structs/DefifaTierParams.sol"; /// to winning NFT holders. Games progress through phases: COUNTDOWN → MINT → REFUND → SCORING → COMPLETE (or /// NO_CONTEST if minimum participation isn't met or scorecard ratification times out). contract DefifaDeployer is + ERC2771Context, IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGamePotReporter, @@ -159,7 +161,7 @@ contract DefifaDeployer is /// @notice The account that paid the creation fee for the game currently being launched. /// @dev This contract owns the games it launches, so it advertises the resolved fee payer (the `launchGameWith` - /// caller, or that caller's upstream payer when the caller is itself an `IJBPayerTracker`) while + /// ERC-2771 caller, or that caller's upstream payer when the caller is itself an `IJBPayerTracker`) while /// `JBProjects.createFor` runs, letting a `pay`-routing fee receiver credit the true payer instead of this /// deployer. Cleared back to `address(0)` once the call returns. address public transient override originalPayer; @@ -328,6 +330,7 @@ contract DefifaDeployer is /// @param defifaProjectId The ID of the project that should take the fee from the games. /// @param baseProtocolProjectId The ID of the protocol project that will receive fees from fulfilling commitments. /// @param hookStore The store used by Defifa hooks. + /// @param trustedForwarder The trusted forwarder for the ERC2771Context. constructor( address hookCodeOrigin, IJB721TokenUriResolver tokenUriResolver, @@ -336,8 +339,11 @@ contract DefifaDeployer is IJBAddressRegistry registry, uint256 defifaProjectId, uint256 baseProtocolProjectId, - IJB721TiersHookStore hookStore - ) { + IJB721TiersHookStore hookStore, + address trustedForwarder + ) + ERC2771Context(trustedForwarder) + { HOOK_CODE_ORIGIN = hookCodeOrigin; TOKEN_URI_RESOLVER = tokenUriResolver; GOVERNOR = governor; @@ -381,7 +387,7 @@ contract DefifaDeployer is // If the pot is empty, queue the final ruleset without attempting payouts. if (pot == 0) { _queueFinalRuleset({gameId: gameId, metadata: metadata}); - emit FulfilledCommitments({gameId: gameId, pot: 0, caller: msg.sender}); + emit FulfilledCommitments({gameId: gameId, pot: 0, caller: _msgSender()}); return; } @@ -403,13 +409,13 @@ contract DefifaDeployer is // Payout failed — fee stays in pot. Reset to 0 so currentGamePotOf // doesn't double-count the fee. fulfilledCommitmentsOf[gameId] = 0; - emit CommitmentPayoutFailed({gameId: gameId, amount: feeAmount, reason: reason, caller: msg.sender}); + emit CommitmentPayoutFailed({gameId: gameId, amount: feeAmount, reason: reason, caller: _msgSender()}); } // Queue the final ruleset and emit. _queueFinalRuleset({gameId: gameId, metadata: metadata}); - emit FulfilledCommitments({gameId: gameId, pot: pot, caller: msg.sender}); + emit FulfilledCommitments({gameId: gameId, pot: pot, caller: _msgSender()}); } /// @notice Launches a new game owned by this contract with a DefifaHook attached. @@ -515,10 +521,11 @@ contract DefifaDeployer is } } + address caller = _msgSender(); + // Expose the resolved fee payer so a `pay`-routing fee receiver credits the true payer, not this deployer. - // This contract uses raw `msg.sender` (it is not an `ERC2771Context`), so the fee payer is the direct caller. // Cleared immediately after. - originalPayer = JBPayerTrackerLib.resolve(msg.sender); + originalPayer = JBPayerTrackerLib.resolve(caller); // Reserve the game ID up front so permissionless project creations cannot invalidate hook deployment. gameId = PROJECTS.createFor{value: msg.value}(address(this)); @@ -585,14 +592,14 @@ contract DefifaDeployer is // Increment the nonce for this deployment. uint256 currentNonce = ++_nonce; - // Clone deterministically using sender and nonce to prevent front-running. + // Clone deterministically using caller and nonce to prevent front-running. // Clones.clone() creates the proxy before initialize() is called, allowing an // attacker to front-run initialization and DOS the game deployment. Using - // cloneDeterministic with msg.sender in the salt prevents this since a different + // cloneDeterministic with the ERC-2771 caller in the salt prevents this since a different // caller produces a different address. DefifaHook hook = DefifaHook( Clones.cloneDeterministic({ - implementation: HOOK_CODE_ORIGIN, salt: keccak256(abi.encodePacked(msg.sender, currentNonce)) + implementation: HOOK_CODE_ORIGIN, salt: keccak256(abi.encodePacked(caller, currentNonce)) }) ); @@ -637,13 +644,11 @@ contract DefifaDeployer is // that produced the deployed hook. REGISTRY.registerAddress({ deployer: address(this), - salt: keccak256(abi.encodePacked(msg.sender, currentNonce)), + salt: keccak256(abi.encodePacked(caller, currentNonce)), bytecode: _cloneCreationCodeFor(address(HOOK_CODE_ORIGIN)) }); - emit LaunchGame({ - gameId: gameId, hook: hook, governor: GOVERNOR, tokenUriResolver: uriResolver, caller: msg.sender - }); + emit LaunchGame({gameId: gameId, hook: hook, governor: GOVERNOR, tokenUriResolver: uriResolver, caller: caller}); } /// @notice Allows this contract to receive 721s. @@ -718,7 +723,7 @@ contract DefifaDeployer is projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game: no contest." }); - emit QueuedNoContest({gameId: gameId, caller: msg.sender}); + emit QueuedNoContest({gameId: gameId, caller: _msgSender()}); } //*********************************************************************// diff --git a/test/DefifaAdversarialQuorum.t.sol b/test/DefifaAdversarialQuorum.t.sol index 4b901f71..7e866459 100644 --- a/test/DefifaAdversarialQuorum.t.sol +++ b/test/DefifaAdversarialQuorum.t.sol @@ -129,7 +129,8 @@ contract DefifaAdversarialQuorumTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _protocolFeeProjectId, baseProtocolProjectId: _defifaProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); diff --git a/test/DefifaFeeAccounting.t.sol b/test/DefifaFeeAccounting.t.sol index 9c4e1020..ab8bd209 100644 --- a/test/DefifaFeeAccounting.t.sol +++ b/test/DefifaFeeAccounting.t.sol @@ -116,7 +116,8 @@ contract DefifaFeeAccountingTest is JBTest, TestBaseWorkflow { registry: _registry, defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); diff --git a/test/DefifaFeePayer.t.sol b/test/DefifaFeePayer.t.sol index 58a64642..8c4719ee 100644 --- a/test/DefifaFeePayer.t.sol +++ b/test/DefifaFeePayer.t.sol @@ -52,6 +52,7 @@ contract DefifaFeePayerTest is JBTest, TestBaseWorkflow { DefifaGovernor governor; address projectOwner = address(bytes20(keccak256("projectOwner"))); + address _trustedForwarder = address(bytes20(keccak256("trustedForwarder"))); function setUp() public virtual override { super.setUp(); @@ -76,7 +77,8 @@ contract DefifaFeePayerTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: protocolFeeProjectId, baseProtocolProjectId: defifaProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: _trustedForwarder }); hook.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); @@ -108,6 +110,29 @@ contract DefifaFeePayerTest is JBTest, TestBaseWorkflow { assertEq(deployer.originalPayer(), address(0), "transient payer not cleared"); } + /// @notice Launching through the trusted forwarder advertises the ERC-2771 signer, not the forwarder. + function testAdvertisesForwardedFeePayerDuringLaunch() external { + PayerRecordingFeeReceiver feeReceiver = new PayerRecordingFeeReceiver(IJBPayerTracker(address(jbProjects()))); + uint256 fee = jbProjects().MAX_CREATION_FEE(); + vm.prank(multisig()); + jbProjects().setCreationFee(fee, payable(address(feeReceiver))); + + address player = address(bytes20(keccak256("forwardedPlayer"))); + bytes memory data = abi.encodeCall(DefifaDeployer.launchGameWith, (_launchData())); + + vm.deal(_trustedForwarder, fee); + vm.prank(_trustedForwarder); + (bool success, bytes memory reason) = address(deployer).call{value: fee}(abi.encodePacked(data, player)); + if (!success) { + assembly ("memory-safe") { + revert(add(reason, 32), mload(reason)) + } + } + + assertEq(feeReceiver.recordedPayer(), player, "fee credited to forwarder instead of signer"); + assertEq(deployer.originalPayer(), address(0), "transient payer not cleared"); + } + function _launchData() internal view returns (DefifaLaunchProjectData memory) { DefifaTierParams[] memory tiers = new DefifaTierParams[](2); for (uint256 i; i < tiers.length; i++) { diff --git a/test/DefifaGovernanceHardening.t.sol b/test/DefifaGovernanceHardening.t.sol index 95ddf490..7d44addd 100644 --- a/test/DefifaGovernanceHardening.t.sol +++ b/test/DefifaGovernanceHardening.t.sol @@ -128,7 +128,8 @@ contract DefifaGovernanceHardeningTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _protocolFeeProjectId, baseProtocolProjectId: _defifaProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); diff --git a/test/DefifaGovernor.t.sol b/test/DefifaGovernor.t.sol index c5409af8..9e60618c 100644 --- a/test/DefifaGovernor.t.sol +++ b/test/DefifaGovernor.t.sol @@ -138,7 +138,8 @@ contract DefifaGovernorTest is JBTest, TestBaseWorkflow { registry: _registry, defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); // Transfer ownership of the hook to the deployer. diff --git a/test/DefifaHookRegressions.t.sol b/test/DefifaHookRegressions.t.sol index c0fc9f2e..9d00e479 100644 --- a/test/DefifaHookRegressions.t.sol +++ b/test/DefifaHookRegressions.t.sol @@ -123,7 +123,8 @@ contract DefifaHookRegressions is JBTest, TestBaseWorkflow { registry: _registry, defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); diff --git a/test/DefifaMintCostInvariant.t.sol b/test/DefifaMintCostInvariant.t.sol index b76bbbef..b62459ad 100644 --- a/test/DefifaMintCostInvariant.t.sol +++ b/test/DefifaMintCostInvariant.t.sol @@ -234,7 +234,8 @@ contract DefifaMintCostInvariantTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _protocolFeeProjectId, baseProtocolProjectId: _defifaProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hookImpl.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); diff --git a/test/DefifaNoContest.t.sol b/test/DefifaNoContest.t.sol index f71a9605..37136f27 100644 --- a/test/DefifaNoContest.t.sol +++ b/test/DefifaNoContest.t.sol @@ -115,7 +115,8 @@ contract DefifaNoContestTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _protocolFeeProjectId, baseProtocolProjectId: _defifaProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); diff --git a/test/DefifaRegressionLowGuards.t.sol b/test/DefifaRegressionLowGuards.t.sol index 0c670551..2cb77435 100644 --- a/test/DefifaRegressionLowGuards.t.sol +++ b/test/DefifaRegressionLowGuards.t.sol @@ -114,7 +114,8 @@ contract DefifaRegressionLowGuardsTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _protocolFeeProjectId, baseProtocolProjectId: _defifaProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); diff --git a/test/DefifaSecurity.t.sol b/test/DefifaSecurity.t.sol index 6548ed8a..b54b0e09 100644 --- a/test/DefifaSecurity.t.sol +++ b/test/DefifaSecurity.t.sol @@ -125,7 +125,8 @@ contract DefifaSecurityTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _protocolFeeProjectId, baseProtocolProjectId: _defifaProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); diff --git a/test/DefifaUSDC.t.sol b/test/DefifaUSDC.t.sol index 41ab2057..9500b37a 100644 --- a/test/DefifaUSDC.t.sol +++ b/test/DefifaUSDC.t.sol @@ -142,7 +142,8 @@ contract DefifaUSDCTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); diff --git a/test/Fork.t.sol b/test/Fork.t.sol index b9fedac1..a58b8529 100644 --- a/test/Fork.t.sol +++ b/test/Fork.t.sol @@ -135,7 +135,8 @@ contract DefifaForkTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); diff --git a/test/TestQALastMile.t.sol b/test/TestQALastMile.t.sol index 6b0bcc0b..b2f364c4 100644 --- a/test/TestQALastMile.t.sol +++ b/test/TestQALastMile.t.sol @@ -135,7 +135,8 @@ contract TestQACashOutDoSDuringFulfillmentWindow is JBTest, TestBaseWorkflow { registry: _registry, defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); @@ -328,8 +329,8 @@ contract TestQACashOutDoSDuringFulfillmentWindow is JBTest, TestBaseWorkflow { /// @dev The deployer predicts gameId = PROJECTS().count() + 1, then clones and initializes a hook with that ID. /// If another project is created between the count() read and launchProjectFor(), the actual ID differs and /// the transaction reverts with DefifaDeployer_InvalidGameConfiguration. Because the clone uses -/// cloneDeterministic with msg.sender in the salt, a retry from the same caller succeeds with a new nonce. -/// No orphaned state remains after the revert. +/// cloneDeterministic with the resolved caller in the salt, a retry from the same caller succeeds with a new +/// nonce. No orphaned state remains after the revert. contract TestQAGameIdPredictionRace is JBTest, TestBaseWorkflow { using JBRulesetMetadataResolver for JBRuleset; @@ -413,7 +414,8 @@ contract TestQAGameIdPredictionRace is JBTest, TestBaseWorkflow { registry: _registry, defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); diff --git a/test/TestRegressionGaps.sol b/test/TestRegressionGaps.sol index a6e2ed26..57dde6e9 100644 --- a/test/TestRegressionGaps.sol +++ b/test/TestRegressionGaps.sol @@ -155,7 +155,8 @@ contract TestRegressionGapsERC20Games is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); @@ -619,7 +620,8 @@ contract TestRegressionGapsMultiGameIsolation is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); diff --git a/test/regression/AdjustedPendingReserves.t.sol b/test/regression/AdjustedPendingReserves.t.sol index d3b231e8..8f04ba94 100644 --- a/test/regression/AdjustedPendingReserves.t.sol +++ b/test/regression/AdjustedPendingReserves.t.sol @@ -128,7 +128,8 @@ contract AdjustedPendingReservesTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _protocolFeeProjectId, baseProtocolProjectId: _defifaProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); diff --git a/test/regression/AttestationDelegateBeneficiary.t.sol b/test/regression/AttestationDelegateBeneficiary.t.sol index f1eab854..d855d734 100644 --- a/test/regression/AttestationDelegateBeneficiary.t.sol +++ b/test/regression/AttestationDelegateBeneficiary.t.sol @@ -106,7 +106,8 @@ contract AttestationDelegateBeneficiary is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); diff --git a/test/regression/AttestationDoubleCount.t.sol b/test/regression/AttestationDoubleCount.t.sol index 2769c49b..a6cb9772 100644 --- a/test/regression/AttestationDoubleCount.t.sol +++ b/test/regression/AttestationDoubleCount.t.sol @@ -106,7 +106,8 @@ contract AttestationDoubleCountTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); diff --git a/test/regression/DefifaRegressionFixes.t.sol b/test/regression/DefifaRegressionFixes.t.sol index fd759cab..43530e7f 100644 --- a/test/regression/DefifaRegressionFixes.t.sol +++ b/test/regression/DefifaRegressionFixes.t.sol @@ -128,7 +128,8 @@ contract DefifaRegressionFixesTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _protocolFeeProjectId, baseProtocolProjectId: _defifaProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); diff --git a/test/regression/FixPendingReserveDilution.t.sol b/test/regression/FixPendingReserveDilution.t.sol index 3546a937..53c8478f 100644 --- a/test/regression/FixPendingReserveDilution.t.sol +++ b/test/regression/FixPendingReserveDilution.t.sol @@ -123,7 +123,8 @@ contract FixPendingReserveDilutionTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _protocolFeeProjectId, baseProtocolProjectId: _defifaProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); diff --git a/test/regression/FulfillmentBlocksRatification.t.sol b/test/regression/FulfillmentBlocksRatification.t.sol index 02b9e584..189f4666 100644 --- a/test/regression/FulfillmentBlocksRatification.t.sol +++ b/test/regression/FulfillmentBlocksRatification.t.sol @@ -130,7 +130,8 @@ contract FulfillmentBlocksRatification is JBTest, TestBaseWorkflow { registry: _registry, defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); diff --git a/test/regression/GracePeriodBypass.t.sol b/test/regression/GracePeriodBypass.t.sol index 6c8ef6ee..7efd6008 100644 --- a/test/regression/GracePeriodBypass.t.sol +++ b/test/regression/GracePeriodBypass.t.sol @@ -129,7 +129,8 @@ contract GracePeriodBypass is JBTest, TestBaseWorkflow { registry: _registry, defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); diff --git a/test/regression/NoContestReserveDrain.t.sol b/test/regression/NoContestReserveDrain.t.sol index ad2ff96e..d16321be 100644 --- a/test/regression/NoContestReserveDrain.t.sol +++ b/test/regression/NoContestReserveDrain.t.sol @@ -102,7 +102,8 @@ contract NoContestReserveDrainTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); _hookImpl.transferOwnership(address(_deployer)); diff --git a/test/regression/OneTierZeroTimeoutLock.t.sol b/test/regression/OneTierZeroTimeoutLock.t.sol index 44bb10b4..27d11b05 100644 --- a/test/regression/OneTierZeroTimeoutLock.t.sol +++ b/test/regression/OneTierZeroTimeoutLock.t.sol @@ -106,7 +106,8 @@ contract OneTierZeroTimeoutLockTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: defifaProjectId, baseProtocolProjectId: protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); diff --git a/test/regression/PendingReserveDilution.t.sol b/test/regression/PendingReserveDilution.t.sol index 1843b696..084d324b 100644 --- a/test/regression/PendingReserveDilution.t.sol +++ b/test/regression/PendingReserveDilution.t.sol @@ -116,7 +116,8 @@ contract PendingReserveDilutionTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _protocolFeeProjectId, baseProtocolProjectId: _defifaProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hook.transferOwnership(address(deployer)); governor.transferOwnership(address(deployer)); diff --git a/test/regression/PendingReserveQuorumGrief.t.sol b/test/regression/PendingReserveQuorumGrief.t.sol index b5642279..0c0fdf3e 100644 --- a/test/regression/PendingReserveQuorumGrief.t.sol +++ b/test/regression/PendingReserveQuorumGrief.t.sol @@ -118,7 +118,8 @@ contract PendingReserveQuorumGriefTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); _hookImpl.transferOwnership(address(_deployer)); diff --git a/test/regression/PendingReserveSnapshotBypass.t.sol b/test/regression/PendingReserveSnapshotBypass.t.sol index 3cd2c694..3b42de1d 100644 --- a/test/regression/PendingReserveSnapshotBypass.t.sol +++ b/test/regression/PendingReserveSnapshotBypass.t.sol @@ -119,7 +119,8 @@ contract PendingReserveSnapshotBypassTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); _hookImpl.transferOwnership(address(_deployer)); diff --git a/test/regression/RegistryMismatch.t.sol b/test/regression/RegistryMismatch.t.sol index 6c4f1902..6b5a7fc8 100644 --- a/test/regression/RegistryMismatch.t.sol +++ b/test/regression/RegistryMismatch.t.sol @@ -94,7 +94,8 @@ contract RegistryMismatchTest is JBTest, TestBaseWorkflow { registry: registry, defifaProjectId: defifaProjectId, baseProtocolProjectId: protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hookCodeOrigin.transferOwnership(address(deployer)); diff --git a/test/regression/SingleTierTimeoutLock.t.sol b/test/regression/SingleTierTimeoutLock.t.sol index d1206e2d..0c0a83ae 100644 --- a/test/regression/SingleTierTimeoutLock.t.sol +++ b/test/regression/SingleTierTimeoutLock.t.sol @@ -107,7 +107,8 @@ contract SingleTierTimeoutLockTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: _defifaProjectId, baseProtocolProjectId: _protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); _hookImpl.transferOwnership(address(_deployer)); diff --git a/test/regression/TierCapMismatch.t.sol b/test/regression/TierCapMismatch.t.sol index bab2a9c8..feff058f 100644 --- a/test/regression/TierCapMismatch.t.sol +++ b/test/regression/TierCapMismatch.t.sol @@ -98,7 +98,8 @@ contract TierCapMismatchTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: defifaProjectId, baseProtocolProjectId: protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hookCodeOrigin.transferOwnership(address(deployer)); diff --git a/test/regression/TierCapValidationRegression.t.sol b/test/regression/TierCapValidationRegression.t.sol index 912bf8a2..5c273eb6 100644 --- a/test/regression/TierCapValidationRegression.t.sol +++ b/test/regression/TierCapValidationRegression.t.sol @@ -94,7 +94,8 @@ contract TierCapValidationRegressionTest is JBTest, TestBaseWorkflow { registry: new JBAddressRegistry(), defifaProjectId: defifaProjectId, baseProtocolProjectId: protocolFeeProjectId, - hookStore: new JB721TiersHookStore() + hookStore: new JB721TiersHookStore(), + trustedForwarder: address(0) }); hookCodeOrigin.transferOwnership(address(deployer));