@croptop/core-v6 0.0.61 → 0.0.65
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/README.md +1 -1
- package/package.json +10 -9
- package/script/Deploy.s.sol +4 -1
- package/src/CTPublisher.sol +515 -115
- package/src/interfaces/ICTPublisher.sol +22 -4
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Croptop is built around three ideas:
|
|
|
24
24
|
- publishers call `mintFrom` to create or reuse 721 tiers that represent their post
|
|
25
25
|
- a one-click deployer can create a full Juicebox project, its 721 hook config, and its posting rules in one transaction
|
|
26
26
|
|
|
27
|
-
Every mint collects a 5% Croptop fee unless the target project is itself the fee project. If the fee terminal rejects
|
|
27
|
+
Every mint collects a 5% Croptop fee unless the target project is itself the fee project. `CTPublisher.mintFrom` takes a terminal `token` and `amount`, like a Juicebox terminal payment: native-token mints use `msg.value`, while ERC-20 mints pull `amount` from `_msgSender()` using either direct approval or a publisher-targeted Permit2 metadata entry. Croptop converts the hook's tier price into the selected terminal token's accounting units, using the hook's `PRICES` oracle when the payment currency differs from the hook pricing currency. The project terminal still decides whether the token is accepted, and the transaction reverts if the project payment does not mint the requested NFTs to the beneficiary. If the fee terminal is missing or rejects the fee payment, Croptop refunds the fee portion to `_msgSender()` and still lets the publish continue. Native refunds can still revert if `_msgSender()` cannot receive ETH.
|
|
28
28
|
|
|
29
29
|
Use this repo when the product is permissioned publishing on top of a Juicebox project. Do not use it for plain 721 tier sales.
|
|
30
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@croptop/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.65",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,17 +26,18 @@
|
|
|
26
26
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'croptop-core-v6'"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@bananapus/721-hook-v6": "^0.0.
|
|
30
|
-
"@bananapus/core-v6": "^0.0.
|
|
31
|
-
"@bananapus/ownable-v6": "^0.0.
|
|
32
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
33
|
-
"@bananapus/router-terminal-v6": "^0.0.
|
|
34
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
29
|
+
"@bananapus/721-hook-v6": "^0.0.65",
|
|
30
|
+
"@bananapus/core-v6": "^0.0.78",
|
|
31
|
+
"@bananapus/ownable-v6": "^0.0.34",
|
|
32
|
+
"@bananapus/permission-ids-v6": "^0.0.28",
|
|
33
|
+
"@bananapus/router-terminal-v6": "^0.0.60",
|
|
34
|
+
"@bananapus/suckers-v6": "^0.0.67",
|
|
35
35
|
"@openzeppelin/contracts": "5.6.1",
|
|
36
|
-
"@rev-net/core-v6": "^0.0.
|
|
36
|
+
"@rev-net/core-v6": "^0.0.84",
|
|
37
|
+
"@uniswap/permit2": "github:Uniswap/permit2#cc56ad0f3439c502c246fc5cfcc3db92bb8b7219"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"@bananapus/address-registry-v6": "^0.0.
|
|
40
|
+
"@bananapus/address-registry-v6": "^0.0.32",
|
|
40
41
|
"@sphinx-labs/plugins": "0.33.3"
|
|
41
42
|
}
|
|
42
43
|
}
|
package/script/Deploy.s.sol
CHANGED
|
@@ -6,6 +6,7 @@ import {SuckerDeployment, SuckerDeploymentLib} from "@bananapus/suckers-v6/scrip
|
|
|
6
6
|
|
|
7
7
|
import {Sphinx} from "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
|
|
8
8
|
import {Script} from "forge-std/Script.sol";
|
|
9
|
+
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
9
10
|
|
|
10
11
|
import {CTDeployer} from "./../src/CTDeployer.sol";
|
|
11
12
|
import {CTProjectOwner} from "./../src/CTProjectOwner.sol";
|
|
@@ -27,6 +28,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
27
28
|
bytes32 private constant _PUBLISHER_SALT = "_PUBLISHER_SALTV6_";
|
|
28
29
|
bytes32 private constant _DEPLOYER_SALT = "_DEPLOYER_SALTV6_";
|
|
29
30
|
bytes32 private constant _PROJECT_OWNER_SALT = "_PROJECT_OWNER_SALTV6_";
|
|
31
|
+
IPermit2 private constant _PERMIT2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3);
|
|
30
32
|
address private trustedForwarder;
|
|
31
33
|
|
|
32
34
|
function configureSphinx() public override {
|
|
@@ -69,7 +71,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
69
71
|
(address _publisher, bool _publisherIsDeployed) = _isDeployed({
|
|
70
72
|
salt: _PUBLISHER_SALT,
|
|
71
73
|
creationCode: type(CTPublisher).creationCode,
|
|
72
|
-
arguments: abi.encode(core.directory, core.permissions, feeProjectId, trustedForwarder)
|
|
74
|
+
arguments: abi.encode(core.directory, core.permissions, feeProjectId, _PERMIT2, trustedForwarder)
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
// Deploy it if it has not been deployed yet.
|
|
@@ -78,6 +80,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
78
80
|
directory: core.directory,
|
|
79
81
|
permissions: core.permissions,
|
|
80
82
|
feeProjectId: feeProjectId,
|
|
83
|
+
permit2: _PERMIT2,
|
|
81
84
|
trustedForwarder: trustedForwarder
|
|
82
85
|
})
|
|
83
86
|
: CTPublisher(_publisher);
|
package/src/CTPublisher.sol
CHANGED
|
@@ -9,13 +9,22 @@ import {JB721TierConfigFlags} from "@bananapus/721-hook-v6/src/structs/JB721Tier
|
|
|
9
9
|
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
10
10
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
11
11
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
12
|
+
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
12
13
|
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
13
14
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
15
|
+
import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
|
|
14
16
|
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
17
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
18
|
+
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
15
19
|
import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
|
|
16
20
|
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
17
21
|
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
22
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
23
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
18
24
|
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
25
|
+
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
|
|
26
|
+
import {IAllowanceTransfer} from "@uniswap/permit2/src/interfaces/IAllowanceTransfer.sol";
|
|
27
|
+
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
19
28
|
|
|
20
29
|
import {ICTPublisher} from "./interfaces/ICTPublisher.sol";
|
|
21
30
|
import {CTAllowedPost} from "./structs/CTAllowedPost.sol";
|
|
@@ -27,23 +36,40 @@ import {CTPost} from "./structs/CTPost.sol";
|
|
|
27
36
|
/// remainder into the project's terminal. Duplicate IPFS URIs are tracked so subsequent mints of the same content reuse
|
|
28
37
|
/// the existing tier rather than creating a new one.
|
|
29
38
|
contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
39
|
+
// A library that handles optional-return ERC-20 operations.
|
|
40
|
+
using SafeERC20 for IERC20;
|
|
41
|
+
|
|
30
42
|
//*********************************************************************//
|
|
31
43
|
// --------------------------- custom errors ------------------------- //
|
|
32
44
|
//*********************************************************************//
|
|
33
45
|
|
|
46
|
+
error CTPublisher_DuplicatePayMetadata(bytes4 payMetadataId);
|
|
34
47
|
error CTPublisher_DuplicatePost(bytes32 encodedIpfsUri);
|
|
35
48
|
error CTPublisher_EmptyEncodedIpfsUri(uint256 postIndex);
|
|
49
|
+
error CTPublisher_FeePaymentFailed(uint256 feeAmount);
|
|
36
50
|
error CTPublisher_InsufficientEthSent(uint256 expected, uint256 sent);
|
|
51
|
+
error CTPublisher_InsufficientPayment(uint256 expected, uint256 sent);
|
|
52
|
+
error CTPublisher_InvalidFeeBeneficiary();
|
|
53
|
+
error CTPublisher_InvalidPaymentTokenContext(
|
|
54
|
+
address token, uint256 tokenCurrency, uint256 tokenDecimals, uint256 pricingCurrency, uint256 pricingDecimals
|
|
55
|
+
);
|
|
37
56
|
error CTPublisher_MaxTotalSupplyLessThanMin(uint256 min, uint256 max);
|
|
57
|
+
error CTPublisher_MintNotDelivered(
|
|
58
|
+
address hook, address beneficiary, uint256 expectedBalance, uint256 actualBalance
|
|
59
|
+
);
|
|
60
|
+
error CTPublisher_MsgValueNotAllowed(uint256 value);
|
|
61
|
+
error CTPublisher_NativeTokenAmountMismatch(uint256 amount, uint256 msgValue);
|
|
62
|
+
error CTPublisher_NoPosts(address caller);
|
|
38
63
|
error CTPublisher_NotInAllowList(address addr, address[] allowedAddresses);
|
|
64
|
+
error CTPublisher_OverflowAlert(uint256 value, uint256 limit);
|
|
65
|
+
error CTPublisher_PermitAllowanceNotEnough(uint256 amount, uint256 allowance);
|
|
66
|
+
error CTPublisher_PriceFeedUnavailable(uint256 paymentCurrency, uint256 pricingCurrency);
|
|
39
67
|
error CTPublisher_PriceTooSmall(uint256 price, uint256 minimumPrice);
|
|
40
|
-
error
|
|
41
|
-
error CTPublisher_FeePaymentFailed(uint256 feeAmount);
|
|
68
|
+
error CTPublisher_ReentrantTokenTransfer(address token);
|
|
42
69
|
error CTPublisher_SplitPercentExceedsMaximum(uint256 splitPercent, uint256 maximumSplitPercent);
|
|
70
|
+
error CTPublisher_TemporaryAllowanceNotConsumed(address token, address spender, uint256 allowance);
|
|
43
71
|
error CTPublisher_TotalSupplyTooBig(uint256 totalSupply, uint256 maximumTotalSupply);
|
|
44
72
|
error CTPublisher_TotalSupplyTooSmall(uint256 totalSupply, uint256 minimumTotalSupply);
|
|
45
|
-
error CTPublisher_NoPosts(address caller);
|
|
46
|
-
error CTPublisher_InvalidFeeBeneficiary();
|
|
47
73
|
error CTPublisher_UnauthorizedToPostInCategory(address hook, uint24 category);
|
|
48
74
|
error CTPublisher_ZeroTotalSupply(address hook, uint24 category);
|
|
49
75
|
|
|
@@ -65,6 +91,9 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
65
91
|
/// @notice The ID of the project to which fees will be routed.
|
|
66
92
|
uint256 public immutable override FEE_PROJECT_ID;
|
|
67
93
|
|
|
94
|
+
/// @notice The Permit2 utility used to pull ERC-20 payments from posters.
|
|
95
|
+
IPermit2 public immutable override PERMIT2;
|
|
96
|
+
|
|
68
97
|
//*********************************************************************//
|
|
69
98
|
// --------------------- public stored properties -------------------- //
|
|
70
99
|
//*********************************************************************//
|
|
@@ -89,6 +118,13 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
89
118
|
/// @custom:param category The category for which the allowance applies.
|
|
90
119
|
mapping(address hook => mapping(uint256 category => uint256)) internal _packedAllowanceFor;
|
|
91
120
|
|
|
121
|
+
//*********************************************************************//
|
|
122
|
+
// ------------------- transient stored properties ------------------- //
|
|
123
|
+
//*********************************************************************//
|
|
124
|
+
|
|
125
|
+
/// @notice Whether this publisher is currently measuring an incoming ERC-20 balance delta.
|
|
126
|
+
bool internal transient _acceptingToken;
|
|
127
|
+
|
|
92
128
|
//*********************************************************************//
|
|
93
129
|
// -------------------------- constructor ---------------------------- //
|
|
94
130
|
//*********************************************************************//
|
|
@@ -96,11 +132,13 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
96
132
|
/// @param directory The directory that contains the projects to post to.
|
|
97
133
|
/// @param permissions A contract storing permissions.
|
|
98
134
|
/// @param feeProjectId The ID of the project to which fees will be routed.
|
|
135
|
+
/// @param permit2 The Permit2 utility used to pull ERC-20 payments from posters.
|
|
99
136
|
/// @param trustedForwarder The trusted forwarder for the ERC2771Context.
|
|
100
137
|
constructor(
|
|
101
138
|
IJBDirectory directory,
|
|
102
139
|
IJBPermissions permissions,
|
|
103
140
|
uint256 feeProjectId,
|
|
141
|
+
IPermit2 permit2,
|
|
104
142
|
address trustedForwarder
|
|
105
143
|
)
|
|
106
144
|
JBPermissioned(permissions)
|
|
@@ -108,6 +146,7 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
108
146
|
{
|
|
109
147
|
DIRECTORY = directory;
|
|
110
148
|
FEE_PROJECT_ID = feeProjectId;
|
|
149
|
+
PERMIT2 = permit2;
|
|
111
150
|
}
|
|
112
151
|
|
|
113
152
|
//*********************************************************************//
|
|
@@ -184,12 +223,17 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
184
223
|
/// @dev Reverts if any post violates the category's configured allowance (price, supply, split, allowlist).
|
|
185
224
|
/// @param hook The hook to mint from.
|
|
186
225
|
/// @param posts An array of posts that should be published as NFTs to the specified project.
|
|
226
|
+
/// @param token The terminal token to pay with.
|
|
227
|
+
/// @param amount The total token amount supplied for the post payment and Croptop fee.
|
|
187
228
|
/// @param nftBeneficiary The beneficiary of the NFT mints.
|
|
188
229
|
/// @param feeBeneficiary The beneficiary of the fee project's token.
|
|
189
230
|
/// @param additionalPayMetadata Metadata bytes to include in the payment after Croptop prepends NFT mint metadata.
|
|
231
|
+
/// Include a Permit2 entry targeted to this publisher to pay ERC-20s without a direct publisher approval.
|
|
190
232
|
function mintFrom(
|
|
191
233
|
IJB721TiersHook hook,
|
|
192
234
|
CTPost[] calldata posts,
|
|
235
|
+
address token,
|
|
236
|
+
uint256 amount,
|
|
193
237
|
address nftBeneficiary,
|
|
194
238
|
address feeBeneficiary,
|
|
195
239
|
bytes calldata additionalPayMetadata
|
|
@@ -197,6 +241,207 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
197
241
|
external
|
|
198
242
|
payable
|
|
199
243
|
override
|
|
244
|
+
{
|
|
245
|
+
uint256 acceptedAmount = _acceptFundsFor({token: token, amount: amount, metadata: additionalPayMetadata});
|
|
246
|
+
|
|
247
|
+
_mintFrom({
|
|
248
|
+
hook: hook,
|
|
249
|
+
posts: posts,
|
|
250
|
+
token: token,
|
|
251
|
+
amount: acceptedAmount,
|
|
252
|
+
nftBeneficiary: nftBeneficiary,
|
|
253
|
+
feeBeneficiary: feeBeneficiary,
|
|
254
|
+
additionalPayMetadata: additionalPayMetadata
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//*********************************************************************//
|
|
259
|
+
// ------------------------- external views -------------------------- //
|
|
260
|
+
//*********************************************************************//
|
|
261
|
+
|
|
262
|
+
/// @notice Get the tiers for the provided encoded IPFS URIs.
|
|
263
|
+
/// @dev The returned tier IDs may be stale if the corresponding tiers were removed externally via adjustTiers.
|
|
264
|
+
/// In that case, the store's tierOf call will return a tier with default/empty values. Callers should check
|
|
265
|
+
/// tier fields before relying on them.
|
|
266
|
+
/// @param hook The hook from which to get tiers.
|
|
267
|
+
/// @param encodedIpfsUris The URIs to get tiers of.
|
|
268
|
+
/// @return tiers The tiers that correspond to the provided encoded IPFS URIs.
|
|
269
|
+
function tiersFor(
|
|
270
|
+
address hook,
|
|
271
|
+
bytes32[] memory encodedIpfsUris
|
|
272
|
+
)
|
|
273
|
+
external
|
|
274
|
+
view
|
|
275
|
+
override
|
|
276
|
+
returns (JB721Tier[] memory tiers)
|
|
277
|
+
{
|
|
278
|
+
// Set the number of tiers.
|
|
279
|
+
tiers = new JB721Tier[](encodedIpfsUris.length);
|
|
280
|
+
|
|
281
|
+
// Get the hook's store.
|
|
282
|
+
IJB721TiersHookStore store = IJB721TiersHook(hook).STORE();
|
|
283
|
+
|
|
284
|
+
// For each encoded IPFS URI being checked.
|
|
285
|
+
for (uint256 i; i < encodedIpfsUris.length;) {
|
|
286
|
+
// Get the tier ID the URI is stored in.
|
|
287
|
+
uint256 tierId = tierIdForEncodedIpfsUriOf[hook][encodedIpfsUris[i]];
|
|
288
|
+
|
|
289
|
+
// Get the tier.
|
|
290
|
+
tiers[i] = store.tierOf({hook: hook, id: tierId, includeResolvedUri: true});
|
|
291
|
+
|
|
292
|
+
unchecked {
|
|
293
|
+
++i;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
//*********************************************************************//
|
|
299
|
+
// -------------------------- public views --------------------------- //
|
|
300
|
+
//*********************************************************************//
|
|
301
|
+
|
|
302
|
+
/// @notice Get the allowance for the provided hook and category.
|
|
303
|
+
/// @param hook The hook to get the allowance for.
|
|
304
|
+
/// @param category The category to get the allowance for.
|
|
305
|
+
/// @return minimumPrice The minimum price.
|
|
306
|
+
/// @return minimumTotalSupply The minimum total supply.
|
|
307
|
+
/// @return maximumTotalSupply The maximum total supply.
|
|
308
|
+
/// @return maximumSplitPercent The maximum split percent.
|
|
309
|
+
/// @return allowedAddresses The addresses that are allowed to post.
|
|
310
|
+
function allowanceFor(
|
|
311
|
+
address hook,
|
|
312
|
+
uint256 category
|
|
313
|
+
)
|
|
314
|
+
public
|
|
315
|
+
view
|
|
316
|
+
override
|
|
317
|
+
returns (
|
|
318
|
+
uint256 minimumPrice,
|
|
319
|
+
uint256 minimumTotalSupply,
|
|
320
|
+
uint256 maximumTotalSupply,
|
|
321
|
+
uint256 maximumSplitPercent,
|
|
322
|
+
address[] memory allowedAddresses
|
|
323
|
+
)
|
|
324
|
+
{
|
|
325
|
+
// Get a reference to the packed allowance for the hook and category.
|
|
326
|
+
uint256 packed = _packedAllowanceFor[hook][category];
|
|
327
|
+
|
|
328
|
+
// Get a reference to the minimum price.
|
|
329
|
+
minimumPrice = packed & ((1 << 104) - 1);
|
|
330
|
+
// Get a reference to the minimum total supply.
|
|
331
|
+
minimumTotalSupply = (packed >> 104) & ((1 << 32) - 1);
|
|
332
|
+
// Get a reference to the maximum total supply.
|
|
333
|
+
maximumTotalSupply = (packed >> 136) & ((1 << 32) - 1);
|
|
334
|
+
// Get a reference to the maximum split percent.
|
|
335
|
+
maximumSplitPercent = (packed >> 168) & ((1 << 32) - 1);
|
|
336
|
+
|
|
337
|
+
allowedAddresses = _allowedAddresses[hook][category];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
//*********************************************************************//
|
|
341
|
+
// ---------------------- internal transactions ---------------------- //
|
|
342
|
+
//*********************************************************************//
|
|
343
|
+
|
|
344
|
+
/// @notice Accept incoming native or ERC-20 funds from the caller.
|
|
345
|
+
/// @param token The token to accept.
|
|
346
|
+
/// @param amount The number of tokens to accept.
|
|
347
|
+
/// @param metadata The metadata in which optional Permit2 context is provided.
|
|
348
|
+
/// @return acceptedAmount The number of tokens that were received.
|
|
349
|
+
function _acceptFundsFor(
|
|
350
|
+
address token,
|
|
351
|
+
uint256 amount,
|
|
352
|
+
bytes calldata metadata
|
|
353
|
+
)
|
|
354
|
+
internal
|
|
355
|
+
returns (uint256 acceptedAmount)
|
|
356
|
+
{
|
|
357
|
+
// Native-token payments arrive as msg.value, so they never need ERC-20 pulls or Permit2 handling.
|
|
358
|
+
if (token == JBConstants.NATIVE_TOKEN) {
|
|
359
|
+
// Keep the explicit `amount` aligned with msg.value because downstream fee math uses `amount`.
|
|
360
|
+
if (amount != msg.value) {
|
|
361
|
+
revert CTPublisher_NativeTokenAmountMismatch({amount: amount, msgValue: msg.value});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ETH cannot be fee-on-transfer, so the accepted amount is exactly the supplied value.
|
|
365
|
+
return amount;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ERC-20 payments must not also carry ETH, otherwise the native value would be trapped in this contract.
|
|
369
|
+
if (msg.value != 0) revert CTPublisher_MsgValueNotAllowed({value: msg.value});
|
|
370
|
+
|
|
371
|
+
// Permit2 data is optional and lives in the caller's pay metadata so `mintFrom` can keep one public signature.
|
|
372
|
+
(bool hasPermit2Allowance, bytes memory permit2Data) =
|
|
373
|
+
JBMetadataResolver.getDataFor({id: JBMetadataResolver.getId("permit2"), metadata: metadata});
|
|
374
|
+
|
|
375
|
+
if (hasPermit2Allowance) {
|
|
376
|
+
// Decode only when present so ordinary ERC-20 approvals and native payments do not need dummy structs.
|
|
377
|
+
JBSingleAllowance memory permit2Allowance = abi.decode(permit2Data, (JBSingleAllowance));
|
|
378
|
+
// The signed allowance must cover the requested pull; otherwise Permit2 could approve less than this
|
|
379
|
+
// publish path is about to try to collect.
|
|
380
|
+
if (amount > permit2Allowance.amount) {
|
|
381
|
+
revert CTPublisher_PermitAllowanceNotEnough({amount: amount, allowance: permit2Allowance.amount});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Rebuild Permit2's expected struct with this token and this publisher as spender. This prevents metadata
|
|
385
|
+
// for a different token or spender from being replayed through the publisher.
|
|
386
|
+
IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({
|
|
387
|
+
details: IAllowanceTransfer.PermitDetails({
|
|
388
|
+
token: token,
|
|
389
|
+
amount: permit2Allowance.amount,
|
|
390
|
+
expiration: permit2Allowance.expiration,
|
|
391
|
+
nonce: permit2Allowance.nonce
|
|
392
|
+
}),
|
|
393
|
+
spender: address(this),
|
|
394
|
+
sigDeadline: permit2Allowance.sigDeadline
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Submit the Permit2 approval for the ERC-2771-aware caller before the pull. If it has already been used or
|
|
398
|
+
// otherwise fails, continue so an existing ERC-20 approval or Permit2 allowance can still satisfy the pull.
|
|
399
|
+
try PERMIT2.permit({
|
|
400
|
+
owner: _msgSender(), permitSingle: permitSingle, signature: permit2Allowance.signature
|
|
401
|
+
}) {}
|
|
402
|
+
catch (bytes memory reason) {
|
|
403
|
+
// Surface failed permit setup without masking the later transfer failure or success path.
|
|
404
|
+
emit Permit2AllowanceFailed({token: token, owner: _msgSender(), reason: reason});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Measure the actual ERC-20 balance delta so fee-on-transfer or otherwise nonstandard tokens cannot overstate
|
|
409
|
+
// how much the publisher received.
|
|
410
|
+
uint256 balanceBefore = _balanceOf(token);
|
|
411
|
+
|
|
412
|
+
// Prevent callback-capable tokens from nesting another incoming ERC-20 transfer inside this balance-delta
|
|
413
|
+
// measurement.
|
|
414
|
+
if (_acceptingToken) revert CTPublisher_ReentrantTokenTransfer(token);
|
|
415
|
+
// The flag is transient, so it only protects the current transaction's balance-delta measurement.
|
|
416
|
+
_acceptingToken = true;
|
|
417
|
+
|
|
418
|
+
// Pull from the ERC-2771-aware caller using either direct ERC-20 approval or Permit2 fallback.
|
|
419
|
+
_transferFrom({from: _msgSender(), to: payable(address(this)), token: token, amount: amount});
|
|
420
|
+
|
|
421
|
+
// Return what arrived rather than what was requested; downstream pricing and fee math use the real receipt.
|
|
422
|
+
acceptedAmount = _balanceOf(token) - balanceBefore;
|
|
423
|
+
// Clear the transient guard once the balance-delta measurement has finished.
|
|
424
|
+
_acceptingToken = false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/// @notice Shared implementation for native and ERC-20 publish payments.
|
|
428
|
+
/// @param hook The hook to mint from.
|
|
429
|
+
/// @param posts An array of posts that should be published as NFTs to the specified project.
|
|
430
|
+
/// @param token The terminal token to pay with.
|
|
431
|
+
/// @param amount The total token amount supplied for the post payment and Croptop fee.
|
|
432
|
+
/// @param nftBeneficiary The beneficiary of the NFT mints.
|
|
433
|
+
/// @param feeBeneficiary The beneficiary of the fee project's token.
|
|
434
|
+
/// @param additionalPayMetadata Metadata bytes to include in the payment after Croptop prepends NFT mint metadata.
|
|
435
|
+
function _mintFrom(
|
|
436
|
+
IJB721TiersHook hook,
|
|
437
|
+
CTPost[] calldata posts,
|
|
438
|
+
address token,
|
|
439
|
+
uint256 amount,
|
|
440
|
+
address nftBeneficiary,
|
|
441
|
+
address feeBeneficiary,
|
|
442
|
+
bytes calldata additionalPayMetadata
|
|
443
|
+
)
|
|
444
|
+
internal
|
|
200
445
|
{
|
|
201
446
|
// Reject empty posts to prevent fee-free metadata shadowing.
|
|
202
447
|
if (posts.length == 0) revert CTPublisher_NoPosts(_msgSender());
|
|
@@ -204,8 +449,8 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
204
449
|
// Reject address(0) as fee beneficiary to prevent burning fee project tokens.
|
|
205
450
|
if (feeBeneficiary == address(0)) revert CTPublisher_InvalidFeeBeneficiary();
|
|
206
451
|
|
|
207
|
-
// Keep a reference to the amount being paid, which is
|
|
208
|
-
uint256 payValue =
|
|
452
|
+
// Keep a reference to the amount being paid, which is `amount` minus the fee.
|
|
453
|
+
uint256 payValue = amount;
|
|
209
454
|
|
|
210
455
|
// Keep a reference to the mint metadata.
|
|
211
456
|
bytes memory mintMetadata;
|
|
@@ -213,11 +458,29 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
213
458
|
// Keep a reference to the project's ID.
|
|
214
459
|
uint256 projectId = hook.projectId();
|
|
215
460
|
|
|
461
|
+
(uint256 pricingCurrency, uint256 pricingDecimals) = hook.pricingContext();
|
|
462
|
+
|
|
463
|
+
// Get a reference to the project's current payment terminal.
|
|
464
|
+
IJBTerminal projectTerminal = DIRECTORY.primaryTerminalOf({projectId: projectId, token: token});
|
|
465
|
+
|
|
466
|
+
JBAccountingContext memory paymentContext =
|
|
467
|
+
_paymentContextOf({terminal: projectTerminal, projectId: projectId, token: token});
|
|
468
|
+
|
|
469
|
+
uint256 mintCount = posts.length;
|
|
216
470
|
{
|
|
217
471
|
// Setup the posts.
|
|
218
472
|
(JB721TierConfig[] memory tiersToAdd, uint256[] memory tierIdsToMint, uint256 totalPrice) =
|
|
219
473
|
_setupPosts({hook: hook, posts: posts});
|
|
220
474
|
|
|
475
|
+
totalPrice = _paymentAmountFromPricing({
|
|
476
|
+
hook: hook,
|
|
477
|
+
projectId: projectId,
|
|
478
|
+
paymentContext: paymentContext,
|
|
479
|
+
pricingCurrency: pricingCurrency,
|
|
480
|
+
pricingDecimals: pricingDecimals,
|
|
481
|
+
pricingAmount: totalPrice
|
|
482
|
+
});
|
|
483
|
+
|
|
221
484
|
if (projectId != FEE_PROJECT_ID) {
|
|
222
485
|
// Keep a reference to the fee that will be paid.
|
|
223
486
|
// Note: integer division truncates, so the fee loses up to (FEE_DIVISOR - 1) wei of dust.
|
|
@@ -225,9 +488,9 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
225
488
|
// This rounding is in the payer's favor and the loss is negligible for practical amounts.
|
|
226
489
|
uint256 fee = totalPrice / FEE_DIVISOR;
|
|
227
490
|
|
|
228
|
-
// Make sure enough
|
|
491
|
+
// Make sure enough was sent to cover the fee.
|
|
229
492
|
if (payValue < fee) {
|
|
230
|
-
|
|
493
|
+
_revertInsufficientPayment({token: token, expected: totalPrice + fee, sent: amount});
|
|
231
494
|
}
|
|
232
495
|
|
|
233
496
|
payValue -= fee;
|
|
@@ -235,7 +498,7 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
235
498
|
|
|
236
499
|
// Make sure the amount sent to this function is at least the specified price of the tier plus the fee.
|
|
237
500
|
if (totalPrice > payValue) {
|
|
238
|
-
|
|
501
|
+
_revertInsufficientPayment({token: token, expected: totalPrice, sent: amount});
|
|
239
502
|
}
|
|
240
503
|
|
|
241
504
|
// Add the new tiers.
|
|
@@ -270,152 +533,291 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
270
533
|
emit Mint({
|
|
271
534
|
projectId: projectId,
|
|
272
535
|
hook: hook,
|
|
536
|
+
token: token,
|
|
273
537
|
nftBeneficiary: nftBeneficiary,
|
|
274
538
|
feeBeneficiary: feeBeneficiary,
|
|
275
539
|
posts: posts,
|
|
276
540
|
postValue: payValue,
|
|
277
|
-
|
|
541
|
+
amount: amount,
|
|
278
542
|
caller: _msgSender()
|
|
279
543
|
});
|
|
280
544
|
|
|
281
545
|
{
|
|
282
|
-
|
|
283
|
-
IJBTerminal projectTerminal =
|
|
284
|
-
DIRECTORY.primaryTerminalOf({projectId: projectId, token: JBConstants.NATIVE_TOKEN});
|
|
546
|
+
uint256 balanceBefore = hook.STORE().balanceOf({hook: address(hook), owner: nftBeneficiary});
|
|
285
547
|
|
|
286
548
|
// Make the payment.
|
|
287
|
-
|
|
549
|
+
_prepareAllowanceFor({token: token, spender: address(projectTerminal), amount: payValue});
|
|
550
|
+
projectTerminal.pay{value: token == JBConstants.NATIVE_TOKEN ? payValue : 0}({
|
|
288
551
|
projectId: projectId,
|
|
289
|
-
token:
|
|
552
|
+
token: token,
|
|
290
553
|
amount: payValue,
|
|
291
554
|
beneficiary: nftBeneficiary,
|
|
292
555
|
minReturnedTokens: 0,
|
|
293
556
|
memo: "Minted from Croptop",
|
|
294
557
|
metadata: mintMetadata
|
|
295
558
|
});
|
|
559
|
+
_requireTemporaryAllowanceConsumed({token: token, spender: address(projectTerminal)});
|
|
560
|
+
|
|
561
|
+
uint256 expectedBalance = balanceBefore + mintCount;
|
|
562
|
+
uint256 balanceAfter = hook.STORE().balanceOf({hook: address(hook), owner: nftBeneficiary});
|
|
563
|
+
if (balanceAfter < expectedBalance) {
|
|
564
|
+
revert CTPublisher_MintNotDelivered({
|
|
565
|
+
hook: address(hook),
|
|
566
|
+
beneficiary: nftBeneficiary,
|
|
567
|
+
expectedBalance: expectedBalance,
|
|
568
|
+
actualBalance: balanceAfter
|
|
569
|
+
});
|
|
570
|
+
}
|
|
296
571
|
}
|
|
297
572
|
|
|
298
573
|
// Reuse payValue to hold the pre-computed fee amount, avoiding reliance on address(this).balance
|
|
299
|
-
// after the external call above (which could be manipulated by reentrancy or force-sent
|
|
300
|
-
payValue =
|
|
574
|
+
// after the external call above (which could be manipulated by reentrancy or force-sent tokens).
|
|
575
|
+
payValue = amount - payValue;
|
|
301
576
|
|
|
302
577
|
// Pay the fee if there is one.
|
|
303
578
|
if (payValue != 0) {
|
|
304
|
-
// Get a reference to the fee project's current
|
|
305
|
-
IJBTerminal feeTerminal =
|
|
306
|
-
|
|
579
|
+
// Get a reference to the fee project's current payment terminal.
|
|
580
|
+
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_PROJECT_ID, token: token});
|
|
581
|
+
if (address(feeTerminal) == address(0)) {
|
|
582
|
+
_refundFee({token: token, amount: payValue});
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
307
585
|
|
|
308
586
|
// Make the fee payment. If the fee sink is unavailable, refund the fee to the caller
|
|
309
587
|
// rather than trapping or silently redirecting protocol funds.
|
|
310
|
-
|
|
588
|
+
_prepareAllowanceFor({token: token, spender: address(feeTerminal), amount: payValue});
|
|
589
|
+
try feeTerminal.pay{value: token == JBConstants.NATIVE_TOKEN ? payValue : 0}({
|
|
311
590
|
projectId: FEE_PROJECT_ID,
|
|
312
591
|
amount: payValue,
|
|
313
|
-
token:
|
|
592
|
+
token: token,
|
|
314
593
|
beneficiary: feeBeneficiary,
|
|
315
594
|
minReturnedTokens: 0,
|
|
316
595
|
memo: "",
|
|
317
596
|
metadata: ""
|
|
318
|
-
}) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
597
|
+
}) {
|
|
598
|
+
_requireTemporaryAllowanceConsumed({token: token, spender: address(feeTerminal)});
|
|
599
|
+
} catch {
|
|
600
|
+
_clearAllowanceFor({token: token, spender: address(feeTerminal)});
|
|
601
|
+
_refundFee({token: token, amount: payValue});
|
|
322
602
|
}
|
|
323
603
|
}
|
|
324
604
|
}
|
|
325
605
|
|
|
326
606
|
//*********************************************************************//
|
|
327
|
-
//
|
|
607
|
+
// ------------------------ internal helpers ------------------------- //
|
|
328
608
|
//*********************************************************************//
|
|
329
609
|
|
|
330
|
-
/// @notice
|
|
331
|
-
/// @
|
|
332
|
-
///
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
/// @param encodedIpfsUris The URIs to get tiers of.
|
|
336
|
-
/// @return tiers The tiers that correspond to the provided encoded IPFS URIs. If there's no tier yet, an empty tier
|
|
337
|
-
/// is returned.
|
|
338
|
-
function tiersFor(
|
|
339
|
-
address hook,
|
|
340
|
-
bytes32[] memory encodedIpfsUris
|
|
341
|
-
)
|
|
342
|
-
external
|
|
343
|
-
view
|
|
344
|
-
override
|
|
345
|
-
returns (JB721Tier[] memory tiers)
|
|
346
|
-
{
|
|
347
|
-
uint256 numberOfEncodedIpfsUris = encodedIpfsUris.length;
|
|
610
|
+
/// @notice Return this contract's balance of a token.
|
|
611
|
+
/// @param token The token whose balance should be read.
|
|
612
|
+
/// @return balance The current balance.
|
|
613
|
+
function _balanceOf(address token) internal view returns (uint256 balance) {
|
|
614
|
+
if (token == JBConstants.NATIVE_TOKEN) return address(this).balance;
|
|
348
615
|
|
|
349
|
-
|
|
350
|
-
|
|
616
|
+
return IERC20(token).balanceOf(address(this));
|
|
617
|
+
}
|
|
351
618
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
619
|
+
/// @notice Clear a temporary ERC-20 allowance granted by this publisher.
|
|
620
|
+
/// @param token The token whose allowance should be cleared.
|
|
621
|
+
/// @param spender The contract whose allowance should be cleared.
|
|
622
|
+
function _clearAllowanceFor(address token, address spender) internal {
|
|
623
|
+
if (token == JBConstants.NATIVE_TOKEN) return;
|
|
356
624
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
tiers[i] = IJB721TiersHook(hook).STORE().tierOf({hook: hook, id: tierId, includeResolvedUri: false});
|
|
360
|
-
}
|
|
625
|
+
IERC20(token).forceApprove({spender: spender, value: 0});
|
|
626
|
+
}
|
|
361
627
|
|
|
628
|
+
/// @notice Check if an address is included in an allow list.
|
|
629
|
+
/// @dev Uses an O(n) linear scan over the `addresses` array. This is acceptable for typical allow list sizes
|
|
630
|
+
/// (fewer than ~100 addresses), where gas cost is negligible. For very large allow lists, a Merkle proof
|
|
631
|
+
/// pattern would scale better, but the added complexity is not warranted for the expected use case.
|
|
632
|
+
/// @param addrs The candidate address.
|
|
633
|
+
/// @param addresses An array of allowed addresses.
|
|
634
|
+
function _isAllowed(address addrs, address[] memory addresses) internal pure returns (bool) {
|
|
635
|
+
// Keep a reference to the number of addresses to check against.
|
|
636
|
+
uint256 numberOfAddresses = addresses.length;
|
|
637
|
+
|
|
638
|
+
// Check whether the address is included.
|
|
639
|
+
for (uint256 i; i < numberOfAddresses;) {
|
|
640
|
+
if (addrs == addresses[i]) return true;
|
|
362
641
|
unchecked {
|
|
363
642
|
++i;
|
|
364
643
|
}
|
|
365
644
|
}
|
|
645
|
+
|
|
646
|
+
return false;
|
|
366
647
|
}
|
|
367
648
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
649
|
+
/// @notice Check whether two currency IDs both represent native ETH accounting.
|
|
650
|
+
/// @param currency The first currency ID.
|
|
651
|
+
/// @param otherCurrency The second currency ID.
|
|
652
|
+
function _isNativeEthCurrencyPair(uint256 currency, uint256 otherCurrency) internal pure returns (bool) {
|
|
653
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
654
|
+
uint256 nativeTokenCurrency = uint32(uint160(JBConstants.NATIVE_TOKEN));
|
|
371
655
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
/// @
|
|
377
|
-
///
|
|
378
|
-
/// @
|
|
379
|
-
///
|
|
380
|
-
/// @
|
|
381
|
-
/// @
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
656
|
+
return (currency == JBCurrencyIds.ETH || currency == nativeTokenCurrency)
|
|
657
|
+
&& (otherCurrency == JBCurrencyIds.ETH || otherCurrency == nativeTokenCurrency);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/// @notice Convert a hook pricing amount into the selected terminal token's accounting units.
|
|
661
|
+
/// @param hook The hook whose pricing oracle should be used when currencies differ.
|
|
662
|
+
/// @param projectId The ID of the project being paid.
|
|
663
|
+
/// @param paymentContext The terminal accounting context for the selected payment token.
|
|
664
|
+
/// @param pricingCurrency The hook's pricing currency.
|
|
665
|
+
/// @param pricingDecimals The hook's pricing decimals.
|
|
666
|
+
/// @param pricingAmount The tier-price amount to convert.
|
|
667
|
+
/// @return paymentAmount The required amount denominated in `paymentContext` units.
|
|
668
|
+
function _paymentAmountFromPricing(
|
|
669
|
+
IJB721TiersHook hook,
|
|
670
|
+
uint256 projectId,
|
|
671
|
+
JBAccountingContext memory paymentContext,
|
|
672
|
+
uint256 pricingCurrency,
|
|
673
|
+
uint256 pricingDecimals,
|
|
674
|
+
uint256 pricingAmount
|
|
385
675
|
)
|
|
386
|
-
|
|
676
|
+
internal
|
|
387
677
|
view
|
|
388
|
-
|
|
389
|
-
returns (
|
|
390
|
-
uint256 minimumPrice,
|
|
391
|
-
uint256 minimumTotalSupply,
|
|
392
|
-
uint256 maximumTotalSupply,
|
|
393
|
-
uint256 maximumSplitPercent,
|
|
394
|
-
address[] memory allowedAddresses
|
|
395
|
-
)
|
|
678
|
+
returns (uint256 paymentAmount)
|
|
396
679
|
{
|
|
397
|
-
|
|
398
|
-
|
|
680
|
+
if (pricingAmount == 0) return 0;
|
|
681
|
+
|
|
682
|
+
if (
|
|
683
|
+
paymentContext.currency == pricingCurrency
|
|
684
|
+
|| _isNativeEthCurrencyPair({currency: paymentContext.currency, otherCurrency: pricingCurrency})
|
|
685
|
+
) {
|
|
686
|
+
return _scaleAmount({
|
|
687
|
+
amount: pricingAmount, fromDecimals: pricingDecimals, toDecimals: paymentContext.decimals
|
|
688
|
+
});
|
|
689
|
+
}
|
|
399
690
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// maximum supply in bits 136-167 (32 bits).
|
|
407
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
408
|
-
maximumTotalSupply = uint256(uint32(packed >> 136));
|
|
409
|
-
// maximum split percent in bits 168-199 (32 bits).
|
|
410
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
411
|
-
maximumSplitPercent = uint256(uint32(packed >> 168));
|
|
691
|
+
IJBPrices prices = hook.PRICES();
|
|
692
|
+
if (address(prices) == address(0)) {
|
|
693
|
+
revert CTPublisher_PriceFeedUnavailable({
|
|
694
|
+
paymentCurrency: paymentContext.currency, pricingCurrency: pricingCurrency
|
|
695
|
+
});
|
|
696
|
+
}
|
|
412
697
|
|
|
413
|
-
|
|
698
|
+
uint256 ratio = prices.pricePerUnitOf({
|
|
699
|
+
projectId: projectId,
|
|
700
|
+
pricingCurrency: paymentContext.currency,
|
|
701
|
+
unitCurrency: pricingCurrency,
|
|
702
|
+
decimals: paymentContext.decimals
|
|
703
|
+
});
|
|
704
|
+
if (ratio == 0) {
|
|
705
|
+
revert CTPublisher_PriceFeedUnavailable({
|
|
706
|
+
paymentCurrency: paymentContext.currency, pricingCurrency: pricingCurrency
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return
|
|
711
|
+
Math.mulDiv({x: pricingAmount, y: ratio, denominator: 10 ** pricingDecimals, rounding: Math.Rounding.Ceil});
|
|
414
712
|
}
|
|
415
713
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
714
|
+
/// @notice Return the terminal accounting context for the selected payment token.
|
|
715
|
+
/// @param terminal The terminal being paid.
|
|
716
|
+
/// @param projectId The ID of the project being paid.
|
|
717
|
+
/// @param token The token being paid.
|
|
718
|
+
/// @return context The terminal's accounting context for `token`.
|
|
719
|
+
function _paymentContextOf(
|
|
720
|
+
IJBTerminal terminal,
|
|
721
|
+
uint256 projectId,
|
|
722
|
+
address token
|
|
723
|
+
)
|
|
724
|
+
internal
|
|
725
|
+
view
|
|
726
|
+
returns (JBAccountingContext memory context)
|
|
727
|
+
{
|
|
728
|
+
context = terminal.accountingContextForTokenOf({projectId: projectId, token: token});
|
|
729
|
+
|
|
730
|
+
if (context.token != token || context.currency == 0) {
|
|
731
|
+
revert CTPublisher_InvalidPaymentTokenContext({
|
|
732
|
+
token: token,
|
|
733
|
+
tokenCurrency: context.currency,
|
|
734
|
+
tokenDecimals: context.decimals,
|
|
735
|
+
pricingCurrency: 0,
|
|
736
|
+
pricingDecimals: 0
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/// @notice Grant a temporary ERC-20 allowance for a downstream terminal payment.
|
|
742
|
+
/// @param token The token being paid.
|
|
743
|
+
/// @param spender The terminal expected to consume the allowance.
|
|
744
|
+
/// @param amount The exact allowance to grant.
|
|
745
|
+
function _prepareAllowanceFor(address token, address spender, uint256 amount) internal {
|
|
746
|
+
if (token == JBConstants.NATIVE_TOKEN) return;
|
|
747
|
+
|
|
748
|
+
IERC20(token).forceApprove({spender: spender, value: amount});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/// @notice Refund a fee payment when the fee terminal is unavailable.
|
|
752
|
+
/// @param token The token to refund.
|
|
753
|
+
/// @param amount The amount to refund.
|
|
754
|
+
function _refundFee(address token, uint256 amount) internal {
|
|
755
|
+
if (token == JBConstants.NATIVE_TOKEN) {
|
|
756
|
+
// Native fees are held as ETH, so refund the ERC-2771-aware caller with a raw value transfer.
|
|
757
|
+
(bool success,) = _msgSender().call{value: amount}("");
|
|
758
|
+
// If the caller cannot receive ETH, revert instead of leaving the fee stranded in this contract.
|
|
759
|
+
if (!success) revert CTPublisher_FeePaymentFailed(amount);
|
|
760
|
+
|
|
761
|
+
// The ERC-20 refund path below is only for tokenized terminal tokens.
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ERC-20 fees are already held by this contract, so return the token balance directly to the caller.
|
|
766
|
+
IERC20(token).safeTransfer({to: _msgSender(), value: amount});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/// @notice Revert if a downstream terminal did not consume its exact-use ERC-20 allowance.
|
|
770
|
+
/// @param token The token whose allowance was temporarily granted.
|
|
771
|
+
/// @param spender The terminal expected to consume the allowance.
|
|
772
|
+
function _requireTemporaryAllowanceConsumed(address token, address spender) internal view {
|
|
773
|
+
// Native payments never grant ERC-20 allowance, so there is nothing to verify.
|
|
774
|
+
if (token == JBConstants.NATIVE_TOKEN) return;
|
|
775
|
+
|
|
776
|
+
// The publisher grants exact-use allowances before external terminal calls; any remainder would leave token
|
|
777
|
+
// spend authority live after the payment is complete.
|
|
778
|
+
uint256 allowance = IERC20(token).allowance({owner: address(this), spender: spender});
|
|
779
|
+
if (allowance != 0) {
|
|
780
|
+
revert CTPublisher_TemporaryAllowanceNotConsumed({token: token, spender: spender, allowance: allowance});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/// @notice Revert with the native-compatible error for ETH payments, or the token-generic error otherwise.
|
|
785
|
+
/// @param token The token being paid.
|
|
786
|
+
/// @param expected The minimum required amount.
|
|
787
|
+
/// @param sent The supplied amount.
|
|
788
|
+
function _revertInsufficientPayment(address token, uint256 expected, uint256 sent) internal pure {
|
|
789
|
+
if (token == JBConstants.NATIVE_TOKEN) {
|
|
790
|
+
revert CTPublisher_InsufficientEthSent({expected: expected, sent: sent});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
revert CTPublisher_InsufficientPayment({expected: expected, sent: sent});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/// @notice Scale an amount between decimal domains, rounding up when precision is reduced.
|
|
797
|
+
/// @param amount The amount to scale.
|
|
798
|
+
/// @param fromDecimals The decimals currently used by `amount`.
|
|
799
|
+
/// @param toDecimals The decimals to scale into.
|
|
800
|
+
/// @return scaledAmount The scaled amount.
|
|
801
|
+
function _scaleAmount(
|
|
802
|
+
uint256 amount,
|
|
803
|
+
uint256 fromDecimals,
|
|
804
|
+
uint256 toDecimals
|
|
805
|
+
)
|
|
806
|
+
internal
|
|
807
|
+
pure
|
|
808
|
+
returns (uint256 scaledAmount)
|
|
809
|
+
{
|
|
810
|
+
// Equal decimal domains need no conversion, preserving the exact caller-supplied amount.
|
|
811
|
+
if (fromDecimals == toDecimals) return amount;
|
|
812
|
+
|
|
813
|
+
// Scaling into more decimals is exact: each source unit maps to a whole number of destination units.
|
|
814
|
+
if (fromDecimals < toDecimals) return amount * (10 ** (toDecimals - fromDecimals));
|
|
815
|
+
|
|
816
|
+
// Scaling into fewer decimals can leave a fractional destination unit; round up so the payer cannot underpay
|
|
817
|
+
// a tier by truncating precision.
|
|
818
|
+
uint256 denominator = 10 ** (fromDecimals - toDecimals);
|
|
819
|
+
return Math.mulDiv({x: amount, y: 1, denominator: denominator, rounding: Math.Rounding.Ceil});
|
|
820
|
+
}
|
|
419
821
|
|
|
420
822
|
/// @notice Setup the posts.
|
|
421
823
|
/// @dev `adjustTiers` expects newly added tiers to be sorted by ascending category, while the pay metadata must
|
|
@@ -641,25 +1043,23 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
641
1043
|
}
|
|
642
1044
|
}
|
|
643
1045
|
|
|
644
|
-
/// @notice
|
|
645
|
-
/// @
|
|
646
|
-
///
|
|
647
|
-
///
|
|
648
|
-
/// @param
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
// Check whether the address is included.
|
|
655
|
-
for (uint256 i; i < numberOfAddresses;) {
|
|
656
|
-
if (addrs == addresses[i]) return true;
|
|
657
|
-
unchecked {
|
|
658
|
-
++i;
|
|
659
|
-
}
|
|
1046
|
+
/// @notice Pull tokens from `from`, preferring ERC-20 approvals and falling back to Permit2.
|
|
1047
|
+
/// @param from The address the transfer should originate from.
|
|
1048
|
+
/// @param to The address receiving tokens.
|
|
1049
|
+
/// @param token The token to transfer.
|
|
1050
|
+
/// @param amount The number of tokens to transfer.
|
|
1051
|
+
function _transferFrom(address from, address payable to, address token, uint256 amount) internal {
|
|
1052
|
+
// Prefer ordinary ERC-20 approval when present so existing approval flows do not need Permit2 metadata.
|
|
1053
|
+
if (IERC20(token).allowance({owner: from, spender: address(this)}) >= amount) {
|
|
1054
|
+
return IERC20(token).safeTransferFrom({from: from, to: to, value: amount});
|
|
660
1055
|
}
|
|
661
1056
|
|
|
662
|
-
|
|
1057
|
+
// Permit2 stores transfer amounts as uint160, so reject values that would truncate before calling it.
|
|
1058
|
+
if (amount > type(uint160).max) revert CTPublisher_OverflowAlert({value: amount, limit: type(uint160).max});
|
|
1059
|
+
|
|
1060
|
+
// Direct approval was insufficient; fall back to Permit2, which enforces the submitted allowance/signature.
|
|
1061
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1062
|
+
PERMIT2.transferFrom({from: from, to: to, amount: uint160(amount), token: token});
|
|
663
1063
|
}
|
|
664
1064
|
|
|
665
1065
|
//*********************************************************************//
|
|
@@ -4,6 +4,7 @@ pragma solidity ^0.8.0;
|
|
|
4
4
|
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
5
5
|
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
6
6
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
|
+
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
7
8
|
import {CTAllowedPost} from "../structs/CTAllowedPost.sol";
|
|
8
9
|
import {CTPost} from "../structs/CTPost.sol";
|
|
9
10
|
|
|
@@ -18,23 +19,31 @@ interface ICTPublisher {
|
|
|
18
19
|
/// @notice Emitted when NFT posts are minted.
|
|
19
20
|
/// @param projectId The ID of the project the posts belong to.
|
|
20
21
|
/// @param hook The tiered ERC-721 hook the posts were minted from.
|
|
22
|
+
/// @param token The terminal token used to pay.
|
|
21
23
|
/// @param nftBeneficiary The address that received the minted NFTs.
|
|
22
24
|
/// @param feeBeneficiary The address that received fee project tokens.
|
|
23
25
|
/// @param posts The posts that were minted.
|
|
24
|
-
/// @param postValue The
|
|
25
|
-
/// @param
|
|
26
|
+
/// @param postValue The amount paid to the post's project after the Croptop fee.
|
|
27
|
+
/// @param amount The total token amount supplied for the post payment and Croptop fee.
|
|
26
28
|
/// @param caller The address that minted the posts.
|
|
27
29
|
event Mint(
|
|
28
30
|
uint256 indexed projectId,
|
|
29
31
|
IJB721TiersHook indexed hook,
|
|
32
|
+
address token,
|
|
30
33
|
address indexed nftBeneficiary,
|
|
31
34
|
address feeBeneficiary,
|
|
32
35
|
CTPost[] posts,
|
|
33
36
|
uint256 postValue,
|
|
34
|
-
uint256
|
|
37
|
+
uint256 amount,
|
|
35
38
|
address caller
|
|
36
39
|
);
|
|
37
40
|
|
|
41
|
+
/// @notice Emitted when Permit2 approval by signature fails before the token pull is attempted.
|
|
42
|
+
/// @param token The ERC-20 token being paid.
|
|
43
|
+
/// @param owner The payment owner whose signature was submitted.
|
|
44
|
+
/// @param reason The low-level revert reason returned by Permit2.
|
|
45
|
+
event Permit2AllowanceFailed(address indexed token, address indexed owner, bytes reason);
|
|
46
|
+
|
|
38
47
|
/// @notice The post allowance for a particular category on a particular hook.
|
|
39
48
|
/// @param hook The hook contract for which this allowance applies.
|
|
40
49
|
/// @param category The category for which this allowance applies.
|
|
@@ -69,6 +78,10 @@ interface ICTPublisher {
|
|
|
69
78
|
/// @return The fee project ID.
|
|
70
79
|
function FEE_PROJECT_ID() external view returns (uint256);
|
|
71
80
|
|
|
81
|
+
/// @notice The Permit2 utility used to pull ERC-20 payments from posters.
|
|
82
|
+
/// @return The Permit2 contract.
|
|
83
|
+
function PERMIT2() external view returns (IPermit2);
|
|
84
|
+
|
|
72
85
|
/// @notice The tier ID that an IPFS metadata URI has been saved to for a given hook.
|
|
73
86
|
/// @param hook The hook for which the tier ID applies.
|
|
74
87
|
/// @param encodedIpfsUri The encoded IPFS URI to look up.
|
|
@@ -90,12 +103,17 @@ interface ICTPublisher {
|
|
|
90
103
|
/// @notice Publish NFT posts and mint a first copy of each. A fee is taken for the fee project.
|
|
91
104
|
/// @param hook The hook to mint from.
|
|
92
105
|
/// @param posts An array of posts to publish as NFTs.
|
|
106
|
+
/// @param token The terminal token to pay with.
|
|
107
|
+
/// @param amount The total token amount supplied for the post payment and Croptop fee.
|
|
93
108
|
/// @param nftBeneficiary The beneficiary of the NFT mints.
|
|
94
109
|
/// @param feeBeneficiary The beneficiary of the fee project's tokens.
|
|
95
|
-
/// @param additionalPayMetadata Extra metadata bytes to include in the payment.
|
|
110
|
+
/// @param additionalPayMetadata Extra metadata bytes to include in the payment. Include a Permit2 entry targeted
|
|
111
|
+
/// to this publisher to pay ERC-20s without a direct publisher approval.
|
|
96
112
|
function mintFrom(
|
|
97
113
|
IJB721TiersHook hook,
|
|
98
114
|
CTPost[] calldata posts,
|
|
115
|
+
address token,
|
|
116
|
+
uint256 amount,
|
|
99
117
|
address nftBeneficiary,
|
|
100
118
|
address feeBeneficiary,
|
|
101
119
|
bytes calldata additionalPayMetadata
|