@bananapus/core-v6 0.0.58 → 0.0.60
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 +2 -5
- package/script/Deploy.s.sol +121 -0
- package/script/DeployPeriphery.s.sol +346 -0
- 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/CHANGELOG.md +0 -73
- package/foundry.lock +0 -5
- package/sphinx.lock +0 -507
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);
|