@bananapus/core-v6 0.0.58 → 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 +126 -95
- 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";
|
|
@@ -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
|
}
|
|
@@ -498,7 +525,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
498
525
|
|
|
499
526
|
_efficientPay({
|
|
500
527
|
terminal: feeTerminal,
|
|
501
|
-
projectId:
|
|
528
|
+
projectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID,
|
|
502
529
|
token: token,
|
|
503
530
|
amount: amount,
|
|
504
531
|
beneficiary: beneficiary,
|
|
@@ -569,7 +596,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
569
596
|
// Migration to a non-feeless terminal incurs the standard 2.5% fee, same as any other fund egress.
|
|
570
597
|
// This also settles any fee-free surplus liability that would otherwise be lost on the new terminal.
|
|
571
598
|
uint256 feeAmount;
|
|
572
|
-
if (
|
|
599
|
+
if (
|
|
600
|
+
!_isFeeless({addr: address(to), projectId: projectId})
|
|
601
|
+
&& projectId != JBConstants.FEE_BENEFICIARY_PROJECT_ID
|
|
602
|
+
) {
|
|
573
603
|
// Fee processing failures never block migration. If the fee route is broken, `_processFee` credits
|
|
574
604
|
// the fee amount back to this source terminal and emits `FeeReverted`; the post-fee amount still
|
|
575
605
|
// migrates so project funds are not trapped behind project #1 routing issues.
|
|
@@ -666,7 +696,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
666
696
|
/// @param count The number of fees to process.
|
|
667
697
|
function processHeldFeesOf(uint256 projectId, address token, uint256 count) external override {
|
|
668
698
|
// Keep a reference to the terminal that'll receive the fees.
|
|
669
|
-
IJBTerminal feeTerminal = _primaryTerminalOf({projectId:
|
|
699
|
+
IJBTerminal feeTerminal = _primaryTerminalOf({projectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID, token: token});
|
|
670
700
|
|
|
671
701
|
// Process each fee. Re-read the index and array length from storage each iteration to account for reentrant
|
|
672
702
|
// calls that may have already advanced the index or cleaned up the array.
|
|
@@ -696,6 +726,13 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
696
726
|
delete _heldFeesOf[projectId][token][currentIndex];
|
|
697
727
|
_nextHeldFeeIndexOf[projectId][token] = currentIndex + 1;
|
|
698
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
|
+
|
|
699
736
|
// Process the standard fee on the original gross amount recorded when the held fee was created.
|
|
700
737
|
_processFee({
|
|
701
738
|
projectId: projectId,
|
|
@@ -705,11 +742,17 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
705
742
|
feeTerminal: feeTerminal,
|
|
706
743
|
wasHeld: true
|
|
707
744
|
});
|
|
745
|
+
|
|
708
746
|
unchecked {
|
|
709
747
|
++i;
|
|
710
748
|
}
|
|
711
749
|
}
|
|
712
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
|
+
|
|
713
756
|
// If all held fees have been processed, reset the array and index entirely to bound storage growth.
|
|
714
757
|
if (
|
|
715
758
|
_nextHeldFeeIndexOf[projectId][token] >= _heldFeesOf[projectId][token].length
|
|
@@ -736,20 +779,29 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
736
779
|
/// in terms of the token's accounting context currency), as a fixed point number with the same number of decimals
|
|
737
780
|
/// as the token's accounting context. If the amount of tokens paid out would be less than this amount, the send is
|
|
738
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.
|
|
739
786
|
/// @return amountPaidOut The total amount paid out.
|
|
740
787
|
function sendPayoutsOf(
|
|
741
788
|
uint256 projectId,
|
|
742
789
|
address token,
|
|
743
790
|
uint256 amount,
|
|
744
791
|
uint256 currency,
|
|
745
|
-
uint256 minTokensPaidOut
|
|
792
|
+
uint256 minTokensPaidOut,
|
|
793
|
+
uint256 referralProjectId
|
|
746
794
|
)
|
|
747
795
|
external
|
|
748
796
|
override
|
|
749
797
|
returns (uint256 amountPaidOut)
|
|
750
798
|
{
|
|
799
|
+
uint256 priorReferral = _setReferralProjectId(referralProjectId);
|
|
800
|
+
|
|
751
801
|
amountPaidOut = _sendPayoutsOf({projectId: projectId, token: token, amount: amount, currency: currency});
|
|
752
802
|
|
|
803
|
+
_setReferralProjectId(priorReferral);
|
|
804
|
+
|
|
753
805
|
// The amount being paid out must be at least as much as was expected.
|
|
754
806
|
_checkMin({value: amountPaidOut, min: minTokensPaidOut});
|
|
755
807
|
}
|
|
@@ -772,6 +824,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
772
824
|
/// @param feeBeneficiary The address that receives the **project tokens** minted by the fee project in exchange
|
|
773
825
|
/// for the protocol fee paid in terminal tokens.
|
|
774
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.
|
|
775
831
|
/// @return netAmountPaidOut The number of **terminal tokens** sent to `beneficiary`, net of the protocol fee, as a
|
|
776
832
|
/// fixed point number with the same number of decimals as the terminal token's accounting context.
|
|
777
833
|
function useAllowanceOf(
|
|
@@ -782,7 +838,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
782
838
|
uint256 minTokensPaidOut,
|
|
783
839
|
address payable beneficiary,
|
|
784
840
|
address payable feeBeneficiary,
|
|
785
|
-
string calldata memo
|
|
841
|
+
string calldata memo,
|
|
842
|
+
uint256 referralProjectId
|
|
786
843
|
)
|
|
787
844
|
external
|
|
788
845
|
override
|
|
@@ -794,6 +851,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
794
851
|
// Enforce permissions.
|
|
795
852
|
_requirePermissionFrom({account: owner, projectId: projectId, permissionId: JBPermissionIds.USE_ALLOWANCE});
|
|
796
853
|
|
|
854
|
+
uint256 priorReferral = _setReferralProjectId(referralProjectId);
|
|
855
|
+
|
|
797
856
|
netAmountPaidOut = _useAllowanceOf({
|
|
798
857
|
projectId: projectId,
|
|
799
858
|
owner: owner,
|
|
@@ -805,6 +864,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
805
864
|
memo: memo
|
|
806
865
|
});
|
|
807
866
|
|
|
867
|
+
_setReferralProjectId(priorReferral);
|
|
868
|
+
|
|
808
869
|
// The amount being withdrawn must be at least as much as was expected.
|
|
809
870
|
_checkMin({value: netAmountPaidOut, min: minTokensPaidOut});
|
|
810
871
|
}
|
|
@@ -879,30 +940,15 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
879
940
|
external
|
|
880
941
|
view
|
|
881
942
|
override
|
|
882
|
-
returns (JBFee[] memory
|
|
943
|
+
returns (JBFee[] memory)
|
|
883
944
|
{
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
if (startIndex >= numberOfHeldFees) return new JBFee[](0);
|
|
892
|
-
|
|
893
|
-
// If the start index plus the count is greater than the number of fees, set the count to the number of fees
|
|
894
|
-
if (startIndex + count > numberOfHeldFees) count = numberOfHeldFees - startIndex;
|
|
895
|
-
|
|
896
|
-
// Create a new array to hold the fees.
|
|
897
|
-
heldFees = new JBFee[](count);
|
|
898
|
-
|
|
899
|
-
// Copy the fees into the array.
|
|
900
|
-
for (uint256 i; i < count;) {
|
|
901
|
-
heldFees[i] = _heldFeesOf[projectId][token][startIndex + i];
|
|
902
|
-
unchecked {
|
|
903
|
-
++i;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
945
|
+
return JBHeldFees.viewHeldFees({
|
|
946
|
+
heldFeesOf: _heldFeesOf,
|
|
947
|
+
nextHeldFeeIndexOf: _nextHeldFeeIndexOf,
|
|
948
|
+
projectId: projectId,
|
|
949
|
+
token: token,
|
|
950
|
+
count: count
|
|
951
|
+
});
|
|
906
952
|
}
|
|
907
953
|
|
|
908
954
|
/// @notice Simulates a cash out without modifying state — use this to preview how many tokens a holder would
|
|
@@ -1705,6 +1751,15 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1705
1751
|
payer: payer, amount: tokenAmount, projectId: projectId, beneficiary: beneficiary, metadata: metadata
|
|
1706
1752
|
});
|
|
1707
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
|
+
|
|
1708
1763
|
// Only the value retained in the destination balance needs later cashout fee recovery. Non-feeless pay-hook
|
|
1709
1764
|
// forwards pay their source-equivalent fee inline before leaving the project.
|
|
1710
1765
|
if (internalSplitPayProjectId != 0) {
|
|
@@ -1804,7 +1859,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1804
1859
|
emit FeeReverted({
|
|
1805
1860
|
projectId: projectId,
|
|
1806
1861
|
token: token,
|
|
1807
|
-
feeProjectId:
|
|
1862
|
+
feeProjectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID,
|
|
1808
1863
|
amount: amount,
|
|
1809
1864
|
reason: reason,
|
|
1810
1865
|
caller: _msgSender()
|
|
@@ -1832,66 +1887,13 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1832
1887
|
/// as the token's accounting context.
|
|
1833
1888
|
/// @return returnedFees The amount of held fees that were returned, as a fixed point number with the same number of
|
|
1834
1889
|
/// decimals as the token's accounting context.
|
|
1835
|
-
function _returnHeldFees(uint256 projectId, address token, uint256 amount) internal returns (uint256
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
// Use the original array length as the upper bound. Returning held fees never appends new entries.
|
|
1840
|
-
uint256 numberOfHeldFees = _heldFeesOf[projectId][token].length;
|
|
1841
|
-
|
|
1842
|
-
if (startIndex >= numberOfHeldFees) return 0;
|
|
1843
|
-
|
|
1844
|
-
// Track how much of the new balance remains available to match against held fees.
|
|
1845
|
-
uint256 leftoverAmount = amount;
|
|
1846
|
-
|
|
1847
|
-
// Move this forward for each fully returned held fee.
|
|
1848
|
-
uint256 newStartIndex = startIndex;
|
|
1849
|
-
|
|
1850
|
-
for (uint256 i = startIndex; i < numberOfHeldFees;) {
|
|
1851
|
-
if (leftoverAmount == 0) break;
|
|
1852
|
-
|
|
1853
|
-
// Held fees store the original gross amount that paid out before its fee was removed.
|
|
1854
|
-
JBFee memory heldFee = _heldFeesOf[projectId][token][i];
|
|
1855
|
-
|
|
1856
|
-
// Recompute the standard fee associated with the held gross amount.
|
|
1857
|
-
uint256 feeAmount = _feeAmountFrom(heldFee.amount);
|
|
1858
|
-
|
|
1859
|
-
// This is the net amount that originally left the project after the held fee was removed.
|
|
1860
|
-
uint256 amountPaidOut = heldFee.amount - feeAmount;
|
|
1861
|
-
|
|
1862
|
-
if (leftoverAmount >= amountPaidOut) {
|
|
1863
|
-
unchecked {
|
|
1864
|
-
leftoverAmount -= amountPaidOut;
|
|
1865
|
-
returnedFees += feeAmount;
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
// Move the start index forward to the fee after this fully returned one.
|
|
1869
|
-
newStartIndex = i + 1;
|
|
1870
|
-
} else {
|
|
1871
|
-
// Only part of this held fee can be returned. Convert the remaining net replenishment back into
|
|
1872
|
-
// its corresponding gross fee and shrink the stored gross amount.
|
|
1873
|
-
feeAmount = JBFees.standardFeeAmountResultingIn(leftoverAmount);
|
|
1874
|
-
|
|
1875
|
-
unchecked {
|
|
1876
|
-
_heldFeesOf[projectId][token][i].amount -= (leftoverAmount + feeAmount);
|
|
1877
|
-
returnedFees += feeAmount;
|
|
1878
|
-
}
|
|
1879
|
-
leftoverAmount = 0;
|
|
1880
|
-
}
|
|
1881
|
-
unchecked {
|
|
1882
|
-
++i;
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
// Update the next held fee index.
|
|
1887
|
-
if (startIndex != newStartIndex) _nextHeldFeeIndexOf[projectId][token] = newStartIndex;
|
|
1888
|
-
|
|
1889
|
-
emit ReturnHeldFees({
|
|
1890
|
+
function _returnHeldFees(uint256 projectId, address token, uint256 amount) internal returns (uint256) {
|
|
1891
|
+
return JBHeldFees.returnHeldFees({
|
|
1892
|
+
heldFeesOf: _heldFeesOf,
|
|
1893
|
+
nextHeldFeeIndexOf: _nextHeldFeeIndexOf,
|
|
1890
1894
|
projectId: projectId,
|
|
1891
1895
|
token: token,
|
|
1892
1896
|
amount: amount,
|
|
1893
|
-
returnedFees: returnedFees,
|
|
1894
|
-
leftoverAmount: leftoverAmount,
|
|
1895
1897
|
caller: _msgSender()
|
|
1896
1898
|
});
|
|
1897
1899
|
}
|
|
@@ -2012,7 +2014,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2012
2014
|
});
|
|
2013
2015
|
}
|
|
2014
2016
|
|
|
2015
|
-
/// @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`).
|
|
2016
2018
|
/// @param projectId The ID of the project paying the fee.
|
|
2017
2019
|
/// @param token The address of the token that the fee is paid in.
|
|
2018
2020
|
/// @param amount The fee's token amount, as a fixed point number with the same number of decimals as the token's
|
|
@@ -2036,12 +2038,22 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2036
2038
|
|
|
2037
2039
|
if (shouldHoldFees) {
|
|
2038
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;
|
|
2039
2046
|
_heldFeesOf[projectId][token].push(
|
|
2040
2047
|
JBFee({
|
|
2041
|
-
|
|
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),
|
|
2042
2052
|
beneficiary: beneficiary,
|
|
2043
2053
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
2044
|
-
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)
|
|
2045
2057
|
})
|
|
2046
2058
|
);
|
|
2047
2059
|
|
|
@@ -2055,7 +2067,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2055
2067
|
});
|
|
2056
2068
|
} else {
|
|
2057
2069
|
// Resolve the fee project's terminal for this token and process the fee immediately.
|
|
2058
|
-
IJBTerminal feeTerminal =
|
|
2070
|
+
IJBTerminal feeTerminal =
|
|
2071
|
+
_primaryTerminalOf({projectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID, token: token});
|
|
2059
2072
|
|
|
2060
2073
|
_processFee({
|
|
2061
2074
|
projectId: projectId,
|
|
@@ -2295,4 +2308,22 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2295
2308
|
function _feeAmountFrom(uint256 amount) private pure returns (uint256) {
|
|
2296
2309
|
return JBFees.standardFeeAmountFrom(amount);
|
|
2297
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
|
+
}
|
|
2298
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
|
|