@towns-protocol/contracts 0.0.391 → 0.0.393

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@towns-protocol/contracts",
3
- "version": "0.0.391",
4
- "packageManager": "yarn@3.8.0",
3
+ "version": "0.0.393",
5
4
  "scripts": {
6
5
  "clean": "forge clean",
7
6
  "compile": "forge build",
@@ -34,7 +33,7 @@
34
33
  "@layerzerolabs/oapp-evm": "^0.3.2",
35
34
  "@openzeppelin/merkle-tree": "^1.0.8",
36
35
  "@prb/test": "^0.6.4",
37
- "@towns-protocol/prettier-config": "^0.0.391",
36
+ "@towns-protocol/prettier-config": "^0.0.393",
38
37
  "@wagmi/cli": "^2.2.0",
39
38
  "forge-std": "github:foundry-rs/forge-std#v1.10.0",
40
39
  "prettier": "^3.5.3",
@@ -54,5 +53,5 @@
54
53
  "publishConfig": {
55
54
  "access": "public"
56
55
  },
57
- "gitHead": "cee1e497a4c9c60bad2675c43b20aabd0171e9a4"
56
+ "gitHead": "9379e7b0270d20a5b69bbaa1b674bdb27cef6c08"
58
57
  }
@@ -23,6 +23,7 @@ import {DeployMockLegacyArchitect} from "../facets/DeployMockLegacyArchitect.s.s
23
23
  import {DeployPartnerRegistry} from "../facets/DeployPartnerRegistry.s.sol";
24
24
  import {DeployPlatformRequirements} from "../facets/DeployPlatformRequirements.s.sol";
25
25
  import {DeployWalletLink} from "../facets/DeployWalletLink.s.sol";
26
+ import {DeployFeeManager} from "../facets/DeployFeeManager.s.sol";
26
27
  import {LibString} from "solady/utils/LibString.sol";
27
28
 
28
29
  // contracts
@@ -152,6 +153,7 @@ contract DeploySpaceFactory is IDiamondInitHelper, DiamondHelper, Deployer {
152
153
  facetHelper.add("EIP712Facet");
153
154
  facetHelper.add("PartnerRegistry");
154
155
  facetHelper.add("FeatureManagerFacet");
156
+ facetHelper.add("FeeManagerFacet");
155
157
  facetHelper.add("SpaceProxyInitializer");
156
158
  facetHelper.add("SpaceFactoryInit");
157
159
 
@@ -272,6 +274,13 @@ contract DeploySpaceFactory is IDiamondInitHelper, DiamondHelper, Deployer {
272
274
  DeployFeatureManager.makeInitData()
273
275
  );
274
276
 
277
+ facet = facetHelper.getDeployedAddress("FeeManagerFacet");
278
+ addFacet(
279
+ makeCut(facet, FacetCutAction.Add, DeployFeeManager.selectors()),
280
+ facet,
281
+ DeployFeeManager.makeInitData(deployer)
282
+ );
283
+
275
284
  address spaceProxyInitializer = facetHelper.getDeployedAddress("SpaceProxyInitializer");
276
285
  spaceFactoryInit = facetHelper.getDeployedAddress("SpaceFactoryInit");
277
286
  spaceFactoryInitData = DeploySpaceFactoryInit.makeInitData(spaceProxyInitializer);
@@ -346,7 +355,7 @@ contract DeploySpaceFactory is IDiamondInitHelper, DiamondHelper, Deployer {
346
355
  DeployPlatformRequirements.makeInitData(
347
356
  deployer, // feeRecipient
348
357
  500, // membershipBps 5%
349
- 0.005 ether, // membershipFee
358
+ 0.0005 ether, // membershipFee
350
359
  1000, // membershipFreeAllocation
351
360
  365 days, // membershipDuration
352
361
  0.001 ether // membershipMinPrice
@@ -395,6 +404,12 @@ contract DeploySpaceFactory is IDiamondInitHelper, DiamondHelper, Deployer {
395
404
  facet,
396
405
  DeployFeatureManager.makeInitData()
397
406
  );
407
+ } else if (facetName.eq("FeeManagerFacet")) {
408
+ addFacet(
409
+ makeCut(facet, FacetCutAction.Add, DeployFeeManager.selectors()),
410
+ facet,
411
+ DeployFeeManager.makeInitData(deployer)
412
+ );
398
413
  }
399
414
  }
400
415
  }
@@ -0,0 +1,46 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ // interfaces
5
+ import {IDiamond} from "@towns-protocol/diamond/src/Diamond.sol";
6
+ import {IFeeManager} from "src/factory/facets/fee/IFeeManager.sol";
7
+
8
+ // libraries
9
+ import {LibDeploy} from "@towns-protocol/diamond/src/utils/LibDeploy.sol";
10
+ import {DynamicArrayLib} from "solady/utils/DynamicArrayLib.sol";
11
+
12
+ library DeployFeeManager {
13
+ using DynamicArrayLib for DynamicArrayLib.DynamicArray;
14
+
15
+ function selectors() internal pure returns (bytes4[] memory res) {
16
+ DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(8);
17
+ arr.p(IFeeManager.calculateFee.selector);
18
+ arr.p(IFeeManager.chargeFee.selector);
19
+ arr.p(IFeeManager.setFeeConfig.selector);
20
+ arr.p(IFeeManager.setFeeHook.selector);
21
+ arr.p(IFeeManager.setProtocolFeeRecipient.selector);
22
+ arr.p(IFeeManager.getFeeConfig.selector);
23
+ arr.p(IFeeManager.getFeeHook.selector);
24
+ arr.p(IFeeManager.getProtocolFeeRecipient.selector);
25
+
26
+ bytes32[] memory selectors_ = arr.asBytes32Array();
27
+ assembly ("memory-safe") {
28
+ res := selectors_
29
+ }
30
+ }
31
+
32
+ function makeCut(
33
+ address facetAddress,
34
+ IDiamond.FacetCutAction action
35
+ ) internal pure returns (IDiamond.FacetCut memory) {
36
+ return IDiamond.FacetCut(facetAddress, action, selectors());
37
+ }
38
+
39
+ function deploy() internal returns (address) {
40
+ return LibDeploy.deployCode("FeeManagerFacet.sol", "");
41
+ }
42
+
43
+ function makeInitData(address protocolFeeRecipient) internal pure returns (bytes memory) {
44
+ return abi.encodeCall(IFeeManager.__FeeManagerFacet__init, protocolFeeRecipient);
45
+ }
46
+ }
@@ -2,21 +2,21 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
- import {IAppFactory, IAppFactoryBase} from "src/apps/facets/factory/IAppFactory.sol";
5
+ import {IAppFactoryBase} from "src/apps/facets/factory/IAppFactory.sol";
6
6
 
7
7
  // libraries
8
8
  import {console} from "forge-std/console.sol";
9
9
 
10
10
  // contracts
11
- import {Interaction} from "../../common/Interaction.s.sol";
12
- import {AlphaHelper} from "../helpers/AlphaHelper.sol";
13
- import {DeployAppRegistry} from "../../deployments/diamonds/DeployAppRegistry.s.sol";
14
- import {DeploySimpleAppBeacon} from "../../deployments/diamonds/DeploySimpleAppBeacon.s.sol";
11
+ import {Interaction} from "../common/Interaction.s.sol";
12
+ import {AlphaHelper} from "./helpers/AlphaHelper.sol";
13
+ import {DeployAppRegistry} from "../deployments/diamonds/DeployAppRegistry.s.sol";
14
+ import {DeploySimpleAppBeacon} from "../deployments/diamonds/DeploySimpleAppBeacon.s.sol";
15
15
 
