@bananapus/core-v6 0.0.57 → 0.0.59
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 +1 -1
- package/src/JBMultiTerminal.sol +130 -97
- package/src/JBTerminalStore.sol +111 -0
- package/src/interfaces/IJBCashOutTerminal.sol +4 -1
- package/src/interfaces/IJBMultiTerminal.sol +13 -0
- package/src/interfaces/IJBPayoutTerminal.sol +8 -2
- package/src/interfaces/IJBTerminalStore.sol +55 -0
- package/src/libraries/JBConstants.sol +18 -8
- package/src/libraries/JBHeldFees.sol +170 -0
- package/src/structs/JBFee.sol +13 -2
- package/test/mock/MockMaliciousBeneficiary.sol +4 -2
package/package.json
CHANGED
package/src/JBMultiTerminal.sol
CHANGED
|
@@ -30,6 +30,7 @@ import {IJBTerminalStore} from "./interfaces/IJBTerminalStore.sol";
|
|
|
30
30
|
import {IJBTokens} from "./interfaces/IJBTokens.sol";
|
|
31
31
|
import {JBConstants} from "./libraries/JBConstants.sol";
|
|
32
32
|
import {JBFees} from "./libraries/JBFees.sol";
|
|
33
|
+
import {JBHeldFees} from "./libraries/JBHeldFees.sol";
|
|
33
34
|
import {JBMetadataResolver} from "./libraries/JBMetadataResolver.sol";
|
|
34
35
|
import {JBPayoutSplitGroupLib} from "./libraries/JBPayoutSplitGroupLib.sol";
|
|
35
36
|
import {JBRulesetMetadataResolver} from "./libraries/JBRulesetMetadataResolver.sol";
|
|
@@ -64,7 +65,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
64
65
|
//*********************************************************************//
|
|
65
66
|
|
|
66
67
|
error JBMultiTerminal_FeeTerminalNotFound(address token);
|
|
67
|
-
error JBMultiTerminal_MintNotAllowed();
|
|
68
|
+
error JBMultiTerminal_MintNotAllowed(uint256 projectId, address terminal);
|
|
68
69
|
error JBMultiTerminal_NoMsgValueAllowed(uint256 value);
|
|
69
70
|
error JBMultiTerminal_OverflowAlert(uint256 value, uint256 limit);
|
|
70
71
|
error JBMultiTerminal_PermitAllowanceNotEnough(uint256 amount, uint256 allowance);
|
|
@@ -81,9 +82,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
81
82
|
// ------------------------ internal constants ----------------------- //
|
|
82
83
|
//*********************************************************************//
|
|
83
84
|
|
|
84
|
-
/// @notice Project ID #1 receives fees. It should be the first project launched during the deployment process.
|
|
85
|
-
uint256 internal constant _FEE_BENEFICIARY_PROJECT_ID = 1;
|
|
86
|
-
|
|
87
85
|
/// @notice The number of seconds fees can be held for.
|
|
88
86
|
uint256 internal constant _FEE_HOLDING_SECONDS = 2_419_200; // 28 days
|
|
89
87
|
|
|
@@ -143,7 +141,26 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
143
141
|
mapping(uint256 projectId => mapping(address token => uint256)) internal _nextHeldFeeIndexOf;
|
|
144
142
|
|
|
145
143
|
//*********************************************************************//
|
|
146
|
-
//
|
|
144
|
+
// --------------- public transient stored properties --------------- //
|
|
145
|
+
//*********************************************************************//
|
|
146
|
+
|
|
147
|
+
/// @notice Caller-originated referrer reference for the duration of the current external fee-paying call.
|
|
148
|
+
/// @dev Encoded as `(referralChainId << 48) | referralProjectId`: bits [79:48] are the referrer's EIP-155 chain
|
|
149
|
+
/// ID (uint32), bits [47:0] are the referrer's project ID on that chain (uint48). The upper bits of the
|
|
150
|
+
/// uint256 are reserved for future encoding extensions.
|
|
151
|
+
/// @dev The entry points `cashOutTokensOf`, `sendPayoutsOf`, and `useAllowanceOf` auto-resolve a bare project ID
|
|
152
|
+
/// (non-zero project, zero chain bits) to the current execution chain via `block.chainid` before writing this
|
|
153
|
+
/// slot. So storage (and indexers reading `feeVolumeByReferralOf`) always observe a fully-resolved
|
|
154
|
+
/// `(chainId, projectId)` pair — callers can write `referralProjectId: someProjectId` without manually packing
|
|
155
|
+
/// their own chain ID.
|
|
156
|
+
/// @dev Set by the three entry points via the `_setReferralProjectId` save-restore wrapper. Read inside `_pay`
|
|
157
|
+
/// to credit `feeVolumeByReferralOf` when the fee project's pay call is recorded locally.
|
|
158
|
+
/// @dev Public so pay/cashout/split hooks can introspect which referral originated the in-flight call (e.g. to
|
|
159
|
+
/// apply referral-specific logic). Reads `0` outside any fee-paying call.
|
|
160
|
+
uint256 public transient override currentReferralProjectId;
|
|
161
|
+
|
|
162
|
+
//*********************************************************************//
|
|
163
|
+
// -------------- internal transient stored properties -------------- //
|
|
147
164
|
//*********************************************************************//
|
|
148
165
|
|
|
149
166
|
/// @notice Whether this terminal is currently measuring an incoming ERC-20 balance delta.
|
|
@@ -277,6 +294,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
277
294
|
/// data hook and cash out hook if applicable.
|
|
278
295
|
/// @param metadata Bytes to send along to the emitted event, as well as the data hook and cash out hook if
|
|
279
296
|
/// applicable.
|
|
297
|
+
/// @param referralProjectId Optional referrer reference to credit with the protocol fee volume taken by this
|
|
298
|
+
/// call, encoded as `(referralChainId << 48) | referralProjectId` (chain ID in the upper 32 bits, project ID
|
|
299
|
+
/// in the lower 48). A bare project ID (chain bits zero) is auto-resolved to the current chain via `block.chainid`.
|
|
300
|
+
/// Pass `0` for no referral credit.
|
|
280
301
|
/// @return reclaimAmount The amount of **terminal tokens** sent to `beneficiary` in exchange for the burned project
|
|
281
302
|
/// tokens, as a fixed point number with the same number of decimals as the terminal token's accounting context.
|
|
282
303
|
function cashOutTokensOf(
|
|
@@ -286,7 +307,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
286
307
|
address tokenToReclaim,
|
|
287
308
|
uint256 minTokensReclaimed,
|
|
288
309
|
address payable beneficiary,
|
|
289
|
-
bytes calldata metadata
|
|
310
|
+
bytes calldata metadata,
|
|
311
|
+
uint256 referralProjectId
|
|
290
312
|
)
|
|
291
313
|
external
|
|
292
314
|
override
|
|
@@ -295,6 +317,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
295
317
|
// Enforce permissions.
|
|
296
318
|
_requirePermissionFrom({account: holder, projectId: projectId, permissionId: JBPermissionIds.CASH_OUT_TOKENS});
|
|
297
319
|
|
|
320
|
+
// Save-restore the transient referral slot so nested reentrant fee-paying calls don't pollute each other.
|
|
321
|
+
uint256 priorReferral = _setReferralProjectId(referralProjectId);
|
|
322
|
+
|
|
298
323
|
reclaimAmount = _cashOutTokensOf({
|
|
299
324
|
holder: holder,
|
|
300
325
|
projectId: projectId,
|
|
@@ -304,6 +329,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
304
329
|
metadata: metadata
|
|
305
330
|
});
|
|
306
331
|
|
|
332
|
+
_setReferralProjectId(priorReferral);
|
|
333
|
+
|
|
307
334
|
// The amount being reclaimed must be at least as much as was expected.
|
|
308
335
|
_checkMin({value: reclaimAmount, min: minTokensReclaimed});
|
|
309
336
|
}
|
|
@@ -401,7 +428,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
401
428
|
// consumption, payout-limit drawdown, fee-free-surplus accounting); routing it
|
|
402
429
|
// through `sendPayoutsOf` is never the right surface.
|
|
403
430
|
// The try-catch in the split group lib catches this revert and restores the balance.
|
|
404
|
-
if (split.projectId == projectId)
|
|
431
|
+
if (split.projectId == projectId) {
|
|
432
|
+
revert JBMultiTerminal_MintNotAllowed({projectId: projectId, terminal: address(terminal)});
|
|
433
|
+
}
|
|
405
434
|
|
|
406
435
|
// Send the `projectId` in the metadata as a referral.
|
|
407
436
|
bytes memory metadata = bytes(abi.encodePacked(projectId));
|
|
@@ -496,7 +525,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
496
525
|
|
|
497
526
|
_efficientPay({
|
|
498
527
|
terminal: feeTerminal,
|
|
499
|
-
projectId:
|
|
528
|
+
projectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID,
|
|
500
529
|
token: token,
|
|
501
530
|
amount: amount,
|
|
502
531
|
beneficiary: beneficiary,
|
|
@@ -567,7 +596,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
567
596
|
// Migration to a non-feeless terminal incurs the standard 2.5% fee, same as any other fund egress.
|
|
568
597
|
// This also settles any fee-free surplus liability that would otherwise be lost on the new terminal.
|
|
569
598
|
uint256 feeAmount;
|
|
570
|
-
if (
|
|
599
|
+
if (
|
|
600
|
+
!_isFeeless({addr: address(to), projectId: projectId})
|
|
601
|
+
&& projectId != JBConstants.FEE_BENEFICIARY_PROJECT_ID
|
|
602
|
+
) {
|
|
571
603
|
// Fee processing failures never block migration. If the fee route is broken, `_processFee` credits
|
|
572
604
|
// the fee amount back to this source terminal and emits `FeeReverted`; the post-fee amount still
|
|
573
605
|
// migrates so project funds are not trapped behind project #1 routing issues.
|
|
@@ -664,7 +696,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
664
696
|
/// @param count The number of fees to process.
|
|
665
697
|
function processHeldFeesOf(uint256 projectId, address token, uint256 count) external override {
|
|
666
698
|
// Keep a reference to the terminal that'll receive the fees.
|
|
667
|
-
IJBTerminal feeTerminal = _primaryTerminalOf({projectId:
|
|
699
|
+
IJBTerminal feeTerminal = _primaryTerminalOf({projectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID, token: token});
|
|
668
700
|
|
|
669
701
|
// Process each fee. Re-read the index and array length from storage each iteration to account for reentrant
|
|
670
702
|
// calls that may have already advanced the index or cleaned up the array.
|
|
@@ -694,6 +726,13 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
694
726
|
delete _heldFeesOf[projectId][token][currentIndex];
|
|
695
727
|
_nextHeldFeeIndexOf[projectId][token] = currentIndex + 1;
|
|
696
728
|
|
|
729
|
+
// Restore the originating fee-paying call's referral project for the duration of this fee's processing
|
|
730
|
+
// so the credit in `_processFee` attributes to the right (chain, project) pair. No save needed:
|
|
731
|
+
// `processHeldFeesOf` is a top-level keeper call in practice — the incoming transient is 0. Defensive
|
|
732
|
+
// cleanup happens once after the loop instead of per-iteration. Reconstructs the packed encoding from
|
|
733
|
+
// the two struct halves: `(chainId << 48) | projectId`.
|
|
734
|
+
currentReferralProjectId = (uint256(heldFee.referralChainId) << 48) | uint256(heldFee.referralProjectId);
|
|
735
|
+
|
|
697
736
|
// Process the standard fee on the original gross amount recorded when the held fee was created.
|
|
698
737
|
_processFee({
|
|
699
738
|
projectId: projectId,
|
|
@@ -703,11 +742,17 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
703
742
|
feeTerminal: feeTerminal,
|
|
704
743
|
wasHeld: true
|
|
705
744
|
});
|
|
745
|
+
|
|
706
746
|
unchecked {
|
|
707
747
|
++i;
|
|
708
748
|
}
|
|
709
749
|
}
|
|
710
750
|
|
|
751
|
+
// Defensive cleanup: zero the transient so any subsequent in-tx work (e.g. hook reentrancy) doesn't
|
|
752
|
+
// inherit the last processed fee's referral. EIP-1153 would clear it at end-of-tx anyway, but in-tx
|
|
753
|
+
// reentry is the only case where this matters.
|
|
754
|
+
currentReferralProjectId = 0;
|
|
755
|
+
|
|
711
756
|
// If all held fees have been processed, reset the array and index entirely to bound storage growth.
|
|
712
757
|
if (
|
|
713
758
|
_nextHeldFeeIndexOf[projectId][token] >= _heldFeesOf[projectId][token].length
|
|
@@ -734,20 +779,29 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
734
779
|
/// in terms of the token's accounting context currency), as a fixed point number with the same number of decimals
|
|
735
780
|
/// as the token's accounting context. If the amount of tokens paid out would be less than this amount, the send is
|
|
736
781
|
/// reverted.
|
|
782
|
+
/// @param referralProjectId Optional referrer reference to credit with the protocol fee volume taken by this
|
|
783
|
+
/// call, encoded as `(referralChainId << 48) | referralProjectId` (chain ID in the upper 32 bits, project ID
|
|
784
|
+
/// in the lower 48). A bare project ID (chain bits zero) is auto-resolved to the current chain via `block.chainid`.
|
|
785
|
+
/// Pass `0` for no referral credit.
|
|
737
786
|
/// @return amountPaidOut The total amount paid out.
|
|
738
787
|
function sendPayoutsOf(
|
|
739
788
|
uint256 projectId,
|
|
740
789
|
address token,
|
|
741
790
|
uint256 amount,
|
|
742
791
|
uint256 currency,
|
|
743
|
-
uint256 minTokensPaidOut
|
|
792
|
+
uint256 minTokensPaidOut,
|
|
793
|
+
uint256 referralProjectId
|
|
744
794
|
)
|
|
745
795
|
external
|
|
746
796
|
override
|
|
747
797
|
returns (uint256 amountPaidOut)
|
|
748
798
|
{
|
|
799
|
+
uint256 priorReferral = _setReferralProjectId(referralProjectId);
|
|
800
|
+
|
|
749
801
|
amountPaidOut = _sendPayoutsOf({projectId: projectId, token: token, amount: amount, currency: currency});
|
|
750
802
|
|
|
803
|
+
_setReferralProjectId(priorReferral);
|
|
804
|
+
|
|
751
805
|
// The amount being paid out must be at least as much as was expected.
|
|
752
806
|
_checkMin({value: amountPaidOut, min: minTokensPaidOut});
|
|
753
807
|
}
|
|
@@ -770,6 +824,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
770
824
|
/// @param feeBeneficiary The address that receives the **project tokens** minted by the fee project in exchange
|
|
771
825
|
/// for the protocol fee paid in terminal tokens.
|
|
772
826
|
/// @param memo A memo to pass along to the emitted event.
|
|
827
|
+
/// @param referralProjectId Optional referrer reference to credit with the protocol fee volume taken by this
|
|
828
|
+
/// call, encoded as `(referralChainId << 48) | referralProjectId` (chain ID in the upper 32 bits, project ID
|
|
829
|
+
/// in the lower 48). A bare project ID (chain bits zero) is auto-resolved to the current chain via `block.chainid`.
|
|
830
|
+
/// Pass `0` for no referral credit.
|
|
773
831
|
/// @return netAmountPaidOut The number of **terminal tokens** sent to `beneficiary`, net of the protocol fee, as a
|
|
774
832
|
/// fixed point number with the same number of decimals as the terminal token's accounting context.
|
|
775
833
|
function useAllowanceOf(
|
|
@@ -780,7 +838,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
780
838
|
uint256 minTokensPaidOut,
|
|
781
839
|
address payable beneficiary,
|
|
782
840
|
address payable feeBeneficiary,
|
|
783
|
-
string calldata memo
|
|
841
|
+
string calldata memo,
|
|
842
|
+
uint256 referralProjectId
|
|
784
843
|
)
|
|
785
844
|
external
|
|
786
845
|
override
|
|
@@ -792,6 +851,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
792
851
|
// Enforce permissions.
|
|
793
852
|
_requirePermissionFrom({account: owner, projectId: projectId, permissionId: JBPermissionIds.USE_ALLOWANCE});
|
|
794
853
|
|
|
854
|
+
uint256 priorReferral = _setReferralProjectId(referralProjectId);
|
|
855
|
+
|
|
795
856
|
netAmountPaidOut = _useAllowanceOf({
|
|
796
857
|
projectId: projectId,
|
|
797
858
|
owner: owner,
|
|
@@ -803,6 +864,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
803
864
|
memo: memo
|
|
804
865
|
});
|
|
805
866
|
|
|
867
|
+
_setReferralProjectId(priorReferral);
|
|
868
|
+
|
|
806
869
|
// The amount being withdrawn must be at least as much as was expected.
|
|
807
870
|
_checkMin({value: netAmountPaidOut, min: minTokensPaidOut});
|
|
808
871
|
}
|
|
@@ -877,30 +940,15 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
877
940
|
external
|
|
878
941
|
view
|
|
879
942
|
override
|
|
880
|
-
returns (JBFee[] memory
|
|
943
|
+
returns (JBFee[] memory)
|
|
881
944
|
{
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
if (startIndex >= numberOfHeldFees) return new JBFee[](0);
|
|
890
|
-
|
|
891
|
-
// If the start index plus the count is greater than the number of fees, set the count to the number of fees
|
|
892
|
-
if (startIndex + count > numberOfHeldFees) count = numberOfHeldFees - startIndex;
|
|
893
|
-
|
|
894
|
-
// Create a new array to hold the fees.
|
|
895
|
-
heldFees = new JBFee[](count);
|
|
896
|
-
|
|
897
|
-
// Copy the fees into the array.
|
|
898
|
-
for (uint256 i; i < count;) {
|
|
899
|
-
heldFees[i] = _heldFeesOf[projectId][token][startIndex + i];
|
|
900
|
-
unchecked {
|
|
901
|
-
++i;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
945
|
+
return JBHeldFees.viewHeldFees({
|
|
946
|
+
heldFeesOf: _heldFeesOf,
|
|
947
|
+
nextHeldFeeIndexOf: _nextHeldFeeIndexOf,
|
|
948
|
+
projectId: projectId,
|
|
949
|
+
token: token,
|
|
950
|
+
count: count
|
|
951
|
+
});
|
|
904
952
|
}
|
|
905
953
|
|
|
906
954
|
/// @notice Simulates a cash out without modifying state — use this to preview how many tokens a holder would
|
|
@@ -1703,6 +1751,15 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1703
1751
|
payer: payer, amount: tokenAmount, projectId: projectId, beneficiary: beneficiary, metadata: metadata
|
|
1704
1752
|
});
|
|
1705
1753
|
|
|
1754
|
+
// Credit the originating fee-paying call's referral project. This is only meaningful when this pay is a
|
|
1755
|
+
// protocol-fee payment landing on the fee project AND a referral was set by the outer entry point (or
|
|
1756
|
+
// restored from a held fee by `processHeldFeesOf`). The store handles the no-op case when either input is
|
|
1757
|
+
// zero. Done here (after `recordPaymentFrom`) instead of inside the store so the credit logic stays in the
|
|
1758
|
+
// terminal and the store is never asked to call back into us.
|
|
1759
|
+
if (projectId == JBConstants.FEE_BENEFICIARY_PROJECT_ID && currentReferralProjectId != 0) {
|
|
1760
|
+
STORE.recordFeeReferralCreditOf({referralProjectId: currentReferralProjectId, amount: tokenAmount});
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1706
1763
|
// Only the value retained in the destination balance needs later cashout fee recovery. Non-feeless pay-hook
|
|
1707
1764
|
// forwards pay their source-equivalent fee inline before leaving the project.
|
|
1708
1765
|
if (internalSplitPayProjectId != 0) {
|
|
@@ -1802,7 +1859,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1802
1859
|
emit FeeReverted({
|
|
1803
1860
|
projectId: projectId,
|
|
1804
1861
|
token: token,
|
|
1805
|
-
feeProjectId:
|
|
1862
|
+
feeProjectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID,
|
|
1806
1863
|
amount: amount,
|
|
1807
1864
|
reason: reason,
|
|
1808
1865
|
caller: _msgSender()
|
|
@@ -1830,66 +1887,13 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1830
1887
|
/// as the token's accounting context.
|
|
1831
1888
|
/// @return returnedFees The amount of held fees that were returned, as a fixed point number with the same number of
|
|
1832
1889
|
/// decimals as the token's accounting context.
|
|
1833
|
-
function _returnHeldFees(uint256 projectId, address token, uint256 amount) internal returns (uint256
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
// Use the original array length as the upper bound. Returning held fees never appends new entries.
|
|
1838
|
-
uint256 numberOfHeldFees = _heldFeesOf[projectId][token].length;
|
|
1839
|
-
|
|
1840
|
-
if (startIndex >= numberOfHeldFees) return 0;
|
|
1841
|
-
|
|
1842
|
-
// Track how much of the new balance remains available to match against held fees.
|
|
1843
|
-
uint256 leftoverAmount = amount;
|
|
1844
|
-
|
|
1845
|
-
// Move this forward for each fully returned held fee.
|
|
1846
|
-
uint256 newStartIndex = startIndex;
|
|
1847
|
-
|
|
1848
|
-
for (uint256 i = startIndex; i < numberOfHeldFees;) {
|
|
1849
|
-
if (leftoverAmount == 0) break;
|
|
1850
|
-
|
|
1851
|
-
// Held fees store the original gross amount that paid out before its fee was removed.
|
|
1852
|
-
JBFee memory heldFee = _heldFeesOf[projectId][token][i];
|
|
1853
|
-
|
|
1854
|
-
// Recompute the standard fee associated with the held gross amount.
|
|
1855
|
-
uint256 feeAmount = _feeAmountFrom(heldFee.amount);
|
|
1856
|
-
|
|
1857
|
-
// This is the net amount that originally left the project after the held fee was removed.
|
|
1858
|
-
uint256 amountPaidOut = heldFee.amount - feeAmount;
|
|
1859
|
-
|
|
1860
|
-
if (leftoverAmount >= amountPaidOut) {
|
|
1861
|
-
unchecked {
|
|
1862
|
-
leftoverAmount -= amountPaidOut;
|
|
1863
|
-
returnedFees += feeAmount;
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
// Move the start index forward to the fee after this fully returned one.
|
|
1867
|
-
newStartIndex = i + 1;
|
|
1868
|
-
} else {
|
|
1869
|
-
// Only part of this held fee can be returned. Convert the remaining net replenishment back into
|
|
1870
|
-
// its corresponding gross fee and shrink the stored gross amount.
|
|
1871
|
-
feeAmount = JBFees.standardFeeAmountResultingIn(leftoverAmount);
|
|
1872
|
-
|
|
1873
|
-
unchecked {
|
|
1874
|
-
_heldFeesOf[projectId][token][i].amount -= (leftoverAmount + feeAmount);
|
|
1875
|
-
returnedFees += feeAmount;
|
|
1876
|
-
}
|
|
1877
|
-
leftoverAmount = 0;
|
|
1878
|
-
}
|
|
1879
|
-
unchecked {
|
|
1880
|
-
++i;
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
// Update the next held fee index.
|
|
1885
|
-
if (startIndex != newStartIndex) _nextHeldFeeIndexOf[projectId][token] = newStartIndex;
|
|
1886
|
-
|
|
1887
|
-
emit ReturnHeldFees({
|
|
1890
|
+
function _returnHeldFees(uint256 projectId, address token, uint256 amount) internal returns (uint256) {
|
|
1891
|
+
return JBHeldFees.returnHeldFees({
|
|
1892
|
+
heldFeesOf: _heldFeesOf,
|
|
1893
|
+
nextHeldFeeIndexOf: _nextHeldFeeIndexOf,
|
|
1888
1894
|
projectId: projectId,
|
|
1889
1895
|
token: token,
|
|
1890
1896
|
amount: amount,
|
|
1891
|
-
returnedFees: returnedFees,
|
|
1892
|
-
leftoverAmount: leftoverAmount,
|
|
1893
1897
|
caller: _msgSender()
|
|
1894
1898
|
});
|
|
1895
1899
|
}
|
|
@@ -2010,7 +2014,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2010
2014
|
});
|
|
2011
2015
|
}
|
|
2012
2016
|
|
|
2013
|
-
/// @notice Takes a fee into the platform's project (with the `
|
|
2017
|
+
/// @notice Takes a fee into the platform's project (with the `JBConstants.FEE_BENEFICIARY_PROJECT_ID`).
|
|
2014
2018
|
/// @param projectId The ID of the project paying the fee.
|
|
2015
2019
|
/// @param token The address of the token that the fee is paid in.
|
|
2016
2020
|
/// @param amount The fee's token amount, as a fixed point number with the same number of decimals as the token's
|
|
@@ -2034,12 +2038,22 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2034
2038
|
|
|
2035
2039
|
if (shouldHoldFees) {
|
|
2036
2040
|
// Store the gross amount so future repayments can recover the corresponding fee.
|
|
2041
|
+
// Capture the in-flight `currentReferralProjectId` so attribution survives the 28-day hold window —
|
|
2042
|
+
// by the time `processHeldFeesOf` runs, the transient slot has been cleared. The encoded
|
|
2043
|
+
// `(chainId << 48) | projectId` pair in the transient slot is decomposed into its two halves so the
|
|
2044
|
+
// struct stays in 2 storage slots.
|
|
2045
|
+
uint256 encodedReferral = currentReferralProjectId;
|
|
2037
2046
|
_heldFeesOf[projectId][token].push(
|
|
2038
2047
|
JBFee({
|
|
2039
|
-
|
|
2048
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
2049
|
+
amount: uint224(amount),
|
|
2050
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
2051
|
+
referralChainId: uint32(encodedReferral >> 48),
|
|
2040
2052
|
beneficiary: beneficiary,
|
|
2041
2053
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
2042
|
-
unlockTimestamp: uint48(block.timestamp + _FEE_HOLDING_SECONDS)
|
|
2054
|
+
unlockTimestamp: uint48(block.timestamp + _FEE_HOLDING_SECONDS),
|
|
2055
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
2056
|
+
referralProjectId: uint48(encodedReferral)
|
|
2043
2057
|
})
|
|
2044
2058
|
);
|
|
2045
2059
|
|
|
@@ -2053,7 +2067,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2053
2067
|
});
|
|
2054
2068
|
} else {
|
|
2055
2069
|
// Resolve the fee project's terminal for this token and process the fee immediately.
|
|
2056
|
-
IJBTerminal feeTerminal =
|
|
2070
|
+
IJBTerminal feeTerminal =
|
|
2071
|
+
_primaryTerminalOf({projectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID, token: token});
|
|
2057
2072
|
|
|
2058
2073
|
_processFee({
|
|
2059
2074
|
projectId: projectId,
|
|
@@ -2293,4 +2308,22 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2293
2308
|
function _feeAmountFrom(uint256 amount) private pure returns (uint256) {
|
|
2294
2309
|
return JBFees.standardFeeAmountFrom(amount);
|
|
2295
2310
|
}
|
|
2311
|
+
|
|
2312
|
+
/// @notice Set the transient `currentReferralProjectId` slot and return the prior value (for save-restore).
|
|
2313
|
+
/// @dev Returning the prior value lets the caller restore it after the inner call completes, which is required
|
|
2314
|
+
/// to keep nested reentrant fee-paying calls from polluting each other. Per EIP-1153, a revert in the inner
|
|
2315
|
+
/// call also reverts the transient write, so no explicit cleanup on the failure path is needed.
|
|
2316
|
+
/// @dev Backfills `block.chainid` into the chain-bits half of the encoding when the caller passed a bare
|
|
2317
|
+
/// project ID (non-zero project, zero chain). This lets callers write `referralProjectId: someProjectId`
|
|
2318
|
+
/// without manually packing their own chain ID — the entry point resolves it to the current execution chain,
|
|
2319
|
+
/// so storage and indexers always see a fully-resolved `(chainId, projectId)` pair.
|
|
2320
|
+
/// @param referralProjectId The new value to write into the transient slot.
|
|
2321
|
+
/// @return prior The value previously stored in the slot.
|
|
2322
|
+
function _setReferralProjectId(uint256 referralProjectId) private returns (uint256 prior) {
|
|
2323
|
+
if (referralProjectId != 0 && referralProjectId >> 48 == 0) {
|
|
2324
|
+
referralProjectId |= block.chainid << 48;
|
|
2325
|
+
}
|
|
2326
|
+
prior = currentReferralProjectId;
|
|
2327
|
+
currentReferralProjectId = referralProjectId;
|
|
2328
|
+
}
|
|
2296
2329
|
}
|
package/src/JBTerminalStore.sol
CHANGED
|
@@ -91,6 +91,26 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
91
91
|
public
|
|
92
92
|
override balanceOf;
|
|
93
93
|
|
|
94
|
+
/// @notice Cumulative fee payment amount credited to a referrer (chainId, projectId) pair as a result of
|
|
95
|
+
/// fee-paying calls that originated through a given terminal.
|
|
96
|
+
/// @dev Written by terminals via `recordFeeReferralCreditOf`; the writing terminal is `msg.sender`, so a caller
|
|
97
|
+
/// can only pollute their own bucket.
|
|
98
|
+
/// @dev Nested by chain ID then project ID so off-chain consumers can read the credit for a specific
|
|
99
|
+
/// `(chainId, projectId)` directly without re-packing the encoded form themselves.
|
|
100
|
+
/// @custom:param terminal The terminal that originated the fee-paying call.
|
|
101
|
+
/// @custom:param referralChainId The EIP-155 chain ID of the referrer's home chain.
|
|
102
|
+
/// @custom:param referralProjectId The referrer's bare project ID on `referralChainId`.
|
|
103
|
+
mapping(address terminal => mapping(uint256 referralChainId => mapping(uint256 referralProjectId => uint256)))
|
|
104
|
+
public
|
|
105
|
+
override feeVolumeByReferralOf;
|
|
106
|
+
|
|
107
|
+
/// @notice Cumulative fee payment amount credited across all referral projects for a given terminal.
|
|
108
|
+
/// @dev Updated in lockstep with `feeVolumeByReferralOf` so consumers can compute a referrer's pro-rata share
|
|
109
|
+
/// in a single SLOAD pair without enumerating referrers. Used as the denominator by split hooks that distribute
|
|
110
|
+
/// rewards proportional to attributed fee volume.
|
|
111
|
+
/// @custom:param terminal The terminal that originated the fee-paying calls.
|
|
112
|
+
mapping(address terminal => uint256) public override totalFeeVolumeOf;
|
|
113
|
+
|
|
94
114
|
/// @notice The currency-denominated amount of funds that a project has already paid out from its payout limit
|
|
95
115
|
/// during the current ruleset for each terminal, in terms of the payout limit's currency.
|
|
96
116
|
/// @dev Increases as projects pay out funds.
|
|
@@ -322,6 +342,22 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
322
342
|
}
|
|
323
343
|
}
|
|
324
344
|
|
|
345
|
+
/// @notice Credit a referral project with a fee payment amount routed through `msg.sender` (the calling terminal).
|
|
346
|
+
/// @dev Called by `JBMultiTerminal._pay` after a payment lands on the fee project. Permissionless: writes are
|
|
347
|
+
/// scoped to `msg.sender`'s slots so an arbitrary caller can only pollute their own buckets — off-chain
|
|
348
|
+
/// consumers should filter on known terminal addresses. The amount is normalized to `NATIVE_TOKEN` units
|
|
349
|
+
/// (18 decimals) here in the store (where `PRICES` is available) so all credits share a common denominator.
|
|
350
|
+
/// @param referralProjectId The referral project to credit.
|
|
351
|
+
/// @param amount The fee amount paid by the originating fee-take call (raw value, decimals, currency).
|
|
352
|
+
function recordFeeReferralCreditOf(uint256 referralProjectId, JBTokenAmount calldata amount) external override {
|
|
353
|
+
_creditFeeReferral({
|
|
354
|
+
referralProjectId: referralProjectId,
|
|
355
|
+
amount: _normalizeToNativeTokenUnits({
|
|
356
|
+
value: amount.value, decimals: amount.decimals, currency: amount.currency
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
325
361
|
/// @notice Records a payment — calculates how many project tokens to mint based on the payment amount and the
|
|
326
362
|
/// current ruleset's weight. Uses the data hook if configured, otherwise mints proportionally.
|
|
327
363
|
/// @dev Called by the terminal after accepting funds. Updates the project's recorded balance.
|
|
@@ -1378,4 +1414,79 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1378
1414
|
}
|
|
1379
1415
|
}
|
|
1380
1416
|
}
|
|
1417
|
+
|
|
1418
|
+
//*********************************************************************//
|
|
1419
|
+
// -------------------------- private helpers ------------------------ //
|
|
1420
|
+
//*********************************************************************//
|
|
1421
|
+
|
|
1422
|
+
/// @notice Credit a referrer with a fee payment amount. Internal counterpart of `recordFeeReferralCreditOf` —
|
|
1423
|
+
/// both write to the same slots and emit the same event.
|
|
1424
|
+
/// @dev No-op when `referralProjectId == 0` or `amount == 0`.
|
|
1425
|
+
/// @dev Unpacks the encoded `(chainId << 48) | projectId` form into the nested mapping at write-time so the
|
|
1426
|
+
/// public getter exposes `(terminal, chainId, projectId) → cumulative` directly. The event topics carry the
|
|
1427
|
+
/// two halves separately as well.
|
|
1428
|
+
/// @param referralProjectId The packed `(chainId << 48) | projectId` referrer reference to credit.
|
|
1429
|
+
/// @param amount The fee amount to credit.
|
|
1430
|
+
function _creditFeeReferral(uint256 referralProjectId, uint256 amount) private {
|
|
1431
|
+
if (referralProjectId == 0 || amount == 0) return;
|
|
1432
|
+
|
|
1433
|
+
uint256 referralChainId = referralProjectId >> 48;
|
|
1434
|
+
uint256 bareProjectId = referralProjectId & ((1 << 48) - 1);
|
|
1435
|
+
|
|
1436
|
+
feeVolumeByReferralOf[msg.sender][referralChainId][bareProjectId] += amount;
|
|
1437
|
+
uint256 newTotal = totalFeeVolumeOf[msg.sender] + amount;
|
|
1438
|
+
totalFeeVolumeOf[msg.sender] = newTotal;
|
|
1439
|
+
|
|
1440
|
+
emit ReferralCredit({
|
|
1441
|
+
terminal: msg.sender,
|
|
1442
|
+
referralChainId: referralChainId,
|
|
1443
|
+
referralProjectId: bareProjectId,
|
|
1444
|
+
amount: amount,
|
|
1445
|
+
newTotal: newTotal
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/// @notice Normalize a fee-token amount to `JBConstants.NATIVE_TOKEN` units at 18 decimals.
|
|
1450
|
+
/// @dev Two-step: first adjust decimals to 18 via `JBFixedPointNumber.adjustDecimals`, then convert currency
|
|
1451
|
+
/// via `PRICES.pricePerUnitOf` using the fee project's price feeds. If no price feed exists for the pair, the
|
|
1452
|
+
/// `try` block catches the revert and the credit is silently skipped — the payment itself still succeeds.
|
|
1453
|
+
/// @param value The amount in the source token's native decimals.
|
|
1454
|
+
/// @param decimals The source token's decimals.
|
|
1455
|
+
/// @param currency The source token's accounting-context currency (`uint32(uint160(token))`).
|
|
1456
|
+
/// @return normalized The amount expressed in `NATIVE_TOKEN` units (18 decimals), or 0 if conversion failed.
|
|
1457
|
+
function _normalizeToNativeTokenUnits(
|
|
1458
|
+
uint256 value,
|
|
1459
|
+
uint256 decimals,
|
|
1460
|
+
uint256 currency
|
|
1461
|
+
)
|
|
1462
|
+
private
|
|
1463
|
+
view
|
|
1464
|
+
returns (uint256 normalized)
|
|
1465
|
+
{
|
|
1466
|
+
// Adjust the source amount up/down to 18 decimals so all credits share a common precision.
|
|
1467
|
+
normalized = decimals == _MAX_FIXED_POINT_FIDELITY
|
|
1468
|
+
? value
|
|
1469
|
+
: JBFixedPointNumber.adjustDecimals({
|
|
1470
|
+
value: value, decimals: decimals, targetDecimals: _MAX_FIXED_POINT_FIDELITY
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
if (normalized == 0 || currency == JBConstants.NATIVE_TOKEN_CURRENCY) return normalized;
|
|
1474
|
+
|
|
1475
|
+
// Convert from the source currency to NATIVE_TOKEN via the fee project's price feeds. A missing feed
|
|
1476
|
+
// reverts inside `PRICES.pricePerUnitOf` — caught here so the payment is not blocked.
|
|
1477
|
+
try PRICES.pricePerUnitOf({
|
|
1478
|
+
projectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID,
|
|
1479
|
+
pricingCurrency: currency,
|
|
1480
|
+
unitCurrency: JBConstants.NATIVE_TOKEN_CURRENCY,
|
|
1481
|
+
decimals: _MAX_FIXED_POINT_FIDELITY
|
|
1482
|
+
}) returns (
|
|
1483
|
+
uint256 price
|
|
1484
|
+
) {
|
|
1485
|
+
normalized = price == 0
|
|
1486
|
+
? 0
|
|
1487
|
+
: mulDiv({x: normalized, y: 10 ** _MAX_FIXED_POINT_FIDELITY, denominator: price});
|
|
1488
|
+
} catch {
|
|
1489
|
+
normalized = 0;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1381
1492
|
}
|
|
@@ -84,6 +84,8 @@ interface IJBCashOutTerminal is IJBTerminal {
|
|
|
84
84
|
/// @param minTokensReclaimed The minimum number of terminal tokens that must be reclaimed.
|
|
85
85
|
/// @param beneficiary The address to send the reclaimed terminal tokens to.
|
|
86
86
|
/// @param metadata Extra data to send to the data hook and cash out hooks.
|
|
87
|
+
/// @param referralProjectId Optional project to credit with the protocol fee volume taken by this call. Pass `0`
|
|
88
|
+
/// to credit the project being cashed out.
|
|
87
89
|
/// @return reclaimAmount The number of terminal tokens reclaimed from the project's surplus.
|
|
88
90
|
function cashOutTokensOf(
|
|
89
91
|
address holder,
|
|
@@ -92,7 +94,8 @@ interface IJBCashOutTerminal is IJBTerminal {
|
|
|
92
94
|
address tokenToReclaim,
|
|
93
95
|
uint256 minTokensReclaimed,
|
|
94
96
|
address payable beneficiary,
|
|
95
|
-
bytes calldata metadata
|
|
97
|
+
bytes calldata metadata,
|
|
98
|
+
uint256 referralProjectId
|
|
96
99
|
)
|
|
97
100
|
external
|
|
98
101
|
returns (uint256 reclaimAmount);
|
|
@@ -30,4 +30,17 @@ interface IJBMultiTerminal is IJBTerminal, IJBFeeTerminal, IJBCashOutTerminal, I
|
|
|
30
30
|
|
|
31
31
|
/// @notice The contract that manages token minting and burning.
|
|
32
32
|
function TOKENS() external view returns (IJBTokens);
|
|
33
|
+
|
|
34
|
+
/// @notice The caller-originated referrer reference for the in-flight fee-paying external call.
|
|
35
|
+
/// @dev Encoded as `(referralChainId << 48) | referralProjectId`: bits [79:48] are the referrer's EIP-155 chain
|
|
36
|
+
/// ID (uint32), bits [47:0] are the referrer's project ID on that chain (uint48). Allows a referrer with a
|
|
37
|
+
/// project on chain X to be credited for activity on chain Y. A bare project ID (chain bits zero) is auto-
|
|
38
|
+
/// resolved to the current execution chain via `block.chainid` at the entry point, so storage and indexers
|
|
39
|
+
/// always see a fully-resolved `(chainId, projectId)` pair.
|
|
40
|
+
/// @dev Backed by transient storage. Set by `cashOutTokensOf`, `sendPayoutsOf`, and `useAllowanceOf` (save-
|
|
41
|
+
/// restore wrapper) so hooks invoked during that call (pay hooks, cashout hooks, split hooks) can introspect
|
|
42
|
+
/// which referrer originated the activity. Reads `0` outside any fee-paying call.
|
|
43
|
+
/// @dev Per-referrer cumulative fee payment amounts credited via this terminal are stored in
|
|
44
|
+
/// `JBTerminalStore.feeVolumeByReferralOf(address terminal, uint256 referralProjectId)`.
|
|
45
|
+
function currentReferralProjectId() external view returns (uint256);
|
|
33
46
|
}
|
|
@@ -102,13 +102,16 @@ interface IJBPayoutTerminal is IJBTerminal {
|
|
|
102
102
|
/// @param amount The total amount of tokens to pay out.
|
|
103
103
|
/// @param currency The currency the amount is denominated in.
|
|
104
104
|
/// @param minTokensPaidOut The minimum number of terminal tokens expected to be paid out.
|
|
105
|
+
/// @param referralProjectId Optional project to credit with the protocol fee volume taken by this call. Pass `0`
|
|
106
|
+
/// to credit the project sending payouts.
|
|
105
107
|
/// @return amountPaidOut The total amount paid out.
|
|
106
108
|
function sendPayoutsOf(
|
|
107
109
|
uint256 projectId,
|
|
108
110
|
address token,
|
|
109
111
|
uint256 amount,
|
|
110
112
|
uint256 currency,
|
|
111
|
-
uint256 minTokensPaidOut
|
|
113
|
+
uint256 minTokensPaidOut,
|
|
114
|
+
uint256 referralProjectId
|
|
112
115
|
)
|
|
113
116
|
external
|
|
114
117
|
returns (uint256 amountPaidOut);
|
|
@@ -122,6 +125,8 @@ interface IJBPayoutTerminal is IJBTerminal {
|
|
|
122
125
|
/// @param beneficiary The address to send the funds to.
|
|
123
126
|
/// @param feeBeneficiary The address that will receive any project tokens minted from fees.
|
|
124
127
|
/// @param memo A memo to pass along to the emitted event.
|
|
128
|
+
/// @param referralProjectId Optional project to credit with the protocol fee volume taken by this call. Pass `0`
|
|
129
|
+
/// to credit the project whose surplus allowance is being used.
|
|
125
130
|
/// @return netAmountPaidOut The net amount paid out to the beneficiary after fees.
|
|
126
131
|
function useAllowanceOf(
|
|
127
132
|
uint256 projectId,
|
|
@@ -131,7 +136,8 @@ interface IJBPayoutTerminal is IJBTerminal {
|
|
|
131
136
|
uint256 minTokensPaidOut,
|
|
132
137
|
address payable beneficiary,
|
|
133
138
|
address payable feeBeneficiary,
|
|
134
|
-
string calldata memo
|
|
139
|
+
string calldata memo,
|
|
140
|
+
uint256 referralProjectId
|
|
135
141
|
)
|
|
136
142
|
external
|
|
137
143
|
returns (uint256 netAmountPaidOut);
|
|
@@ -15,6 +15,24 @@ import {JBTokenAmount} from "../structs/JBTokenAmount.sol";
|
|
|
15
15
|
/// allowances, calculates token issuance per payment, and determines cash-out reclaim amounts via the bonding curve.
|
|
16
16
|
/// Terminals delegate all state-changing accounting to this contract.
|
|
17
17
|
interface IJBTerminalStore {
|
|
18
|
+
/// @notice Emitted when a referrer is credited with a fee payment amount.
|
|
19
|
+
/// @dev `referralChainId` and `referralProjectId` are emitted as separate indexed topics so off-chain consumers
|
|
20
|
+
/// can filter directly on either dimension. The `feeVolumeByReferralOf` storage key is the packed
|
|
21
|
+
/// `(referralChainId << 48) | referralProjectId` form — indexers re-pack the two fields when looking up the
|
|
22
|
+
/// cumulative balance for a referrer.
|
|
23
|
+
/// @param terminal The terminal that originated the fee-paying call (`msg.sender` on `recordFeeReferralCreditOf`).
|
|
24
|
+
/// @param referralChainId The EIP-155 chain ID of the referrer's home chain.
|
|
25
|
+
/// @param referralProjectId The referrer's bare project ID on `referralChainId` (no chain bits).
|
|
26
|
+
/// @param amount The fee amount credited, in the terminal's accounting-context units.
|
|
27
|
+
/// @param newTotal The new value of `totalFeeVolumeOf[terminal]` after this credit.
|
|
28
|
+
event ReferralCredit(
|
|
29
|
+
address indexed terminal,
|
|
30
|
+
uint256 indexed referralChainId,
|
|
31
|
+
uint256 indexed referralProjectId,
|
|
32
|
+
uint256 amount,
|
|
33
|
+
uint256 newTotal
|
|
34
|
+
);
|
|
35
|
+
|
|
18
36
|
/// @notice The directory of terminals and controllers for projects.
|
|
19
37
|
function DIRECTORY() external view returns (IJBDirectory);
|
|
20
38
|
|
|
@@ -142,6 +160,27 @@ interface IJBTerminalStore {
|
|
|
142
160
|
view
|
|
143
161
|
returns (uint256);
|
|
144
162
|
|
|
163
|
+
/// @notice The cumulative fee payment amount credited to a referrer (chainId, projectId) pair as a result of
|
|
164
|
+
/// fee-paying calls that originated through a given terminal.
|
|
165
|
+
/// @dev Written by terminals via `recordFeeReferralCreditOf` — `msg.sender` is recorded as the writing terminal,
|
|
166
|
+
/// so a malicious caller can only pollute their own slot. Off-chain consumers should filter on known terminal
|
|
167
|
+
/// addresses.
|
|
168
|
+
/// @dev Nested by chain ID then project ID so consumers can read the credit for a specific
|
|
169
|
+
/// `(chainId, projectId)` directly without re-packing the encoded form. The encoded `(chainId << 48) | projectId`
|
|
170
|
+
/// form is used only inside the transient slot and the entry-point parameter; storage exposes the unpacked pair.
|
|
171
|
+
/// @param terminal The terminal that originated the fee-paying call.
|
|
172
|
+
/// @param referralChainId The EIP-155 chain ID of the referrer's home chain.
|
|
173
|
+
/// @param referralProjectId The referrer's bare project ID on `referralChainId`.
|
|
174
|
+
/// @return The cumulative fee amount credited.
|
|
175
|
+
function feeVolumeByReferralOf(
|
|
176
|
+
address terminal,
|
|
177
|
+
uint256 referralChainId,
|
|
178
|
+
uint256 referralProjectId
|
|
179
|
+
)
|
|
180
|
+
external
|
|
181
|
+
view
|
|
182
|
+
returns (uint256);
|
|
183
|
+
|
|
145
184
|
/// @notice Simulates a cash out without modifying state.
|
|
146
185
|
/// @param terminal The terminal to simulate the cash out from.
|
|
147
186
|
/// @param holder The address cashing out.
|
|
@@ -194,6 +233,13 @@ interface IJBTerminalStore {
|
|
|
194
233
|
view
|
|
195
234
|
returns (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications);
|
|
196
235
|
|
|
236
|
+
/// @notice The cumulative fee payment amount credited across all referral projects for a given terminal.
|
|
237
|
+
/// @dev Incremented in lockstep with `feeVolumeByReferralOf` so consumers can compute pro-rata shares in a
|
|
238
|
+
/// single SLOAD pair without enumerating referrers.
|
|
239
|
+
/// @param terminal The terminal that originated the fee-paying calls.
|
|
240
|
+
/// @return The cumulative total fee amount credited across all referrers for this terminal.
|
|
241
|
+
function totalFeeVolumeOf(address terminal) external view returns (uint256);
|
|
242
|
+
|
|
197
243
|
/// @notice Returns the amount of payout limit used by a terminal for a project in a given cycle.
|
|
198
244
|
/// @param terminal The terminal to get the used payout limit of.
|
|
199
245
|
/// @param projectId The ID of the project.
|
|
@@ -269,6 +315,15 @@ interface IJBTerminalStore {
|
|
|
269
315
|
JBCashOutHookSpecification[] memory hookSpecifications
|
|
270
316
|
);
|
|
271
317
|
|
|
318
|
+
/// @notice Credit a referral project with a fee payment amount routed through `msg.sender` (the calling terminal).
|
|
319
|
+
/// @dev Permissionless: the write is scoped to `msg.sender`'s slot, so an arbitrary caller can only pollute
|
|
320
|
+
/// their own bucket. The amount is normalized to `JBConstants.NATIVE_TOKEN` units (18 decimals) using the fee
|
|
321
|
+
/// project's price feeds so credits across different fee tokens (ETH, USDC, USDT, …) are summable. No-op when
|
|
322
|
+
/// `referralProjectId == 0`, when `amount.value == 0`, or when no price feed exists for the pair.
|
|
323
|
+
/// @param referralProjectId The referral project to credit.
|
|
324
|
+
/// @param amount The fee amount paid by this fee-take call (raw token amount, decimals, and currency).
|
|
325
|
+
function recordFeeReferralCreditOf(uint256 referralProjectId, JBTokenAmount calldata amount) external;
|
|
326
|
+
|
|
272
327
|
/// @notice Records a payment to a project.
|
|
273
328
|
/// @param payer The address of the payer.
|
|
274
329
|
/// @param amount The amount to pay.
|
|
@@ -3,24 +3,34 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
/// @notice Protocol-wide constants. These define the boundaries for economic parameters throughout Juicebox.
|
|
5
5
|
library JBConstants {
|
|
6
|
-
/// @notice The
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/// @notice The maximum reserved token percentage (basis points). 10,000 = 100% of minted tokens go to reserves.
|
|
10
|
-
uint16 public constant MAX_RESERVED_PERCENT = 10_000;
|
|
6
|
+
/// @notice The project ID that receives protocol fees. Project #1 is the protocol's own project; every
|
|
7
|
+
/// terminal forwards 2.5% of qualifying outflows to its primary fee terminal for this project.
|
|
8
|
+
uint256 public constant FEE_BENEFICIARY_PROJECT_ID = 1;
|
|
11
9
|
|
|
12
10
|
/// @notice The maximum cash-out tax rate (basis points). 10,000 = 100% tax, meaning token holders reclaim nothing.
|
|
13
11
|
uint16 public constant MAX_CASH_OUT_TAX_RATE = 10_000;
|
|
14
12
|
|
|
13
|
+
/// @notice The fee denominator. The protocol fee is `STANDARD_FEE / MAX_FEE`.
|
|
14
|
+
uint16 public constant MAX_FEE = 1000;
|
|
15
|
+
|
|
16
|
+
/// @notice The maximum reserved token percentage (basis points). 10,000 = 100% of minted tokens go to reserves.
|
|
17
|
+
uint16 public constant MAX_RESERVED_PERCENT = 10_000;
|
|
18
|
+
|
|
15
19
|
/// @notice The maximum weight cut percent (9-decimal precision). 1,000,000,000 = 100% cut per cycle (no issuance).
|
|
16
20
|
uint32 public constant MAX_WEIGHT_CUT_PERCENT = 1_000_000_000;
|
|
17
21
|
|
|
22
|
+
/// @notice The sentinel address used to represent each chain's native token (ETH on mainnet, etc.).
|
|
23
|
+
address public constant NATIVE_TOKEN = address(0x000000000000000000000000000000000000EEEe);
|
|
24
|
+
|
|
25
|
+
/// @notice The accounting-context currency identifier for `NATIVE_TOKEN`. Derived from `NATIVE_TOKEN`'s address
|
|
26
|
+
/// through the same cast (`uint32(uint160(token))`) that `JBAccountingContext.currency` uses, so a comparison
|
|
27
|
+
/// against this constant identifies the native-token currency without recomputing the cast at each call site.
|
|
28
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
29
|
+
uint32 public constant NATIVE_TOKEN_CURRENCY = uint32(uint160(NATIVE_TOKEN));
|
|
30
|
+
|
|
18
31
|
/// @notice The denominator for split percentages (9-decimal precision). A split of 1,000,000,000 = 100%.
|
|
19
32
|
uint32 public constant SPLITS_TOTAL_PERCENT = 1_000_000_000;
|
|
20
33
|
|
|
21
|
-
/// @notice The fee denominator. The protocol fee is `STANDARD_FEE / MAX_FEE`.
|
|
22
|
-
uint16 public constant MAX_FEE = 1000;
|
|
23
|
-
|
|
24
34
|
/// @notice The standard protocol fee numerator. The protocol fee is `STANDARD_FEE / MAX_FEE` = 2.5%.
|
|
25
35
|
uint16 public constant STANDARD_FEE = 25;
|
|
26
36
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {IJBFeeTerminal} from "../interfaces/IJBFeeTerminal.sol";
|
|
5
|
+
import {JBFee} from "../structs/JBFee.sol";
|
|
6
|
+
import {JBFees} from "./JBFees.sol";
|
|
7
|
+
|
|
8
|
+
/// @notice External-library implementation of held-fee storage operations for `JBMultiTerminal`.
|
|
9
|
+
/// @dev Functions are `external` so they live in this library's deployed bytecode and are reached from the caller
|
|
10
|
+
/// terminal via `DELEGATECALL`, keeping terminal bytecode below the EIP-170 limit. Storage refs are passed as
|
|
11
|
+
/// parameters so the library reads/writes the caller's mappings.
|
|
12
|
+
library JBHeldFees {
|
|
13
|
+
/// @notice Returns held fees back to a project's balance based on the specified incoming amount.
|
|
14
|
+
/// @dev Walks unprocessed held fees in storage order. For each fee, checks if the incoming `amount` covers
|
|
15
|
+
/// the original net payout (gross minus fee). If yes, the fee is fully credited back and the entry is
|
|
16
|
+
/// tombstoned by advancing the index. If only part of the fee can be refunded, the entry's stored gross
|
|
17
|
+
/// amount is shrunk in place using the back-calculated fee so a future top-up can still settle the remainder.
|
|
18
|
+
/// @dev Partial refunds use `standardFeeAmountResultingIn` so repaying a dust amount cannot both credit the
|
|
19
|
+
/// payer project and leave the fee project owed the 1-unit minimum fee. Reads `caller` from a parameter
|
|
20
|
+
/// instead of `msg.sender` because the library runs under `DELEGATECALL` from the terminal — `msg.sender`
|
|
21
|
+
/// here is the original external caller of the terminal, not the meta-tx-aware caller the terminal would
|
|
22
|
+
/// emit. The terminal passes `_msgSender()` so the event carries the meta-tx sender as intended.
|
|
23
|
+
/// @param heldFeesOf The terminal's held-fee storage mapping.
|
|
24
|
+
/// @param nextHeldFeeIndexOf The terminal's per-project/token next-index storage mapping.
|
|
25
|
+
/// @param projectId The project to return held fees to.
|
|
26
|
+
/// @param token The token that the held fees are in.
|
|
27
|
+
/// @param amount The incoming amount available to match against held fees.
|
|
28
|
+
/// @param caller The address that triggered the return, forwarded into the `ReturnHeldFees` event.
|
|
29
|
+
/// @return returnedFees The total fee amount returned to the project (sum of fully and partially refunded fees).
|
|
30
|
+
function returnHeldFees(
|
|
31
|
+
mapping(uint256 => mapping(address => JBFee[])) storage heldFeesOf,
|
|
32
|
+
mapping(uint256 => mapping(address => uint256)) storage nextHeldFeeIndexOf,
|
|
33
|
+
uint256 projectId,
|
|
34
|
+
address token,
|
|
35
|
+
uint256 amount,
|
|
36
|
+
address caller
|
|
37
|
+
)
|
|
38
|
+
external
|
|
39
|
+
returns (uint256 returnedFees)
|
|
40
|
+
{
|
|
41
|
+
// The first slot not yet returned, processed, or forgiven. Earlier slots are tombstones and skipped.
|
|
42
|
+
uint256 startIndex = nextHeldFeeIndexOf[projectId][token];
|
|
43
|
+
|
|
44
|
+
// Upper bound for the loop. Returning held fees never appends new entries, so the live length is fixed
|
|
45
|
+
// for the duration of this call.
|
|
46
|
+
uint256 numberOfHeldFees = heldFeesOf[projectId][token].length;
|
|
47
|
+
|
|
48
|
+
// No live entries — nothing to refund. Early return keeps the gas cost predictable for projects that
|
|
49
|
+
// never held a fee.
|
|
50
|
+
if (startIndex >= numberOfHeldFees) return 0;
|
|
51
|
+
|
|
52
|
+
// Tracks how much of the incoming `amount` is still available to match against further held fees as the
|
|
53
|
+
// loop consumes them. Initialized to the full incoming amount.
|
|
54
|
+
uint256 leftoverAmount = amount;
|
|
55
|
+
|
|
56
|
+
// Tracks how far the live-entry window has advanced. Only persisted if it actually moves, to avoid an
|
|
57
|
+
// unnecessary SSTORE when the loop ends without fully refunding any fee.
|
|
58
|
+
uint256 newStartIndex = startIndex;
|
|
59
|
+
|
|
60
|
+
for (uint256 i = startIndex; i < numberOfHeldFees;) {
|
|
61
|
+
// Stop early once the incoming amount has been fully consumed. Remaining held fees stay live and
|
|
62
|
+
// can be refunded by a future top-up.
|
|
63
|
+
if (leftoverAmount == 0) break;
|
|
64
|
+
|
|
65
|
+
// Snapshot the held fee into memory so subsequent reads (`.amount`) don't re-fetch from storage.
|
|
66
|
+
JBFee memory heldFee = heldFeesOf[projectId][token][i];
|
|
67
|
+
|
|
68
|
+
// Recompute the standard fee that was originally withheld from this entry's gross amount.
|
|
69
|
+
uint256 feeAmount = JBFees.standardFeeAmountFrom(heldFee.amount);
|
|
70
|
+
|
|
71
|
+
// The net amount that originally left the project after the fee was withheld — the deposit threshold
|
|
72
|
+
// this incoming amount must clear to fully release the fee.
|
|
73
|
+
uint256 amountPaidOut = heldFee.amount - feeAmount;
|
|
74
|
+
|
|
75
|
+
if (leftoverAmount >= amountPaidOut) {
|
|
76
|
+
// Full refund: the incoming amount covers everything the project paid out for this entry, so
|
|
77
|
+
// the whole fee comes back. Consume `amountPaidOut` from `leftoverAmount` and advance the
|
|
78
|
+
// tombstone window past this slot.
|
|
79
|
+
unchecked {
|
|
80
|
+
leftoverAmount -= amountPaidOut;
|
|
81
|
+
returnedFees += feeAmount;
|
|
82
|
+
}
|
|
83
|
+
newStartIndex = i + 1;
|
|
84
|
+
} else {
|
|
85
|
+
// Partial refund: only some of the original payout has been redeposited. Back-calculate the
|
|
86
|
+
// fee that corresponds to the remaining net amount (using `standardFeeAmountResultingIn` so
|
|
87
|
+
// the fee project never gets shorted on dust). Shrink the stored gross amount by both the
|
|
88
|
+
// refunded net and its fee, leaving the slot live so the leftover can be refunded later.
|
|
89
|
+
feeAmount = JBFees.standardFeeAmountResultingIn(leftoverAmount);
|
|
90
|
+
unchecked {
|
|
91
|
+
// `JBFee.amount` is `uint224`; the subtraction operand is `uint256`. Narrow the operand to
|
|
92
|
+
// `uint224` for the in-place update. The held gross amount was itself stored as `uint224`
|
|
93
|
+
// and the partial refund is bounded above by it, so the narrowed subtrahend always fits.
|
|
94
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
95
|
+
heldFeesOf[projectId][token][i].amount -= uint224(leftoverAmount + feeAmount);
|
|
96
|
+
returnedFees += feeAmount;
|
|
97
|
+
}
|
|
98
|
+
// All of the incoming amount has been matched — exit the loop next iteration.
|
|
99
|
+
leftoverAmount = 0;
|
|
100
|
+
}
|
|
101
|
+
unchecked {
|
|
102
|
+
++i;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Persist the new tombstone boundary only if any entry was fully refunded — partial refunds leave the
|
|
107
|
+
// boundary alone so they can be revisited.
|
|
108
|
+
if (startIndex != newStartIndex) nextHeldFeeIndexOf[projectId][token] = newStartIndex;
|
|
109
|
+
|
|
110
|
+
// Emit through the interface qualifier so the event topic matches what `IJBFeeTerminal` declares. The
|
|
111
|
+
// log surfaces under the calling terminal's address (we're inside its DELEGATECALL frame), so off-chain
|
|
112
|
+
// consumers filter on the terminal as usual.
|
|
113
|
+
emit IJBFeeTerminal.ReturnHeldFees({
|
|
114
|
+
projectId: projectId,
|
|
115
|
+
token: token,
|
|
116
|
+
amount: amount,
|
|
117
|
+
returnedFees: returnedFees,
|
|
118
|
+
leftoverAmount: leftoverAmount,
|
|
119
|
+
caller: caller
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// @notice Returns up to `count` held fees for a project/token, starting from the next unprocessed index.
|
|
124
|
+
/// @param heldFeesOf The terminal's held-fee storage mapping (project => token => array).
|
|
125
|
+
/// @param nextHeldFeeIndexOf The terminal's per-project/token next-index storage mapping.
|
|
126
|
+
/// @param projectId The ID of the project to read held fees for.
|
|
127
|
+
/// @param token The token the fees are denominated in.
|
|
128
|
+
/// @param count The maximum number of held fees to return.
|
|
129
|
+
/// @return heldFees A view-only copy of the unprocessed held fees, in storage order.
|
|
130
|
+
function viewHeldFees(
|
|
131
|
+
mapping(uint256 => mapping(address => JBFee[])) storage heldFeesOf,
|
|
132
|
+
mapping(uint256 => mapping(address => uint256)) storage nextHeldFeeIndexOf,
|
|
133
|
+
uint256 projectId,
|
|
134
|
+
address token,
|
|
135
|
+
uint256 count
|
|
136
|
+
)
|
|
137
|
+
external
|
|
138
|
+
view
|
|
139
|
+
returns (JBFee[] memory heldFees)
|
|
140
|
+
{
|
|
141
|
+
// The first slot not yet returned, processed, or forgiven. Slots before this are tombstones (zeroed by
|
|
142
|
+
// `processHeldFeesOf` / `returnHeldFees`) and must be skipped so callers only see live fees.
|
|
143
|
+
uint256 startIndex = nextHeldFeeIndexOf[projectId][token];
|
|
144
|
+
|
|
145
|
+
// Total entries ever appended for this project/token (live + tombstoned). Used as the upper bound when
|
|
146
|
+
// bounding `count`.
|
|
147
|
+
uint256 numberOfHeldFees = heldFeesOf[projectId][token].length;
|
|
148
|
+
|
|
149
|
+
// Nothing live to return — either the array was never populated, or every entry has already been processed
|
|
150
|
+
// since the last full-array delete. Return an empty array so callers can branch on `.length == 0`.
|
|
151
|
+
if (startIndex >= numberOfHeldFees) return new JBFee[](0);
|
|
152
|
+
|
|
153
|
+
// Cap `count` to the number of live entries so we don't index past `length`. Keeps the common
|
|
154
|
+
// "ask for more than there is" case from over-allocating the return buffer.
|
|
155
|
+
if (startIndex + count > numberOfHeldFees) count = numberOfHeldFees - startIndex;
|
|
156
|
+
|
|
157
|
+
// Allocate the return buffer at exactly the size we'll fill — saves the caller from filtering trailing
|
|
158
|
+
// empty entries.
|
|
159
|
+
heldFees = new JBFee[](count);
|
|
160
|
+
|
|
161
|
+
// Copy live entries from storage into the return buffer. `++i` is unchecked because `i < count` already
|
|
162
|
+
// bounds it well below `type(uint256).max`.
|
|
163
|
+
for (uint256 i; i < count;) {
|
|
164
|
+
heldFees[i] = heldFeesOf[projectId][token][startIndex + i];
|
|
165
|
+
unchecked {
|
|
166
|
+
++i;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/structs/JBFee.sol
CHANGED
|
@@ -2,11 +2,22 @@
|
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
4
|
/// @custom:member amount The total amount the fee was taken from, as a fixed point number with the same number of
|
|
5
|
-
/// decimals as the token's accounting context.
|
|
5
|
+
/// decimals as the token's accounting context. `uint224` covers any realistic per-call fee basis (max
|
|
6
|
+
/// ~2.7e49 ETH-equivalent at 18 decimals) and is the same width the protocol's payout/surplus-allowance limits use.
|
|
7
|
+
/// @custom:member referralChainId The EIP-155 chain ID of the referrer's home chain, captured from the upper bits of
|
|
8
|
+
/// the transient referral encoding at fee-take time. A value of `0` is the "current chain" sentinel — a referrer that
|
|
9
|
+
/// only encoded a project ID without a chain prefix is credited on the chain the fee is paid on. Packing this beside
|
|
10
|
+
/// `amount` in the same storage slot avoids growing `JBFee` to a third slot.
|
|
6
11
|
/// @custom:member beneficiary The address that will receive the tokens that are minted as a result of the fee payment.
|
|
7
12
|
/// @custom:member unlockTimestamp The timestamp at which the fee is unlocked and can be processed.
|
|
13
|
+
/// @custom:member referralProjectId The referrer's project ID on `referralChainId`. Captured from the lower bits of
|
|
14
|
+
/// `currentReferralProjectId` at fee-take time so held-fee attribution survives the 28-day hold window. The on-chain
|
|
15
|
+
/// transient slot, function argument, and `feeVolumeByReferralOf` mapping key all use the packed `uint256` form
|
|
16
|
+
/// `(referralChainId << 48) | referralProjectId`; this struct stores the two halves separately for cheap storage.
|
|
8
17
|
struct JBFee {
|
|
9
|
-
|
|
18
|
+
uint224 amount;
|
|
19
|
+
uint32 referralChainId;
|
|
10
20
|
address beneficiary;
|
|
11
21
|
uint48 unlockTimestamp;
|
|
22
|
+
uint48 referralProjectId;
|
|
12
23
|
}
|
|
@@ -18,7 +18,8 @@ contract MaliciousPayoutBeneficiary is IERC721Receiver, Test {
|
|
|
18
18
|
amount: 5 * 10 ** 18,
|
|
19
19
|
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
20
20
|
token: JBConstants.NATIVE_TOKEN,
|
|
21
|
-
minTokensPaidOut: 0
|
|
21
|
+
minTokensPaidOut: 0,
|
|
22
|
+
referralProjectId: 0
|
|
22
23
|
});
|
|
23
24
|
assertEq(_reentrantPayout, 0);
|
|
24
25
|
}
|
|
@@ -49,7 +50,8 @@ contract MaliciousAllowanceBeneficiary is IERC721Receiver, Test {
|
|
|
49
50
|
minTokensPaidOut: 0,
|
|
50
51
|
beneficiary: payable(address(this)),
|
|
51
52
|
feeBeneficiary: payable(0x000000000000000000000000000000000000007B),
|
|
52
|
-
memo: "MEMO"
|
|
53
|
+
memo: "MEMO",
|
|
54
|
+
referralProjectId: 0
|
|
53
55
|
});
|
|
54
56
|
}
|
|
55
57
|
|