@bananapus/distributor-v6 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -7
- package/package.json +1 -1
- package/src/JB721Distributor.sol +348 -32
- package/src/JBDistributor.sol +270 -21
- package/src/JBTokenDistributor.sol +268 -12
- package/src/interfaces/IJBDistributor.sol +51 -6
- package/src/structs/JBClaimContext.sol +11 -0
- package/src/structs/JBRewardRoundData.sol +16 -0
- package/src/structs/JBVestContext.sol +21 -0
package/src/JBDistributor.sol
CHANGED
|
@@ -8,6 +8,7 @@ import {mulDiv} from "@prb/math/src/Common.sol";
|
|
|
8
8
|
|
|
9
9
|
import {IJBDistributor} from "./interfaces/IJBDistributor.sol";
|
|
10
10
|
import {JBVestingMath} from "./libraries/JBVestingMath.sol";
|
|
11
|
+
import {JBRewardRoundData} from "./structs/JBRewardRoundData.sol";
|
|
11
12
|
import {JBTokenSnapshotData} from "./structs/JBTokenSnapshotData.sol";
|
|
12
13
|
import {JBVestingData} from "./structs/JBVestingData.sol";
|
|
13
14
|
|
|
@@ -46,6 +47,15 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
46
47
|
/// @notice Thrown when unexpected native ETH is sent with an ERC-20 operation.
|
|
47
48
|
error JBDistributor_UnexpectedNativeValue(uint256 msgValue, address token);
|
|
48
49
|
|
|
50
|
+
/// @notice Thrown when a value cannot fit in a uint208 reward-round field.
|
|
51
|
+
error JBDistributor_Uint208Overflow(uint256 value);
|
|
52
|
+
|
|
53
|
+
/// @notice Thrown when a value cannot fit in a uint48 reward-round field.
|
|
54
|
+
error JBDistributor_Uint48Overflow(uint256 value);
|
|
55
|
+
|
|
56
|
+
/// @notice Thrown when fundings in the same reward round use different claim deadlines.
|
|
57
|
+
error JBDistributor_ClaimDeadlineMismatch(uint256 existingDeadline, uint256 newDeadline);
|
|
58
|
+
|
|
49
59
|
//*********************************************************************//
|
|
50
60
|
// ------------------------- public constants ------------------------ //
|
|
51
61
|
//*********************************************************************//
|
|
@@ -53,6 +63,9 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
53
63
|
/// @notice The number of shares that represent 100%.
|
|
54
64
|
uint256 public constant MAX_SHARE = 100_000;
|
|
55
65
|
|
|
66
|
+
/// @notice Asset-agnostic burn sink for expired rewards.
|
|
67
|
+
address public constant BURN_ADDRESS = address(0x000000000000000000000000000000000000dEaD);
|
|
68
|
+
|
|
56
69
|
//*********************************************************************//
|
|
57
70
|
// ---------------- public immutable stored properties --------------- //
|
|
58
71
|
//*********************************************************************//
|
|
@@ -80,6 +93,12 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
80
93
|
/// @dev Set to `block.number - 1` on first interaction in a round, so that `IVotes.getPastVotes` works.
|
|
81
94
|
mapping(uint256 round => uint256) public override roundSnapshotBlock;
|
|
82
95
|
|
|
96
|
+
/// @notice Reward data assigned to each funding round.
|
|
97
|
+
/// @custom:param hook The stake source whose stakers receive rewards.
|
|
98
|
+
/// @custom:param token The reward token.
|
|
99
|
+
/// @custom:param round The reward round.
|
|
100
|
+
mapping(address hook => mapping(IERC20 token => mapping(uint256 round => JBRewardRoundData))) public rewardRoundOf;
|
|
101
|
+
|
|
83
102
|
/// @notice The amount of a token that is currently vesting for a hook's stakers.
|
|
84
103
|
/// @custom:param hook The hook whose stakers are vesting.
|
|
85
104
|
/// @custom:param token The address of the token that is vesting.
|
|
@@ -151,7 +170,15 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
151
170
|
/// @param hook The hook (IVotes token or 721 hook) whose stakers are vesting.
|
|
152
171
|
/// @param tokenIds The staker token IDs to claim rewards for.
|
|
153
172
|
/// @param tokens The reward tokens to begin vesting.
|
|
154
|
-
function beginVesting(
|
|
173
|
+
function beginVesting(
|
|
174
|
+
address hook,
|
|
175
|
+
uint256[] calldata tokenIds,
|
|
176
|
+
IERC20[] calldata tokens
|
|
177
|
+
)
|
|
178
|
+
external
|
|
179
|
+
virtual
|
|
180
|
+
override
|
|
181
|
+
{
|
|
155
182
|
// Reward accounting cannot change while an ERC-20 `transferFrom` is in progress. A callback-capable reward
|
|
156
183
|
// token could otherwise snapshot, vest, or collect against balances between `balanceBefore` and
|
|
157
184
|
// `balanceAfter`, distorting the delta credited to the funder.
|
|
@@ -211,18 +238,58 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
211
238
|
/// @param hook The hook to fund (determines which staker pool receives the tokens).
|
|
212
239
|
/// @param token The token to fund with.
|
|
213
240
|
/// @param amount The amount to fund (ignored for native ETH — `msg.value` is used instead).
|
|
214
|
-
function fund(address hook, IERC20 token, uint256 amount) external payable override {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
241
|
+
function fund(address hook, IERC20 token, uint256 amount) external payable virtual override {
|
|
242
|
+
_fund({hook: hook, token: token, amount: amount, claimDuration: 0});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// @notice Fund the distributor for a specific hook with expiring rewards.
|
|
246
|
+
/// @dev The claim window starts when the funded round first becomes claimable.
|
|
247
|
+
/// @param hook The hook to fund.
|
|
248
|
+
/// @param token The token to fund with.
|
|
249
|
+
/// @param amount The amount to fund.
|
|
250
|
+
/// @param claimDuration The number of seconds claimants have after the round becomes claimable.
|
|
251
|
+
function fundWithClaimDuration(
|
|
252
|
+
address hook,
|
|
253
|
+
IERC20 token,
|
|
254
|
+
uint256 amount,
|
|
255
|
+
uint48 claimDuration
|
|
256
|
+
)
|
|
257
|
+
external
|
|
258
|
+
payable
|
|
259
|
+
virtual
|
|
260
|
+
override
|
|
261
|
+
{
|
|
262
|
+
_fund({hook: hook, token: token, amount: amount, claimDuration: claimDuration});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// @notice Burn unclaimed rewards from expired reward rounds.
|
|
266
|
+
/// @param hook The hook whose expired rewards should be burned.
|
|
267
|
+
/// @param token The reward token to burn.
|
|
268
|
+
/// @param rounds The reward rounds to burn.
|
|
269
|
+
/// @return amount The total amount burned.
|
|
270
|
+
function burnExpiredRewards(
|
|
271
|
+
address hook,
|
|
272
|
+
IERC20 token,
|
|
273
|
+
uint256[] calldata rounds
|
|
274
|
+
)
|
|
275
|
+
external
|
|
276
|
+
virtual
|
|
277
|
+
override
|
|
278
|
+
returns (uint256 amount)
|
|
279
|
+
{
|
|
280
|
+
// Do not let reward-token callbacks burn inventory during an inbound balance-delta measurement.
|
|
281
|
+
_requireNotAcceptingToken();
|
|
282
|
+
|
|
283
|
+
// Process every requested round independently so callers can batch keeper work.
|
|
284
|
+
for (uint256 i; i < rounds.length;) {
|
|
285
|
+
// Add this round's expired remainder to the batch total.
|
|
286
|
+
amount += _burnExpiredRewardRound({hook: hook, token: token, round: rounds[i]});
|
|
287
|
+
|
|
288
|
+
unchecked {
|
|
289
|
+
// Safe because the loop is bounded by calldata length.
|
|
290
|
+
++i;
|
|
220
291
|
}
|
|
221
|
-
// Use balance delta to handle fee-on-transfer tokens correctly.
|
|
222
|
-
amount = _acceptErc20FundsFrom({token: token, from: msg.sender, amount: amount});
|
|
223
292
|
}
|
|
224
|
-
_balanceOf[hook][token] += amount;
|
|
225
|
-
_accountedBalanceOf[token] += amount;
|
|
226
293
|
}
|
|
227
294
|
|
|
228
295
|
/// @notice Record the snapshot block for the current round (and eagerly for the next round). Callable by anyone —
|
|
@@ -416,6 +483,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
416
483
|
address beneficiary
|
|
417
484
|
)
|
|
418
485
|
public
|
|
486
|
+
virtual
|
|
419
487
|
override
|
|
420
488
|
{
|
|
421
489
|
// Collections transfer reward tokens out. If this runs inside the same reward token's inbound transfer, the
|
|
@@ -515,18 +583,168 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
515
583
|
_acceptingToken = address(0);
|
|
516
584
|
}
|
|
517
585
|
|
|
586
|
+
/// @notice Accept funds and assign them to this round's reward ledger.
|
|
587
|
+
/// @param hook The stake source whose stakers receive the rewards.
|
|
588
|
+
/// @param token The reward token being funded.
|
|
589
|
+
/// @param amount The nominal amount to fund.
|
|
590
|
+
/// @param claimDuration The number of seconds claimants have once the round becomes claimable.
|
|
591
|
+
function _fund(address hook, IERC20 token, uint256 amount, uint48 claimDuration) internal {
|
|
592
|
+
// Native funding is measured by msg.value, not the caller-provided amount.
|
|
593
|
+
if (address(token) == JBConstants.NATIVE_TOKEN) {
|
|
594
|
+
amount = msg.value;
|
|
595
|
+
} else {
|
|
596
|
+
// ERC-20 funding must not carry native ETH.
|
|
597
|
+
if (msg.value != 0) {
|
|
598
|
+
revert JBDistributor_UnexpectedNativeValue({msgValue: msg.value, token: address(token)});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ERC-20 funding is measured by balance delta so fee-on-transfer tokens are accounted correctly.
|
|
602
|
+
amount = _acceptErc20FundsFrom({token: token, from: msg.sender, amount: amount});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Store the accepted amount in this round's historical reward ledger.
|
|
606
|
+
_recordRewardFunding({hook: hook, token: token, amount: amount, claimDuration: claimDuration});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/// @notice Record accepted funding as the current round's reward pot.
|
|
610
|
+
/// @param hook The stake source whose stakers receive the rewards.
|
|
611
|
+
/// @param token The reward token.
|
|
612
|
+
/// @param amount The accepted funding amount.
|
|
613
|
+
/// @param claimDuration The number of seconds claimants have once the round becomes claimable.
|
|
614
|
+
function _recordRewardFunding(address hook, IERC20 token, uint256 amount, uint48 claimDuration) internal {
|
|
615
|
+
// Zero-value transfers do not create reward rounds or alter tracked balances.
|
|
616
|
+
if (amount == 0) return;
|
|
617
|
+
|
|
618
|
+
// Funding belongs to the round in progress when the distributor receives the rewards.
|
|
619
|
+
uint256 round = currentRound();
|
|
620
|
+
|
|
621
|
+
// Load the current round's ledger entry for this hook and reward token.
|
|
622
|
+
JBRewardRoundData storage rewardRound = rewardRoundOf[hook][token][round];
|
|
623
|
+
|
|
624
|
+
// A zero deadline means no expiration; otherwise the clock starts once this round becomes claimable.
|
|
625
|
+
uint48 claimDeadline = _claimDeadlineFor({round: round, claimDuration: claimDuration});
|
|
626
|
+
|
|
627
|
+
// First funding in a round locks that round's snapshot block and total stake for all later claims.
|
|
628
|
+
if (rewardRound.amount == 0) {
|
|
629
|
+
// Record the exact historical block used for all stake lookups in this round.
|
|
630
|
+
uint256 snapshotBlock = _ensureSnapshotBlockFor(round);
|
|
631
|
+
|
|
632
|
+
// Store the snapshot block in the packed uint48 field.
|
|
633
|
+
rewardRound.snapshotBlock = _toUint48(snapshotBlock);
|
|
634
|
+
|
|
635
|
+
// Store the packed claim deadline chosen by the rewarder.
|
|
636
|
+
rewardRound.claimDeadline = claimDeadline;
|
|
637
|
+
|
|
638
|
+
// Store the packed total stake that shares this round's reward pot.
|
|
639
|
+
rewardRound.totalStake = _toUint208(_totalStake({hook: hook, blockNumber: snapshotBlock}));
|
|
640
|
+
} else if (rewardRound.claimDeadline != claimDeadline) {
|
|
641
|
+
// All fundings merged into the same round must have one deadline for deterministic expiry.
|
|
642
|
+
revert JBDistributor_ClaimDeadlineMismatch({
|
|
643
|
+
existingDeadline: rewardRound.claimDeadline, newDeadline: claimDeadline
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Multiple fundings in the same round share the same snapshot and accumulate into one reward pot.
|
|
648
|
+
rewardRound.amount = _toUint208(uint256(rewardRound.amount) + amount);
|
|
649
|
+
|
|
650
|
+
// Keep the base distributor's balance accounting in sync for collection and conservation checks.
|
|
651
|
+
_balanceOf[hook][token] += amount;
|
|
652
|
+
_accountedBalanceOf[token] += amount;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/// @notice Burn one expired reward round's unclaimed inventory.
|
|
656
|
+
/// @param hook The hook whose expired rewards should be burned.
|
|
657
|
+
/// @param token The reward token to burn.
|
|
658
|
+
/// @param round The reward round to burn.
|
|
659
|
+
/// @return burnAmount The amount burned.
|
|
660
|
+
function _burnExpiredRewardRound(address hook, IERC20 token, uint256 round) internal returns (uint256 burnAmount) {
|
|
661
|
+
// Load the reward round once so expiry, claimed amount, and funded amount stay in sync.
|
|
662
|
+
JBRewardRoundData storage rewardRound = rewardRoundOf[hook][token][round];
|
|
663
|
+
|
|
664
|
+
// Ignore rounds that either never expire or have not reached their deadline yet.
|
|
665
|
+
if (!_rewardRoundExpired(rewardRound)) return 0;
|
|
666
|
+
|
|
667
|
+
// If prior claims have already materialized the whole round, there is nothing left to burn.
|
|
668
|
+
if (rewardRound.claimedAmount >= rewardRound.amount) return 0;
|
|
669
|
+
|
|
670
|
+
// Burn only the unclaimed remainder, preserving amounts that already started vesting.
|
|
671
|
+
burnAmount = uint256(rewardRound.amount) - uint256(rewardRound.claimedAmount);
|
|
672
|
+
|
|
673
|
+
// Mark the whole round settled before transferring to close reentrancy-sensitive accounting.
|
|
674
|
+
rewardRound.claimedAmount = rewardRound.amount;
|
|
675
|
+
|
|
676
|
+
// Remove the expired remainder from distributor inventory and send it to the burn sink.
|
|
677
|
+
_burnRewardTokens({hook: hook, token: token, amount: burnAmount});
|
|
678
|
+
|
|
679
|
+
// Surface the permissionless burn for off-chain accounting.
|
|
680
|
+
emit ExpiredRewardsBurned({hook: hook, round: round, token: token, amount: burnAmount, caller: msg.sender});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/// @notice Burn reward inventory by transferring it to the burn sink.
|
|
684
|
+
/// @param hook The hook whose tracked balance is being burned.
|
|
685
|
+
/// @param token The reward token to burn.
|
|
686
|
+
/// @param amount The amount to burn.
|
|
687
|
+
function _burnRewardTokens(address hook, IERC20 token, uint256 amount) internal {
|
|
688
|
+
// No-op zero burns so callers can batch empty or already-settled rounds safely.
|
|
689
|
+
if (amount == 0) return;
|
|
690
|
+
|
|
691
|
+
// Remove the burned amount from the hook's reward inventory.
|
|
692
|
+
_balanceOf[hook][token] -= amount;
|
|
693
|
+
|
|
694
|
+
// Remove the same amount from the global inventory tracked for this token.
|
|
695
|
+
_accountedBalanceOf[token] -= amount;
|
|
696
|
+
|
|
697
|
+
// Native rewards cannot be ERC-20-burned, so send them to the shared burn sink.
|
|
698
|
+
if (address(token) == JBConstants.NATIVE_TOKEN) {
|
|
699
|
+
// Forward the exact expired native amount to the burn sink.
|
|
700
|
+
(bool success,) = BURN_ADDRESS.call{value: amount}("");
|
|
701
|
+
|
|
702
|
+
// Revert if the native sink transfer fails, preserving accounting by reverting the whole burn.
|
|
703
|
+
if (!success) revert JBDistributor_NativeTransferFailed({beneficiary: BURN_ADDRESS, amount: amount});
|
|
704
|
+
} else {
|
|
705
|
+
// ERC-20 rewards are removed from usable inventory by sending them to the same burn sink.
|
|
706
|
+
token.safeTransfer({to: BURN_ADDRESS, value: amount});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/// @notice Cast a reward-round value to uint208.
|
|
711
|
+
/// @param value The value to cast.
|
|
712
|
+
/// @return castValue The cast value.
|
|
713
|
+
function _toUint208(uint256 value) internal pure returns (uint208 castValue) {
|
|
714
|
+
if (value > type(uint208).max) revert JBDistributor_Uint208Overflow({value: value});
|
|
715
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
716
|
+
castValue = uint208(value);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/// @notice Cast a reward-round value to uint48.
|
|
720
|
+
/// @param value The value to cast.
|
|
721
|
+
/// @return castValue The cast value.
|
|
722
|
+
function _toUint48(uint256 value) internal pure returns (uint48 castValue) {
|
|
723
|
+
if (value > type(uint48).max) revert JBDistributor_Uint48Overflow({value: value});
|
|
724
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
725
|
+
castValue = uint48(value);
|
|
726
|
+
}
|
|
727
|
+
|
|
518
728
|
/// @notice Ensures that a snapshot block is recorded for the given round.
|
|
519
729
|
/// @dev Uses `block.number - 1` because `IVotes.getPastVotes` requires a strictly past block.
|
|
520
730
|
/// @param round The round to ensure a snapshot block for.
|
|
521
731
|
function _ensureSnapshotBlock(uint256 round) internal {
|
|
522
|
-
|
|
523
|
-
roundSnapshotBlock[round] = block.number - 1;
|
|
524
|
-
emit RoundSnapshotRecorded({round: round, snapshotBlock: block.number - 1});
|
|
525
|
-
}
|
|
732
|
+
_ensureSnapshotBlockFor(round);
|
|
526
733
|
// Eagerly lock the next round's snapshot to prevent first-caller manipulation.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
734
|
+
_ensureSnapshotBlockFor(round + 1);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/// @notice Ensures that a snapshot block is recorded for exactly the given round.
|
|
738
|
+
/// @dev Token-distributor funding uses this to assign rewards to the funding round without also freezing the next
|
|
739
|
+
/// round earlier than necessary.
|
|
740
|
+
/// @param round The round to ensure a snapshot block for.
|
|
741
|
+
/// @return snapshotBlock The snapshot block recorded for the round.
|
|
742
|
+
function _ensureSnapshotBlockFor(uint256 round) internal returns (uint256 snapshotBlock) {
|
|
743
|
+
snapshotBlock = roundSnapshotBlock[round];
|
|
744
|
+
if (snapshotBlock == 0) {
|
|
745
|
+
snapshotBlock = block.number - 1;
|
|
746
|
+
roundSnapshotBlock[round] = snapshotBlock;
|
|
747
|
+
emit RoundSnapshotRecorded({round: round, snapshotBlock: snapshotBlock, caller: msg.sender});
|
|
530
748
|
}
|
|
531
749
|
}
|
|
532
750
|
|
|
@@ -554,10 +772,39 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
554
772
|
_snapshotInitializedFor[hook][token][round] = true;
|
|
555
773
|
|
|
556
774
|
emit SnapshotCreated({
|
|
557
|
-
hook: hook,
|
|
775
|
+
hook: hook,
|
|
776
|
+
round: round,
|
|
777
|
+
token: token,
|
|
778
|
+
balance: snapshot.balance,
|
|
779
|
+
vestingAmount: snapshot.vestingAmount,
|
|
780
|
+
caller: msg.sender
|
|
558
781
|
});
|
|
559
782
|
}
|
|
560
783
|
|
|
784
|
+
/// @notice The deadline for a reward round with the given claim duration.
|
|
785
|
+
/// @param round The reward round.
|
|
786
|
+
/// @param claimDuration The claim duration once the round becomes claimable.
|
|
787
|
+
/// @return claimDeadline The deadline timestamp. Zero means no expiration.
|
|
788
|
+
function _claimDeadlineFor(uint256 round, uint48 claimDuration) internal view returns (uint48 claimDeadline) {
|
|
789
|
+
// Zero duration keeps the round non-expiring and backward compatible with existing fund paths.
|
|
790
|
+
if (claimDuration == 0) return 0;
|
|
791
|
+
|
|
792
|
+
// Start the window at the next round boundary, when the funded round first becomes claimable.
|
|
793
|
+
claimDeadline = _toUint48(roundStartTimestamp(round + 1) + claimDuration);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/// @notice Whether a reward round has passed its claim deadline.
|
|
797
|
+
/// @param rewardRound The reward round data.
|
|
798
|
+
/// @return expired True if unclaimed rewards can be burned.
|
|
799
|
+
function _rewardRoundExpired(JBRewardRoundData storage rewardRound) internal view returns (bool expired) {
|
|
800
|
+
// Copy the packed deadline into memory so the zero check and timestamp compare use the same value.
|
|
801
|
+
uint48 claimDeadline = rewardRound.claimDeadline;
|
|
802
|
+
|
|
803
|
+
// A zero deadline never expires; non-zero deadlines expire at or after the configured timestamp.
|
|
804
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
805
|
+
expired = claimDeadline != 0 && block.timestamp >= claimDeadline;
|
|
806
|
+
}
|
|
807
|
+
|
|
561
808
|
/// @notice Unlocks rewards for the given token IDs and tokens, either for collection or forfeiture.
|
|
562
809
|
/// @param hook The hook the tokens belong to.
|
|
563
810
|
/// @param tokenIds The IDs of the tokens to unlock rewards for.
|
|
@@ -675,7 +922,8 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
675
922
|
tokenId: tokenId,
|
|
676
923
|
token: token,
|
|
677
924
|
amount: claimAmount,
|
|
678
|
-
vestingReleaseRound: vesting.releaseRound
|
|
925
|
+
vestingReleaseRound: vesting.releaseRound,
|
|
926
|
+
caller: msg.sender
|
|
679
927
|
});
|
|
680
928
|
}
|
|
681
929
|
|
|
@@ -766,7 +1014,8 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
766
1014
|
tokenId: tokenId,
|
|
767
1015
|
token: token,
|
|
768
1016
|
amount: tokenAmount,
|
|
769
|
-
vestingReleaseRound: vestingReleaseRound
|
|
1017
|
+
vestingReleaseRound: vestingReleaseRound,
|
|
1018
|
+
caller: msg.sender
|
|
770
1019
|
});
|
|
771
1020
|
|
|
772
1021
|
unchecked {
|