16
16
  // facet deployers
17
- import {DeployAppRegistryFacet} from "../../deployments/facets/DeployAppRegistryFacet.s.sol";
18
- import {DeployAppInstallerFacet} from "../../deployments/facets/DeployAppInstallerFacet.s.sol";
19
- import {DeployAppFactoryFacet} from "../../deployments/facets/DeployAppFactoryFacet.s.sol";
17
+ import {DeployAppRegistryFacet} from "../deployments/facets/DeployAppRegistryFacet.s.sol";
18
+ import {DeployAppInstallerFacet} from "../deployments/facets/DeployAppInstallerFacet.s.sol";
19
+ import {DeployAppFactoryFacet} from "../deployments/facets/DeployAppFactoryFacet.s.sol";
20
20
 
21
21
  contract InteractAppRegistry is Interaction, AlphaHelper {
22
22
  DeployAppRegistry private deployHelper = new DeployAppRegistry();
@@ -3,7 +3,7 @@ pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
5
  import {IImplementationRegistry} from "../../src/factory/facets/registry/IImplementationRegistry.sol";
6
- import {ISpaceDelegation} from "src/base/registry/facets/delegation/ISpaceDelegation.sol";
6
+ import {IFeeManager} from "../../src/factory/facets/fee/IFeeManager.sol";
7
7
  import {IMainnetDelegation} from "src/base/registry/facets/mainnet/IMainnetDelegation.sol";
8
8
  import {ISpaceOwner} from "src/spaces/facets/owner/ISpaceOwner.sol";
9
9
  import {INodeOperator} from "src/base/registry/facets/operator/INodeOperator.sol";
@@ -12,6 +12,8 @@ import {ISubscriptionModule} from "src/apps/modules/subscription/ISubscriptionMo
12
12
 
13
13
  // libraries
14
14
  import {NodeOperatorStatus} from "src/base/registry/facets/operator/NodeOperatorStorage.sol";
15
+ import {FeeCalculationMethod} from "../../src/factory/facets/fee/FeeManagerStorage.sol";
16
+ import {FeeTypesLib} from "../../src/factory/facets/fee/FeeTypesLib.sol";
15
17
 
16
18
  // contracts
17
19
  import {MAX_CLAIMABLE_SUPPLY} from "./InteractClaimCondition.s.sol";
@@ -49,6 +51,14 @@ contract InteractPostDeploy is Interaction {
49
51
  IImplementationRegistry(spaceFactory).addImplementation(baseRegistry);
50
52
  IImplementationRegistry(spaceFactory).addImplementation(riverAirdrop);
51
53
  IImplementationRegistry(spaceFactory).addImplementation(appRegistry);
54
+ IFeeManager(spaceFactory).setFeeConfig(
55
+ FeeTypesLib.TIP_MEMBER,
56
+ deployer,
57
+ FeeCalculationMethod.PERCENT,
58
+ 50,
59
+ 0,
60
+ true
61
+ );
52
62
  ISubscriptionModule(subscriptionModule).setSpaceFactory(spaceFactory);
53
63
  IMainnetDelegation(baseRegistry).setProxyDelegation(proxyDelegation);
54
64
  IRewardsDistribution(baseRegistry).setRewardNotifier(deployer, true);
@@ -0,0 +1,40 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ // interfaces
5
+ import {IDiamondCut} from "@towns-protocol/diamond/src/facets/cut/IDiamondCut.sol";
6
+
7
+ // libraries
8
+ import {console} from "forge-std/console.sol";
9
+ import {DeployTipping} from "../deployments/facets/DeployTipping.s.sol";
10
+
11
+ // contracts
12
+ import {Interaction} from "../common/Interaction.s.sol";
13
+ import {AlphaHelper} from "./helpers/AlphaHelper.sol";
14
+
15
+ contract InteractSpace is Interaction, AlphaHelper {
16
+ function __interact(address deployer) internal override {
17
+ // Get the deployed Space diamond address
18
+ address space = getDeployment("space");
19
+
20
+ console.log("Space Diamond:", space);
21
+
22
+ // Deploy new TippingFacet implementation
23
+ console.log("\n=== Deploying TippingFacet ===");
24
+ vm.setEnv("OVERRIDE_DEPLOYMENTS", "1");
25
+
26
+ address tippingFacet = DeployTipping.deploy();
27
+ console.log("TippingFacet deployed at:", tippingFacet);
28
+
29
+ // Add the cut for replacing the existing facet implementation
30
+ addCut(DeployTipping.makeCut(tippingFacet, FacetCutAction.Replace));
31
+
32
+ // Execute the diamond cut without initialization
33
+ console.log("\n=== Executing Diamond Cut to Replace TippingFacet ===");
34
+ vm.broadcast(deployer);
35
+ IDiamondCut(space).diamondCut(baseFacets(), address(0), "");
36
+
37
+ console.log("\n=== Diamond Cut Complete ===");
38
+ console.log("TippingFacet successfully updated on Space diamond");
39
+ }
40
+ }
@@ -0,0 +1,71 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ // interfaces
5
+ import {IFeeManager} from "src/factory/facets/fee/IFeeManager.sol";
6
+ import {IPlatformRequirements} from "src/factory/facets/platform/requirements/IPlatformRequirements.sol";
7
+ import {IDiamondCut} from "@towns-protocol/diamond/src/facets/cut/IDiamondCut.sol";
8
+
9
+ // libraries
10
+ import {FeeTypesLib} from "src/factory/facets/fee/FeeTypesLib.sol";
11
+ import {FeeCalculationMethod} from "src/factory/facets/fee/FeeManagerStorage.sol";
12
+ import {console} from "forge-std/console.sol";
13
+ import {DeployFeeManager} from "../deployments/facets/DeployFeeManager.s.sol";
14
+
15
+ // contracts
16
+ import {Interaction} from "../common/Interaction.s.sol";
17
+ import {AlphaHelper} from "./helpers/AlphaHelper.sol";
18
+
19
+ contract InteractSpaceFactory is Interaction, AlphaHelper {
20
+ function __interact(address deployer) internal override {
21
+ // Get the deployed SpaceFactory diamond address
22
+ address spaceFactory = getDeployment("spaceFactory");
23
+
24
+ console.log("SpaceFactory Diamond:", spaceFactory);
25
+
26
+ // Set fee recipient in PlatformRequirementsFacet
27
+ console.log("\n=== Setting Fee Recipient in PlatformRequirementsFacet ===");
28
+ vm.broadcast(deployer);
29
+ IPlatformRequirements(spaceFactory).setFeeRecipient(deployer);
30
+ console.log("Fee recipient set to:", deployer);
31
+
32
+ // Get fee recipient from PlatformRequirementsFacet
33
+ address protocolFeeRecipient = IPlatformRequirements(spaceFactory).getFeeRecipient();
34
+ console.log("Protocol Fee Recipient:", protocolFeeRecipient);
35
+
36
+ // Deploy new FeeManagerFacet implementation
37
+ console.log("\n=== Deploying FeeManagerFacet ===");
38
+ vm.setEnv("OVERRIDE_DEPLOYMENTS", "1");
39
+
40
+ address feeManagerFacet = DeployFeeManager.deploy();
41
+ console.log("FeeManagerFacet deployed at:", feeManagerFacet);
42
+
43
+ // Add the cut for the new facet implementation
44
+ addCut(DeployFeeManager.makeCut(feeManagerFacet, FacetCutAction.Add));
45
+
46
+ // Prepare initialization data with the fee recipient from PlatformRequirementsFacet
47
+ bytes memory initData = DeployFeeManager.makeInitData(protocolFeeRecipient);
48
+
49
+ // Execute the diamond cut with initialization
50
+ console.log("\n=== Executing Diamond Cut with Initialization ===");
51
+ vm.broadcast(deployer);
52
+ IDiamondCut(spaceFactory).diamondCut(baseFacets(), feeManagerFacet, initData);
53
+
54
+ console.log("\n=== Diamond Cut Complete ===");
55
+
56
+ // Set fee config for tipping
57
+ console.log("\n=== Setting Fee Config for Tipping ===");
58
+ vm.broadcast(deployer);
59
+ IFeeManager(spaceFactory).setFeeConfig(
60
+ FeeTypesLib.TIP_MEMBER,
61
+ address(0),
62
+ FeeCalculationMethod.PERCENT,
63
+ 50, // 0.5% (50 basis points)
64
+ 0,
65
+ true
66
+ );
67
+
68
+ console.log("Fee config set for TIP_MEMBER: 0.5% (50 bps)");
69
+ console.log("\n=== Interaction Complete ===");
70
+ }
71
+ }
@@ -48,7 +48,6 @@ interface IAppRegistryBase {
48
48
  /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
49
49
  event AppRegistered(address indexed app, bytes32 uid);
50
50
  event AppUnregistered(address indexed app, bytes32 uid);
51
- event AppUpdated(address indexed app, bytes32 uid);
52
51
  event AppBanned(address indexed app, bytes32 uid);
53
52
  event AppSchemaSet(bytes32 uid);
54
53
  event AppInstalled(address indexed app, address indexed account, bytes32 indexed appId);
@@ -57,10 +57,10 @@ library XChainCheckLib {
57
57
  uint256 requestId,
58
58
  IEntitlementGatedBase.NodeVoteStatus result
59
59
  ) internal {
60
- uint256 nodeCount = self.nodes[requestId].length();
60
+ uint256 voteCount = self.votes[requestId].length;
61
61
  bool voteRecorded = false;
62
62
 
63
- for (uint256 i; i < nodeCount; ++i) {
63
+ for (uint256 i; i < voteCount; ++i) {
64
64
  IEntitlementGatedBase.NodeVote storage currentVote = self.votes[requestId][i];
65
65
 
66
66
  if (currentVote.node == msg.sender) {
@@ -82,9 +82,10 @@ library XChainCheckLib {
82
82
  XChainLib.Check storage self,
83
83
  uint256 requestId
84
84
  ) internal view returns (VoteResults memory results) {
85
+ uint256 voteCount = self.votes[requestId].length;
85
86
  results.totalNodes = self.nodes[requestId].length();
86
87
 
87
- for (uint256 i; i < results.totalNodes; ++i) {
88
+ for (uint256 i; i < voteCount; ++i) {
88
89
  IEntitlementGatedBase.NodeVote storage vote = self.votes[requestId][i];
89
90
 
90
91
  if (vote.vote == IEntitlementGatedBase.NodeVoteStatus.PASSED) {
@@ -0,0 +1,233 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ // interfaces
5
+ import {IFeeHook, FeeHookResult} from "./IFeeHook.sol";
6
+ import {IFeeManagerBase} from "./IFeeManager.sol";
7
+
8
+ // libraries
9
+ import {BasisPoints} from "src/utils/libraries/BasisPoints.sol";
10
+ import {CurrencyTransfer} from "src/utils/libraries/CurrencyTransfer.sol";
11
+ import {CustomRevert} from "src/utils/libraries/CustomRevert.sol";
12
+ import {FeeCalculationMethod, FeeConfig, FeeManagerStorage} from "./FeeManagerStorage.sol";
13
+ import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
14
+
15
+ /// @title FeeManagerBase
16
+ /// @notice Base contract with internal fee management logic
17
+ abstract contract FeeManagerBase is IFeeManagerBase {
18
+ using CustomRevert for bytes4;
19
+ using FeeManagerStorage for FeeManagerStorage.Layout;
20
+
21
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
22
+ /* INTERNAL CALCULATIONS */
23
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
24
+
25
+ /// @notice Calculates base fee before hook processing
26
+ /// @param config Fee configuration
27
+ /// @param amount Base amount for percentage calculations
28
+ /// @return baseFee The calculated base fee
29
+ function _calculateBaseFee(
30
+ FeeConfig storage config,
31
+ uint256 amount
32
+ ) internal view returns (uint256 baseFee) {
33
+ FeeCalculationMethod method = config.method;
34
+ if (method == FeeCalculationMethod.FIXED) {
35
+ return config.fixedFee;
36
+ } else if (method == FeeCalculationMethod.PERCENT) {
37
+ return BasisPoints.calculate(amount, config.bps);
38
+ } else if (method == FeeCalculationMethod.HYBRID) {
39
+ uint256 percentFee = BasisPoints.calculate(amount, config.bps);
40
+ return FixedPointMathLib.max(percentFee, config.fixedFee);
41
+ }
42
+ return 0;
43
+ }
44
+
45
+ /// @notice Calculates fee for estimation (view function)
46
+ /// @dev Does not modify state, calls hook's calculateFee if configured
47
+ /// @param feeType The type of fee to calculate
48
+ /// @param user The address that would be charged
49
+ /// @param amount The base amount for percentage calculations
50
+ /// @param extraData Additional data passed to hooks
51
+ /// @return finalFee The calculated fee amount
52
+ function _calculateFee(
53
+ bytes32 feeType,
54
+ address user,
55
+ uint256 amount,
56
+ bytes calldata extraData
57
+ ) internal view returns (uint256 finalFee) {
58
+ FeeManagerStorage.Layout storage $ = FeeManagerStorage.getLayout();
59
+ FeeConfig storage config = $.feeConfigs[feeType];
60
+
61
+ // Check if fee is configured and enabled
62
+ if (!config.enabled) return 0;
63
+
64
+ // Calculate base fee
65
+ uint256 baseFee = _calculateBaseFee(config, amount);
66
+
67
+ // Apply hook if configured
68
+ address hook = config.hook;
69
+ if (hook != address(0)) {
70
+ try IFeeHook(hook).calculateFee(feeType, user, baseFee, extraData) returns (
71
+ FeeHookResult memory result
72
+ ) {
73
+ return result.finalFee;
74
+ } catch {
75
+ // If hook fails, fall back to base fee
76
+ return baseFee;
77
+ }
78
+ }
79
+
80
+ return baseFee;
81
+ }
82
+
83
+ /// @notice Charges fee and transfers it (state-changing)
84
+ /// @dev Calls hook's onChargeFee if configured, then transfers currency
85
+ /// @dev Note: `user` is metadata for hooks/events. Actual payment comes from msg.sender.
86
+ /// @dev Hook failures fall back to base fee (consistent with calculateFee behavior)
87
+ /// @param feeType The type of fee to charge
88
+ /// @param user The address for whom the fee is being charged (for hooks/events)
89
+ /// @param amount The base amount for percentage calculations
90
+ /// @param currency The currency contract (address(0) for native token)
91
+ /// @param context Additional context passed to hooks
92
+ /// @return finalFee The actual fee charged
93
+ function _chargeFee(
94
+ bytes32 feeType,
95
+ address user,
96
+ uint256 amount,
97
+ address currency,
98
+ uint256 maxFee,
99
+ bytes calldata context
100
+ ) internal virtual returns (uint256 finalFee) {
101
+ FeeManagerStorage.Layout storage $ = FeeManagerStorage.getLayout();
102
+ FeeConfig storage config = _getFeeConfig(feeType);
103
+
104
+ // Check if fee is configured and enabled
105
+ if (!config.enabled) return 0;
106
+
107
+ // Calculate base fee
108
+ uint256 baseFee = _calculateBaseFee(config, amount);
109
+
110
+ // Apply hook if configured, with fallback to base fee on failure
111
+ address hook = config.hook;
112
+ if (hook != address(0)) {
113
+ try IFeeHook(hook).onChargeFee(feeType, user, baseFee, context) returns (
114
+ FeeHookResult memory result
115
+ ) {
116
+ finalFee = result.finalFee;
117
+ } catch {
118
+ // If hook fails, fall back to base fee (consistent with calculateFee)
119
+ finalFee = baseFee;
120
+ }
121
+ } else {
122
+ finalFee = baseFee;
123
+ }
124
+
125
+ // Enforce slippage protection
126
+ if (finalFee > maxFee) FeeManager__ExceedsMaxFee.selector.revertWith();
127
+
128
+ // Determine recipient (fallback to protocol recipient if not set)
129
+ address recipient = config.recipient;
130
+ if (recipient == address(0)) recipient = $.protocolFeeRecipient;
131
+
132
+ // Emit event before external calls (checks-effects-interactions pattern)
133
+ if (finalFee > 0) {
134
+ emit FeeCharged(feeType, user, currency, finalFee, recipient);
135
+ }
136
+
137
+ // Transfer fee and/or refund excess if maxFee > 0
138
+ if (maxFee > 0) {
139
+ // Convert address(0) to NATIVE_TOKEN for CurrencyTransfer library
140
+ address feeCurrency = currency == address(0) ? CurrencyTransfer.NATIVE_TOKEN : currency;
141
+
142
+ // For native token, validate msg.value matches maxFee
143
+ if (feeCurrency == CurrencyTransfer.NATIVE_TOKEN && msg.value != maxFee) {
144
+ CurrencyTransfer.MsgValueMismatch.selector.revertWith();
145
+ }
146
+
147
+ CurrencyTransfer.transferFeeWithRefund(
148
+ feeCurrency,
149
+ msg.sender,
150
+ recipient,
151
+ finalFee,
152
+ maxFee
153
+ );
154
+ }
155
+ }
156
+
157
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
158
+ /* INTERNAL CONFIGURATION */
159
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
160
+
161
+ /// @notice Sets fee configuration
162
+ /// @param feeType The fee type identifier
163
+ /// @param recipient Fee recipient (zero address = use protocol fee recipient)
164
+ /// @param method Calculation method
165
+ /// @param bps Basis points (1-10000)
166
+ /// @param fixedFee Fixed fee amount
167
+ /// @param enabled Whether the fee is active
168
+ function _setFeeConfig(
169
+ bytes32 feeType,
170
+ address recipient,
171
+ FeeCalculationMethod method,
172
+ uint16 bps,
173
+ uint128 fixedFee,
174
+ bool enabled
175
+ ) internal {
176
+ // Allow zero address recipient to fallback to protocol fee recipient
177
+ // Validation that protocol fee recipient is set happens at initialization
178
+ if (bps > BasisPoints.MAX_BPS) FeeManager__InvalidBps.selector.revertWith();
179
+
180
+ FeeConfig storage config = _getFeeConfig(feeType);
181
+ config.recipient = recipient;
182
+ config.lastUpdated = uint48(block.timestamp);
183
+ config.bps = bps;
184
+ config.method = method;
185
+ config.enabled = enabled;
186
+ config.fixedFee = fixedFee;
187
+
188
+ emit FeeConfigured(feeType, recipient, method, bps, fixedFee, enabled);
189
+ }
190
+
191
+ /// @notice Sets fee hook
192
+ /// @param feeType The fee type identifier
193
+ /// @param hook Address of the hook contract (zero to remove)
194
+ function _setFeeHook(bytes32 feeType, address hook) internal {
195
+ FeeConfig storage config = _getFeeConfig(feeType);
196
+ config.hook = hook;
197
+ emit FeeHookSet(feeType, hook);
198
+ }
199
+
200
+ /// @notice Sets protocol fee recipient
201
+ /// @param recipient New protocol fee recipient
202
+ function _setProtocolFeeRecipient(address recipient) internal {
203
+ if (recipient == address(0)) FeeManager__InvalidRecipient.selector.revertWith();
204
+ FeeManagerStorage.Layout storage $ = FeeManagerStorage.getLayout();
205
+ $.protocolFeeRecipient = recipient;
206
+ emit ProtocolFeeRecipientSet(recipient);
207
+ }
208
+
209
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
210
+ /* INTERNAL GETTERS */
211
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
212
+
213
+ /// @notice Returns fee configuration
214
+ /// @param feeType The fee type identifier
215
+ /// @return config The fee configuration
216
+ function _getFeeConfig(bytes32 feeType) internal view returns (FeeConfig storage config) {
217
+ return FeeManagerStorage.getLayout().feeConfigs[feeType];
218
+ }
219
+
220
+ /// @notice Returns fee hook address
221
+ /// @param feeType The fee type identifier
222
+ /// @return hook The hook contract address
223
+ function _getFeeHook(bytes32 feeType) internal view returns (address hook) {
224
+ FeeConfig storage config = _getFeeConfig(feeType);
225
+ return config.hook;
226
+ }
227
+
228
+ /// @notice Returns protocol fee recipient
229
+ /// @return recipient The protocol fee recipient address
230
+ function _getProtocolFeeRecipient() internal view returns (address recipient) {
231
+ return FeeManagerStorage.getLayout().protocolFeeRecipient;
232
+ }
233
+ }
@@ -0,0 +1,103 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ import {IFeeManager} from "./IFeeManager.sol";
5
+ import {FeeManagerBase} from "./FeeManagerBase.sol";
6
+ import {FeeCalculationMethod, FeeConfig} from "./FeeManagerStorage.sol";
7
+ import {OwnableBase} from "@towns-protocol/diamond/src/facets/ownable/OwnableBase.sol";
8
+ import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
9
+ import {ReentrancyGuardTransient} from "solady/utils/ReentrancyGuardTransient.sol";
10
+
11
+ /// @title FeeManagerFacet
12
+ /// @notice Facet for unified fee management across the protocol
13
+ contract FeeManagerFacet is
14
+ IFeeManager,
15
+ FeeManagerBase,
16
+ OwnableBase,
17
+ Facet,
18
+ ReentrancyGuardTransient
19
+ {
20
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
21
+ /* INITIALIZATION */
22
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
23
+
24
+ function __FeeManagerFacet__init(address protocolRecipient) external onlyInitializing {
25
+ _addInterface(type(IFeeManager).interfaceId);
26
+ __FeeManagerFacet__init_unchained(protocolRecipient);
27
+ }
28
+
29
+ function __FeeManagerFacet__init_unchained(address protocolRecipient) internal {
30
+ _setProtocolFeeRecipient(protocolRecipient);
31
+ }
32
+
33
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
34
+ /* FEE OPERATIONS */
35
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
36
+
37
+ /// @inheritdoc IFeeManager
38
+ function calculateFee(
39
+ bytes32 feeType,
40
+ address user,
41
+ uint256 amount,
42
+ bytes calldata extraData
43
+ ) external view returns (uint256 finalFee) {
44
+ return _calculateFee(feeType, user, amount, extraData);
45
+ }
46
+
47
+ /// @inheritdoc IFeeManager
48
+ function chargeFee(
49
+ bytes32 feeType,
50
+ address user,
51
+ uint256 amount,
52
+ address currency,
53
+ uint256 maxFee,
54
+ bytes calldata extraData
55
+ ) external payable nonReentrant returns (uint256 finalFee) {
56
+ return _chargeFee(feeType, user, amount, currency, maxFee, extraData);
57
+ }
58
+
59
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
60
+ /* CONFIGURATION (OWNER) */
61
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
62
+
63
+ /// @inheritdoc IFeeManager
64
+ function setFeeConfig(
65
+ bytes32 feeType,
66
+ address recipient,
67
+ FeeCalculationMethod method,
68
+ uint16 bps,
69
+ uint128 fixedFee,
70
+ bool enabled
71
+ ) external onlyOwner {
72
+ _setFeeConfig(feeType, recipient, method, bps, fixedFee, enabled);
73
+ }
74
+
75
+ /// @inheritdoc IFeeManager
76
+ function setFeeHook(bytes32 feeType, address hook) external onlyOwner {
77
+ _setFeeHook(feeType, hook);
78
+ }
79
+
80
+ /// @inheritdoc IFeeManager
81
+ function setProtocolFeeRecipient(address recipient) external onlyOwner {
82
+ _setProtocolFeeRecipient(recipient);
83
+ }
84
+
85
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
86
+ /* GETTERS */
87
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
88
+
89
+ /// @inheritdoc IFeeManager
90
+ function getFeeConfig(bytes32 feeType) external view returns (FeeConfig memory config) {
91
+ return _getFeeConfig(feeType);
92
+ }
93
+
94
+ /// @inheritdoc IFeeManager
95
+ function getFeeHook(bytes32 feeType) external view returns (address hook) {
96
+ return _getFeeHook(feeType);
97
+ }
98
+
99
+ /// @inheritdoc IFeeManager
100
+ function getProtocolFeeRecipient() external view returns (address recipient) {
101
+ return _getProtocolFeeRecipient();
102
+ }
103
+ }
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ /// @notice Fee calculation methods
5
+ enum FeeCalculationMethod {
6
+ FIXED, // Flat fee amount
7
+ PERCENT, // Percentage based on BPS
8
+ HYBRID // Maximum of fixed fee or percentage
9
+ }
10
+
11
+ /// @notice Fee configuration for a specific fee type
12
+ /// @param recipient Address to receive the fee
13
+ /// @param lastUpdated Timestamp of last configuration update
14
+ /// @param bps Basis points (1-10000) for percentage calculations
15
+ /// @param method Fee calculation method
16
+ /// @param enabled Whether the fee is active
17
+ /// @param fixedFee Fixed amount in wei for FIXED or HYBRID methods
18
+ struct FeeConfig {
19
+ address recipient; // 20 bytes
20
+ uint48 lastUpdated; // 6 bytes
21
+ uint16 bps; // 2 bytes
22
+ FeeCalculationMethod method; // 1 byte
23
+ bool enabled; // 1 byte
24
+ uint128 fixedFee; // 16 bytes
25
+ address hook; // 20 bytes
26
+ }
27
+
28
+ /// @title FeeManagerStorage
29
+ /// @notice Diamond storage for the FeeManager facet
30
+ library FeeManagerStorage {
31
+ /// @dev Storage slot = keccak256(abi.encode(uint256(keccak256("factory.facets.fee.manager")) - 1)) & ~bytes32(uint256(0xff))
32
+ bytes32 internal constant STORAGE_SLOT =
33
+ 0xabafe0aee5292a745cc432476bbe9b496b2fe07074e3d7dcb579a3e420babf00;
34
+
35
+ struct Layout {
36
+ /// @notice Fee configurations by fee type
37
+ mapping(bytes32 => FeeConfig) feeConfigs;
38
+ /// @notice Protocol fee recipient
39
+ address protocolFeeRecipient;
40
+ }
41
+
42
+ /// @notice Returns the diamond storage layout
43
+ function getLayout() internal pure returns (Layout storage $) {
44
+ assembly {
45
+ $.slot := STORAGE_SLOT
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ /// @title FeeTypesLib
5
+ /// @notice Library defining fee type constants for the FeeManager system
6
+ /// @dev Uses keccak256 for gas-efficient constant generation
7
+ library FeeTypesLib {
8
+ /// @notice Fee for space membership purchases
9
+ bytes32 internal constant MEMBERSHIP = keccak256("FEE_TYPE.MEMBERSHIP");
10
+
11
+ /// @notice Fee for app installations
12
+ bytes32 internal constant APP_INSTALL = keccak256("FEE_TYPE.APP_INSTALL");
13
+
14
+ /// @notice Fee for member-to-member tips
15
+ bytes32 internal constant TIP_MEMBER = keccak256("FEE_TYPE.TIP_MEMBER");
16
+
17
+ /// @notice Fee for bot tips (typically zero)
18
+ bytes32 internal constant TIP_BOT = keccak256("FEE_TYPE.TIP_BOT");
19
+
20
+ /// @notice Protocol fee for swap operations
21
+ bytes32 internal constant SWAP_PROTOCOL = keccak256("FEE_TYPE.SWAP_PROTOCOL");
22
+
23
+ /// @notice Poster fee for swap operations
24
+ bytes32 internal constant SWAP_POSTER = keccak256("FEE_TYPE.SWAP_POSTER");
25
+
26
+ /// @notice Fee for bot actions
27
+ bytes32 internal constant BOT_ACTION = keccak256("FEE_TYPE.BOT_ACTION");
28
+ }
@@ -0,0 +1,86 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ /// @notice Result returned by fee hooks
5
+ /// @param finalFee The final fee amount after hook processing
6
+ /// @param metadata Optional metadata for off-chain consumption
7
+ struct FeeHookResult {
8
+ uint256 finalFee;
9
+ bytes metadata;
10
+ }
11
+
12
+ /// @title IFeeHook
13
+ /// @notice Interface for fee hooks that provide dynamic fee adjustments
14
+ /// @dev Hooks can modify fees, grant exemptions, enforce quotas, or apply complex logic
15
+ ///
16
+ /// ## Hook Execution Flow
17
+ ///
18
+ /// 1. **Read-Only Path (`calculateFee`)**: Used for fee estimation and UI display
19
+ /// - Pure view function with no state changes
20
+ /// - Safe to call from any context
21
+ /// - Used by `FeeManager.calculateFee()`
22
+ ///
23
+ /// 2. **Write Path (`onChargeFee`)**: Used during actual fee charging
24
+ /// - May mutate state (e.g., quota tracking, usage limits)
25
+ /// - Called by `FeeManager.chargeFee()`
26
+ /// - Should include all logic from `calculateFee` plus state updates
27
+ ///
28
+ /// ## Implementation Patterns
29
+ ///
30
+ /// ### Simple Exemption (Staking-Based)
31
+ /// ```solidity
32
+ /// function calculateFee(...) external view returns (FeeHookResult memory) {
33
+ /// bool exempt = stakingRegistry.stakedAmount(user) >= threshold;
34
+ /// return FeeHookResult({
35
+ /// finalFee: exempt ? 0 : baseFee,
36
+ /// metadata: ""
37
+ /// });
38
+ /// }
39
+ ///
40
+ /// function onChargeFee(...) external returns (FeeHookResult memory) {
41
+ /// return calculateFee(...); // No state to update
42
+ /// }
43
+ /// ```
44
+ ///
45
+ /// ### Quota-Based (Future)
46
+ /// ```solidity
47
+ /// function onChargeFee(...) external returns (FeeHookResult memory) {
48
+ /// uint256 used = quotaUsed[user]++;
49
+ /// bool exempt = used < quota[user];
50
+ /// return FeeHookResult({
51
+ /// finalFee: exempt ? 0 : baseFee,
52
+ /// metadata: abi.encode(used, quota[user])
53
+ /// });
54
+ /// }
55
+ /// ```
56
+ interface IFeeHook {
57
+ /// @notice Calculates fee for estimation purposes (view function)
58
+ /// @dev This function MUST be view/pure and not modify state
59
+ /// @dev Used by UIs and contracts for fee previews
60
+ /// @param feeType The type of fee being calculated
61
+ /// @param user The address being charged the fee
62
+ /// @param baseFee The base fee amount before hook processing
63
+ /// @param context Additional context (e.g., amount being tipped, item being purchased)
64
+ /// @return result Fee calculation result with final fee
65
+ function calculateFee(
66
+ bytes32 feeType,
67
+ address user,
68
+ uint256 baseFee,
69
+ bytes calldata context
70
+ ) external view returns (FeeHookResult memory result);
71
+
72
+ /// @notice Processes fee during actual charging (may mutate state)
73
+ /// @dev This function MAY modify state (e.g., quota tracking, usage counts)
74
+ /// @dev Called during actual fee charging via `FeeManager.chargeFee()`
75
+ /// @param feeType The type of fee being charged
76
+ /// @param user The address being charged the fee
77
+ /// @param baseFee The base fee amount before hook processing
78
+ /// @param context Additional context (e.g., amount being tipped, item being purchased)
79
+ /// @return result Fee calculation result with final fee
80
+ function onChargeFee(
81
+ bytes32 feeType,
82
+ address user,
83
+ uint256 baseFee,
84
+ bytes calldata context
85
+ ) external returns (FeeHookResult memory result);
86
+ }
@@ -0,0 +1,177 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ import {FeeCalculationMethod, FeeConfig} from "./FeeManagerStorage.sol";
5
+
6
+ /// @title IFeeManagerBase
7
+ /// @notice Base interface with errors and events
8
+ interface IFeeManagerBase {
9
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
10
+ /* CUSTOM ERRORS */
11
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
12
+
13
+ /// @dev Fee type is not configured or is disabled
14
+ error FeeManager__FeeNotConfigured();
15
+
16
+ /// @dev Invalid basis points (must be 1-10000)
17
+ error FeeManager__InvalidBps();
18
+
19
+ /// @dev Invalid fee recipient (zero address)
20
+ error FeeManager__InvalidRecipient();
21
+
22
+ /// @dev Hook execution failed
23
+ error FeeManager__HookFailed();
24
+
25
+ /// @dev Currency transfer failed
26
+ error FeeManager__TransferFailed();
27
+
28
+ /// @dev Invalid hook (zero address)
29
+ error FeeManager__InvalidHook();
30
+
31
+ /// @dev Invalid method
32
+ error FeeManager__InvalidMethod();
33
+
34
+ /// @dev Max fee exceeded
35
+ error FeeManager__ExceedsMaxFee();
36
+
37
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
38
+ /* EVENTS */
39
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
40
+
41
+ /// @notice Emitted when a fee configuration is updated
42
+ /// @param feeType The fee type identifier
43
+ /// @param recipient Fee recipient address
44
+ /// @param method Calculation method
45
+ /// @param bps Basis points
46
+ /// @param fixedFee Fixed fee amount
47
+ /// @param enabled Whether the fee is enabled
48
+ event FeeConfigured(
49
+ bytes32 indexed feeType,
50
+ address recipient,
51
+ FeeCalculationMethod method,
52
+ uint16 bps,
53
+ uint128 fixedFee,
54
+ bool enabled
55
+ );
56
+
57
+ /// @notice Emitted when a fee hook is set or removed
58
+ /// @param feeType The fee type identifier
59
+ /// @param hook Address of the hook contract (zero address to remove)
60
+ event FeeHookSet(bytes32 indexed feeType, address hook);
61
+
62
+ /// @notice Emitted when the protocol fee recipient is updated
63
+ /// @param recipient New protocol fee recipient
64
+ event ProtocolFeeRecipientSet(address recipient);
65
+
66
+ /// @notice Emitted when a fee is charged
67
+ /// @param feeType The fee type identifier
68
+ /// @param user Address being charged
69
+ /// @param currency Currency contract (address(0) for native token)
70
+ /// @param amount Fee amount charged
71
+ /// @param recipient Fee recipient
72
+ event FeeCharged(
73
+ bytes32 indexed feeType,
74
+ address indexed user,
75
+ address indexed currency,
76
+ uint256 amount,
77
+ address recipient
78
+ );
79
+ }
80
+
81
+ /// @title IFeeManager
82
+ /// @notice Complete interface for the FeeManager facet
83
+ interface IFeeManager is IFeeManagerBase {
84
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
85
+ /* INITIALIZATION */
86
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
87
+
88
+ /// @notice Initializes the FeeManager facet
89
+ /// @param protocolFeeRecipient Protocol fee recipient for all fees
90
+ function __FeeManagerFacet__init(address protocolFeeRecipient) external;
91
+
92
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
93
+ /* FEE OPERATIONS */
94
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
95
+
96
+ /// @notice Calculates fee for estimation purposes (view function)
97
+ /// @dev Does not modify state, safe for UI/contract queries
98
+ /// @param feeType The type of fee to calculate
99
+ /// @param user The address that would be charged
100
+ /// @param amount The base amount for percentage calculations
101
+ /// @param extraData Additional data passed to hooks
102
+ /// @return finalFee The calculated fee amount
103
+ function calculateFee(
104
+ bytes32 feeType,
105
+ address user,
106
+ uint256 amount,
107
+ bytes calldata extraData
108
+ ) external view returns (uint256 finalFee);
109
+
110
+ /// @notice Charges a fee and transfers it to the recipient
111
+ /// @dev Modifies state, calls hook's onChargeFee, and transfers currency
112
+ /// @param feeType The type of fee to charge
113
+ /// @param user The address being charged
114
+ /// @param amount The base amount for percentage calculations
115
+ /// @param currency The currency contract (address(0) for native token)
116
+ /// @param maxFee The maximum fee that can be charged (amount + slippage tolerance)
117
+ /// @param extraData Additional data passed to hooks
118
+ /// @return finalFee The actual fee charged
119
+ function chargeFee(
120
+ bytes32 feeType,
121
+ address user,
122
+ uint256 amount,
123
+ address currency,
124
+ uint256 maxFee,
125
+ bytes calldata extraData
126
+ ) external payable returns (uint256 finalFee);
127
+
128
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
129
+ /* CONFIGURATION (OWNER) */
130
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
131
+
132
+ /// @notice Configures a fee type
133
+ /// @dev Only owner can call
134
+ /// @param feeType The fee type identifier
135
+ /// @param recipient Fee recipient (uses global if zero address)
136
+ /// @param method Calculation method
137
+ /// @param bps Basis points (1-10000, 10000 = 100%)
138
+ /// @param fixedFee Fixed fee amount in wei
139
+ /// @param enabled Whether the fee is active
140
+ function setFeeConfig(
141
+ bytes32 feeType,
142
+ address recipient,
143
+ FeeCalculationMethod method,
144
+ uint16 bps,
145
+ uint128 fixedFee,
146
+ bool enabled
147
+ ) external;
148
+
149
+ /// @notice Sets a fee hook for dynamic fee adjustments
150
+ /// @dev Only owner can call. Set to zero address to remove hook.
151
+ /// @param feeType The fee type identifier
152
+ /// @param hook Address of the hook contract
153
+ function setFeeHook(bytes32 feeType, address hook) external;
154
+
155
+ /// @notice Sets the protocol fee recipient
156
+ /// @dev Only owner can call
157
+ /// @param recipient New protocol fee recipient
158
+ function setProtocolFeeRecipient(address recipient) external;
159
+
160
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
161
+ /* GETTERS */
162
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
163
+
164
+ /// @notice Returns the fee configuration for a fee type
165
+ /// @param feeType The fee type identifier
166
+ /// @return config The fee configuration
167
+ function getFeeConfig(bytes32 feeType) external view returns (FeeConfig memory config);
168
+
169
+ /// @notice Returns the fee hook for a fee type
170
+ /// @param feeType The fee type identifier
171
+ /// @return hook The hook contract address (zero if not set)
172
+ function getFeeHook(bytes32 feeType) external view returns (address hook);
173
+
174
+ /// @notice Returns the protocol fee recipient
175
+ /// @return recipient The protocol fee recipient address
176
+ function getProtocolFeeRecipient() external view returns (address recipient);
177
+ }
@@ -141,12 +141,14 @@ abstract contract AppAccountBase is
141
141
  if (currentAppId == EMPTY_UID) AppNotInstalled.selector.revertWith();
142
142
  if (currentAppId == appId) AppAlreadyInstalled.selector.revertWith();
143
143
 
144
- App memory app = _getAppRegistry().getAppById(appId);
144
+ App memory currentApp = _getAppRegistry().getAppById(currentAppId);
145
145
 
146
146
  // revoke the current app
147
- _revokeGroupAccess(currentAppId, app.client);
147
+ _revokeGroupAccess(currentAppId, currentApp.client);
148
148
  _setGroupStatus(currentAppId, false);
149
149
 
150
+ App memory app = _getAppRegistry().getAppById(appId);
151
+
150
152
  // update the app
151
153
  _addApp(app.module, appId);
152
154
  _setGroupStatus(appId, true, _calcExpiration(appId, app.duration));
@@ -460,18 +460,15 @@ abstract contract ExecutorBase is IExecutorBase {
460
460
  // Fetch restrictions that apply to the caller on the targeted function
461
461
  (bool allowed, uint32 delay) = _canCall(msg.sender, target, selector);
462
462
 
463
- // If call is not authorized, revert
464
- if (!allowed && delay == 0) {
465
- UnauthorizedCall.selector.revertWith();
466
- }
463
+ if (!allowed && delay == 0) UnauthorizedCall.selector.revertWith();
467
464
 
468
465
  bytes32 operationId = _hashOperation(msg.sender, target, data);
466
+ uint48 scheduleTimepoint = _getScheduleTimepoint(operationId);
469
467
 
470
- // If caller is authorized, check operation was scheduled early enough
471
- // Consume an available schedule even if there is no currently enforced delay
472
- if (delay != 0 || _getScheduleTimepoint(operationId) != 0) {
473
- nonce = _consumeScheduledOp(operationId);
474
- }
468
+ if (delay != 0 && scheduleTimepoint == 0) NotScheduled.selector.revertWith();
469
+
470
+ // Consume scheduled operation if one exists
471
+ if (scheduleTimepoint != 0) nonce = _consumeScheduledOp(operationId);
475
472
  }
476
473
 
477
474
  /// @dev Determines if a caller can invoke a function on a target, and if a delay is required.
@@ -4,15 +4,18 @@ pragma solidity ^0.8.23;
4
4
  // interfaces
5
5
  import {ITippingBase} from "./ITipping.sol";
6
6
  import {ITownsPointsBase} from "../../../airdrop/points/ITownsPoints.sol";
7
- import {IPlatformRequirements} from "../../../factory/facets/platform/requirements/IPlatformRequirements.sol";
7
+ import {IFeeManager} from "../../../factory/facets/fee/IFeeManager.sol";
8
+ import {FeeTypesLib} from "../../../factory/facets/fee/FeeTypesLib.sol";
9
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8
10
 
9
11
  // libraries
10
12
  import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
11
- import {BasisPoints} from "../../../utils/libraries/BasisPoints.sol";
12
13
  import {CurrencyTransfer} from "../../../utils/libraries/CurrencyTransfer.sol";
13
14
  import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
14
15
  import {MembershipStorage} from "../membership/MembershipStorage.sol";
15
16
  import {TippingStorage} from "./TippingStorage.sol";
17
+ import {BasisPoints} from "../../../utils/libraries/BasisPoints.sol";
18
+ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
16
19
 
17
20
  // contracts
18
21
  import {PointsBase} from "../points/PointsBase.sol";
@@ -21,6 +24,8 @@ abstract contract TippingBase is ITippingBase, PointsBase {
21
24
  using EnumerableSet for EnumerableSet.AddressSet;
22
25
  using CustomRevert for bytes4;
23
26
 
27
+ uint256 internal constant MAX_FEE_TOLERANCE = 100; // 1% tolerance
28
+
24
29
  /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
25
30
  /* Internal Functions */
26
31
  /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
@@ -45,20 +50,15 @@ abstract contract TippingBase is ITippingBase, PointsBase {
45
50
  tipRequest.amount
46
51
  );
47
52
 
48
- uint256 tipAmount = tipRequest.amount;
53
+ // Validate payment and transfer tokens to space
54
+ _validateAndTransferPayment(tipRequest.currency, tipRequest.amount);
49
55
 
50
- // Handle native token
51
- if (tipRequest.currency == CurrencyTransfer.NATIVE_TOKEN) {
52
- if (msg.value != tipAmount) MsgValueMismatch.selector.revertWith();
53
- uint256 protocolFee = _handleProtocolFee(tipAmount);
54
- tipAmount -= protocolFee;
55
- } else {
56
- if (msg.value != 0) UnexpectedETH.selector.revertWith();
57
- }
56
+ // Charge protocol fee and calculate net tip amount
57
+ uint256 protocolFee = _handleProtocolFee(tipRequest.currency, tipRequest.amount);
58
+ uint256 tipAmount = tipRequest.amount - protocolFee;
58
59
 
59
60
  // Process tip
60
61
  _processTip(
61
- msg.sender,
62
62
  tipRequest.receiver,
63
63
  tipRequest.tokenId,
64
64
  TipRecipientType.Member,
@@ -93,20 +93,15 @@ abstract contract TippingBase is ITippingBase, PointsBase {
93
93
  MembershipTipParams memory params = abi.decode(data, (MembershipTipParams));
94
94
  _validateTipRequest(msg.sender, params.receiver, params.currency, params.amount);
95
95
 
96
- uint256 tipAmount = params.amount;
96
+ // Validate payment and transfer tokens to space
97
+ _validateAndTransferPayment(params.currency, params.amount);
97
98
 
98
- // Handle native token
99
- if (params.currency == CurrencyTransfer.NATIVE_TOKEN) {
100
- if (msg.value != tipAmount) MsgValueMismatch.selector.revertWith();
101
- uint256 protocolFee = _handleProtocolFee(tipAmount);
102
- tipAmount -= protocolFee;
103
- } else {
104
- if (msg.value != 0) UnexpectedETH.selector.revertWith();
105
- }
99
+ // Charge protocol fee and calculate net tip amount
100
+ uint256 protocolFee = _handleProtocolFee(params.currency, params.amount);
101
+ uint256 tipAmount = params.amount - protocolFee;
106
102
 
107
103
  // Process tip
108
104
  _processTip(
109
- msg.sender,
110
105
  params.receiver,
111
106
  params.tokenId,
112
107
  TipRecipientType.Member,
@@ -139,24 +134,15 @@ abstract contract TippingBase is ITippingBase, PointsBase {
139
134
  function _sendBotTip(bytes calldata data) internal {
140
135
  BotTipParams memory params = abi.decode(data, (BotTipParams));
141
136
  _validateTipRequest(msg.sender, params.receiver, params.currency, params.amount);
142
-
143
- uint256 tipAmount = params.amount;
144
-
145
- // Handle native token (no protocol fee for bot tips)
146
- if (params.currency == CurrencyTransfer.NATIVE_TOKEN) {
147
- if (msg.value != tipAmount) MsgValueMismatch.selector.revertWith();
148
- } else {
149
- if (msg.value != 0) UnexpectedETH.selector.revertWith();
150
- }
137
+ _validateAndTransferPayment(params.currency, params.amount);
151
138
 
152
139
  // Process tip (tokenId = 0 for bot tips)
153
140
  _processTip(
154
- msg.sender,
155
141
  params.receiver,
156
142
  0, // No tokenId for bot tips
157
143
  TipRecipientType.Bot,
158
144
  params.currency,
159
- tipAmount
145
+ params.amount
160
146
  );
161
147
 
162
148
  emit TipSent(
@@ -164,14 +150,13 @@ abstract contract TippingBase is ITippingBase, PointsBase {
164
150
  params.receiver,
165
151
  TipRecipientType.Bot,
166
152
  params.currency,
167
- tipAmount,
153
+ params.amount,
168
154
  params.metadata.data
169
155
  );
170
156
  }
171
157
 
172
158
  /// @dev Core tip processing logic
173
159
  function _processTip(
174
- address sender,
175
160
  address receiver,
176
161
  uint256 tokenId,
177
162
  TipRecipientType recipientType,
@@ -198,24 +183,63 @@ abstract contract TippingBase is ITippingBase, PointsBase {
198
183
  }
199
184
 
200
185
  // Transfer currency
201
- CurrencyTransfer.transferCurrency(currency, sender, receiver, amount);
186
+ CurrencyTransfer.transferCurrency(currency, address(this), receiver, amount);
187
+ }
188
+
189
+ /// @dev Validates payment and transfers tokens to space contract
190
+ /// @param currency The currency being tipped (NATIVE_TOKEN or ERC20)
191
+ /// @param amount The amount being tipped
192
+ function _validateAndTransferPayment(address currency, uint256 amount) internal {
193
+ if (currency == CurrencyTransfer.NATIVE_TOKEN) {
194
+ if (msg.value != amount) MsgValueMismatch.selector.revertWith();
195
+ } else {
196
+ if (msg.value != 0) UnexpectedETH.selector.revertWith();
197
+ CurrencyTransfer.transferCurrency(currency, msg.sender, address(this), amount);
198
+ }
202
199
  }
203
200
 
204
- /// @dev Handles protocol fee and points minting
205
- function _handleProtocolFee(uint256 amount) internal returns (uint256 protocolFee) {
201
+ /// @dev Handles protocol fee charging and points minting
202
+ /// @param amount The tip amount to calculate fee on
203
+ /// @param currency The currency of the tip (NATIVE_TOKEN or ERC20 address)
204
+ /// @return protocolFee The fee amount charged
205
+ function _handleProtocolFee(
206
+ address currency,
207
+ uint256 amount
208
+ ) internal returns (uint256 protocolFee) {
206
209
  MembershipStorage.Layout storage ds = MembershipStorage.layout();
207
- IPlatformRequirements platform = IPlatformRequirements(ds.spaceFactory);
210
+ address spaceFactory = ds.spaceFactory;
211
+
212
+ // Calculate expected fee
213
+ uint256 expectedFee = IFeeManager(spaceFactory).calculateFee(
214
+ FeeTypesLib.TIP_MEMBER,
215
+ msg.sender,
216
+ amount,
217
+ ""
218
+ );
219
+
220
+ if (expectedFee == 0) return 0;
208
221
 
209
- protocolFee = BasisPoints.calculate(amount, 50); // 0.5%
222
+ // Add slippage tolerance (1%)
223
+ uint256 maxFee = expectedFee + BasisPoints.calculate(expectedFee, MAX_FEE_TOLERANCE);
210
224
 
211
- CurrencyTransfer.transferCurrency(
212
- CurrencyTransfer.NATIVE_TOKEN,
225
+ // Approve ERC20 if needed (native token sends value with call)
226
+ bool isNative = currency == CurrencyTransfer.NATIVE_TOKEN;
227
+ if (!isNative) SafeTransferLib.safeApproveWithRetry(currency, spaceFactory, maxFee);
228
+
229
+ // Charge fee (excess native token will be refunded)
230
+ protocolFee = IFeeManager(spaceFactory).chargeFee{value: isNative ? maxFee : 0}(
231
+ FeeTypesLib.TIP_MEMBER,
213
232
  msg.sender,
214
- platform.getFeeRecipient(),
215
- protocolFee
233
+ amount,
234
+ currency,
235
+ maxFee,
236
+ ""
216
237
  );
217
238
 
218
- // Mint points
239
+ // Reset ERC20 approval
240
+ if (!isNative) SafeTransferLib.safeApprove(currency, spaceFactory, 0);
241
+
242
+ // Mint points for fee payment
219
243
  address airdropDiamond = _getAirdropDiamond();
220
244
  uint256 points = _getPoints(
221
245
  airdropDiamond,
@@ -96,4 +96,32 @@ library CurrencyTransfer {
96
96
  _nativeTokenWrapper.safeTransfer(to, value);
97
97
  }
98
98
  }
99
+
100
+ /// @notice Transfers fee amount and refunds excess native token
101
+ /// @dev For native tokens, the subtraction maxPaid - actualFee will underflow and revert if actualFee > maxPaid.
102
+ /// @param currency The currency (address(0) for native)
103
+ /// @param payer The payer to refund
104
+ /// @param recipient The fee recipient
105
+ /// @param actualFee The actual fee amount to charge
106
+ /// @param maxPaid The maximum amount paid (msg.value)
107
+ function transferFeeWithRefund(
108
+ address currency,
109
+ address payer,
110
+ address recipient,
111
+ uint256 actualFee,
112
+ uint256 maxPaid
113
+ ) internal {
114
+ if (currency == NATIVE_TOKEN) {
115
+ // Transfer fee to recipient if non-zero
116
+ if (actualFee > 0) safeTransferNativeToken(recipient, actualFee);
117
+
118
+ // NOTE: uint256 underflow here (maxPaid - actualFee) will revert if actualFee > maxPaid,
119
+ // acting as a slippage check (never over-withdraws from msg.value).
120
+ uint256 refund = maxPaid - actualFee;
121
+ if (refund > 0) safeTransferNativeToken(payer, refund);
122
+ } else {
123
+ // ERC20: only transfer if actualFee > 0
124
+ if (actualFee > 0) safeTransferERC20(currency, payer, recipient, actualFee);
125
+ }
126
+ }
99
127
  }