@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 +3 -4
- package/scripts/deployments/diamonds/DeploySpaceFactory.s.sol +16 -1
- package/scripts/deployments/facets/DeployFeeManager.s.sol +46 -0
- package/scripts/interactions/{diamonds/InteractAppRegistry.s.sol → InteractAppRegistry.s.sol} +8 -8
- package/scripts/interactions/InteractPostDeploy.s.sol +11 -1
- package/scripts/interactions/InteractSpace.s.sol +40 -0
- package/scripts/interactions/InteractSpaceFactory.s.sol +71 -0
- package/src/apps/facets/registry/IAppRegistry.sol +0 -1
- package/src/base/registry/facets/xchain/XChainCheckLib.sol +4 -3
- package/src/factory/facets/fee/FeeManagerBase.sol +233 -0
- package/src/factory/facets/fee/FeeManagerFacet.sol +103 -0
- package/src/factory/facets/fee/FeeManagerStorage.sol +48 -0
- package/src/factory/facets/fee/FeeTypesLib.sol +28 -0
- package/src/factory/facets/fee/IFeeHook.sol +86 -0
- package/src/factory/facets/fee/IFeeManager.sol +177 -0
- package/src/spaces/facets/account/AppAccountBase.sol +4 -2
- package/src/spaces/facets/executor/ExecutorBase.sol +6 -9
- package/src/spaces/facets/tipping/TippingBase.sol +69 -45
- package/src/utils/libraries/CurrencyTransfer.sol +28 -0
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towns-protocol/contracts",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
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": "
|
|
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.
|
|
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
|
+
}
|
package/scripts/interactions/{diamonds/InteractAppRegistry.s.sol → InteractAppRegistry.s.sol}
RENAMED
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
pragma solidity ^0.8.23;
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
|
-
import {
|
|
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 "
|
|
12
|
-
import {AlphaHelper} from "
|
|
13
|
-
import {DeployAppRegistry} from "
|
|
14
|
-
import {DeploySimpleAppBeacon} from "
|
|
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 "
|
|
18
|
-
import {DeployAppInstallerFacet} from "
|
|
19
|
-
import {DeployAppFactoryFacet} from "
|
|
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 {
|
|
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
|
|
60
|
+
uint256 voteCount = self.votes[requestId].length;
|
|
61
61
|
bool voteRecorded = false;
|
|
62
62
|
|
|
63
|
-
for (uint256 i; 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 <
|
|
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
|
|
144
|
+
App memory currentApp = _getAppRegistry().getAppById(currentAppId);
|
|
145
145
|
|
|
146
146
|
// revoke the current app
|
|
147
|
-
_revokeGroupAccess(currentAppId,
|
|
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
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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 {
|
|
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
|
-
|
|
53
|
+
// Validate payment and transfer tokens to space
|
|
54
|
+
_validateAndTransferPayment(tipRequest.currency, tipRequest.amount);
|
|
49
55
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
96
|
+
// Validate payment and transfer tokens to space
|
|
97
|
+
_validateAndTransferPayment(params.currency, params.amount);
|
|
97
98
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
+
// Add slippage tolerance (1%)
|
|
223
|
+
uint256 maxFee = expectedFee + BasisPoints.calculate(expectedFee, MAX_FEE_TOLERANCE);
|
|
210
224
|
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
233
|
+
amount,
|
|
234
|
+
currency,
|
|
235
|
+
maxFee,
|
|
236
|
+
""
|
|
216
237
|
);
|
|
217
238
|
|
|
218
|
-
//
|
|
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
|
}
|