diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 458893308..d45b0950a 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -108,6 +108,48 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I address indexed newRewardsEligibilityOracle ); + /** + * @notice Emitted when the eligibility reclaim address is set + * @param oldEligibilityReclaimAddress Previous eligibility reclaim address + * @param newEligibilityReclaimAddress New eligibility reclaim address + */ + event EligibilityReclaimAddressSet( + address indexed oldEligibilityReclaimAddress, + address indexed newEligibilityReclaimAddress + ); + + /** + * @notice Emitted when the subgraph reclaim address is set + * @param oldSubgraphReclaimAddress Previous subgraph reclaim address + * @param newSubgraphReclaimAddress New subgraph reclaim address + */ + event SubgraphReclaimAddressSet( + address indexed oldSubgraphReclaimAddress, + address indexed newSubgraphReclaimAddress + ); + + /** + * @notice Emitted when denied rewards are reclaimed due to eligibility + * @param indexer Address of the indexer whose rewards were denied + * @param allocationID Address of the allocation + * @param amount Amount of rewards reclaimed + */ + event RewardsReclaimedDueToEligibility(address indexed indexer, address indexed allocationID, uint256 amount); + + /** + * @notice Emitted when denied rewards are reclaimed due to subgraph denylist + * @param indexer Address of the indexer whose rewards were denied + * @param allocationID Address of the allocation + * @param subgraphDeploymentID Subgraph deployment ID that was denied + * @param amount Amount of rewards reclaimed + */ + event RewardsReclaimedDueToSubgraphDenylist( + address indexed indexer, + address indexed allocationID, + bytes32 indexed subgraphDeploymentID, + uint256 amount + ); + // -- Modifiers -- /** @@ -264,6 +306,30 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I } } + /** + * @inheritdoc IRewardsManager + * @dev Set to zero address to disable eligibility reclaim functionality + */ + function setIndexerEligibilityReclaimAddress(address newEligibilityReclaimAddress) external override onlyGovernor { + if (indexerEligibilityReclaimAddress != newEligibilityReclaimAddress) { + address oldEligibilityReclaimAddress = indexerEligibilityReclaimAddress; + indexerEligibilityReclaimAddress = newEligibilityReclaimAddress; + emit EligibilityReclaimAddressSet(oldEligibilityReclaimAddress, newEligibilityReclaimAddress); + } + } + + /** + * @inheritdoc IRewardsManager + * @dev Set to zero address to disable subgraph reclaim functionality + */ + function setSubgraphDeniedReclaimAddress(address newSubgraphReclaimAddress) external override onlyGovernor { + if (subgraphDeniedReclaimAddress != newSubgraphReclaimAddress) { + address oldSubgraphReclaimAddress = subgraphDeniedReclaimAddress; + subgraphDeniedReclaimAddress = newSubgraphReclaimAddress; + emit SubgraphReclaimAddressSet(oldSubgraphReclaimAddress, newSubgraphReclaimAddress); + } + } + // -- Denylist -- /** @@ -494,6 +560,60 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I return newAccrued.mul(_tokens).div(FIXED_POINT_SCALING_FACTOR); } + /** + * @notice Checks for and handles denial and reclaim of rewards due to subgraph deny list + * @dev If denied, emits RewardsDenied event and mints to reclaim address if configured + * @param indexer Address of the indexer + * @param allocationID Address of the allocation + * @param subgraphDeploymentID Subgraph deployment ID + * @param rewards Amount of rewards that would be distributed + * @return True if rewards are denied, false otherwise + */ + function _rewardsDeniedDueToSubgraphDenyList( + address indexer, + address allocationID, + bytes32 subgraphDeploymentID, + uint256 rewards + ) private returns (bool) { + if (isDenied(subgraphDeploymentID)) { + emit RewardsDenied(indexer, allocationID); + + // If a reclaim address is set, mint the denied rewards there + if (0 < rewards && subgraphDeniedReclaimAddress != address(0)) { + graphToken().mint(subgraphDeniedReclaimAddress, rewards); + emit RewardsReclaimedDueToSubgraphDenylist(indexer, allocationID, subgraphDeploymentID, rewards); + } + return true; + } + return false; + } + + /** + * @notice Checks for and handles denial and reclaim of rewards due to indexer eligibility + * @dev If denied, emits RewardsDeniedDueToEligibility event and mints to reclaim address if configured + * @param indexer Address of the indexer + * @param allocationID Address of the allocation + * @param rewards Amount of rewards that would be distributed + * @return True if rewards are denied, false otherwise + */ + function _rewardsDeniedDueToIndexerEligibility( + address indexer, + address allocationID, + uint256 rewards + ) private returns (bool) { + if (address(rewardsEligibilityOracle) != address(0) && !rewardsEligibilityOracle.isEligible(indexer)) { + emit RewardsDeniedDueToEligibility(indexer, allocationID, rewards); + + // If a reclaim address is set, mint the denied rewards there + if (0 < rewards && indexerEligibilityReclaimAddress != address(0)) { + graphToken().mint(indexerEligibilityReclaimAddress, rewards); + emit RewardsReclaimedDueToEligibility(indexer, allocationID, rewards); + } + return true; + } + return false; + } + /** * @inheritdoc IRewardsManager * @dev This function can only be called by an authorized rewards issuer which are @@ -518,31 +638,24 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I uint256 updatedAccRewardsPerAllocatedToken = onSubgraphAllocationUpdate(subgraphDeploymentID); - // Do not do rewards on denied subgraph deployments ID - if (isDenied(subgraphDeploymentID)) { - emit RewardsDenied(indexer, _allocationID); - return 0; - } - uint256 rewards = 0; if (isActive) { // Calculate rewards accrued by this allocation rewards = accRewardsPending.add( _calcRewards(tokens, accRewardsPerAllocatedToken, updatedAccRewardsPerAllocatedToken) ); + } - // Do not reward if indexer is not eligible based on rewards eligibility - if (address(rewardsEligibilityOracle) != address(0) && !rewardsEligibilityOracle.isEligible(indexer)) { - emit RewardsDeniedDueToEligibility(indexer, _allocationID, rewards); - return 0; - } + if (_rewardsDeniedDueToSubgraphDenyList(indexer, _allocationID, subgraphDeploymentID, rewards)) return 0; - if (rewards > 0) { - // Mint directly to rewards issuer for the reward amount - // The rewards issuer contract will do bookkeeping of the reward and - // assign in proportion to each stakeholder incentive - graphToken().mint(rewardsIssuer, rewards); - } + if (_rewardsDeniedDueToIndexerEligibility(indexer, _allocationID, rewards)) return 0; + + // Mint rewards to the rewards issuer + if (rewards > 0) { + // Mint directly to rewards issuer for the reward amount + // The rewards issuer contract will do bookkeeping of the reward and + // assign in proportion to each stakeholder incentive + graphToken().mint(rewardsIssuer, rewards); } emit HorizonRewardsAssigned(indexer, _allocationID, rewards); diff --git a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol index 63897f431..f32729547 100644 --- a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol +++ b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol @@ -83,11 +83,15 @@ contract RewardsManagerV5Storage is RewardsManagerV4Storage { * @title RewardsManagerV6Storage * @author Edge & Node * @notice Storage layout for RewardsManager V6 - * Includes support for Rewards Eligibility Oracle and Issuance Allocator. + * Includes support for Rewards Eligibility Oracle, Issuance Allocator, and Reclaim Addresses. */ contract RewardsManagerV6Storage is RewardsManagerV5Storage { /// @notice Address of the rewards eligibility oracle contract IRewardsEligibility public rewardsEligibilityOracle; /// @notice Address of the issuance allocator IIssuanceAllocationDistribution public issuanceAllocator; + /// @notice Address to receive tokens denied due to indexer eligibility checks + address public indexerEligibilityReclaimAddress; + /// @notice Address to receive tokens denied due to subgraph denylist + address public subgraphDeniedReclaimAddress; } diff --git a/packages/contracts/test/tests/unit/rewards/rewards-interface.test.ts b/packages/contracts/test/tests/unit/rewards/rewards-interface.test.ts index 3a9b7c23b..d7db40458 100644 --- a/packages/contracts/test/tests/unit/rewards/rewards-interface.test.ts +++ b/packages/contracts/test/tests/unit/rewards/rewards-interface.test.ts @@ -57,7 +57,7 @@ describe('RewardsManager interfaces', () => { }) it('IRewardsManager should have stable interface ID', () => { - expect(IRewardsManager__factory.interfaceId).to.equal('0xa31d8306') + expect(IRewardsManager__factory.interfaceId).to.equal('0x731e44f0') }) }) diff --git a/packages/contracts/test/tests/unit/rewards/rewards-reclaim.test.ts b/packages/contracts/test/tests/unit/rewards/rewards-reclaim.test.ts new file mode 100644 index 000000000..bfbe7f90e --- /dev/null +++ b/packages/contracts/test/tests/unit/rewards/rewards-reclaim.test.ts @@ -0,0 +1,299 @@ +import { Curation } from '@graphprotocol/contracts' +import { EpochManager } from '@graphprotocol/contracts' +import { GraphToken } from '@graphprotocol/contracts' +import { IStaking } from '@graphprotocol/contracts' +import { RewardsManager } from '@graphprotocol/contracts' +import { deriveChannelKey, GraphNetworkContracts, helpers, randomHexBytes, toGRT } from '@graphprotocol/sdk' +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { constants } from 'ethers' +import hre from 'hardhat' + +import { NetworkFixture } from '../lib/fixtures' + +const { HashZero } = constants + +describe('Rewards - Reclaim Addresses', () => { + const graph = hre.graph() + let curator1: SignerWithAddress + let governor: SignerWithAddress + let indexer1: SignerWithAddress + let reclaimWallet: SignerWithAddress + + let fixture: NetworkFixture + + let contracts: GraphNetworkContracts + let grt: GraphToken + let curation: Curation + let epochManager: EpochManager + let staking: IStaking + let rewardsManager: RewardsManager + + // Derive channel key for indexer used to sign attestations + const channelKey1 = deriveChannelKey() + + const subgraphDeploymentID1 = randomHexBytes() + + const allocationID1 = channelKey1.address + + const metadata = HashZero + + const ISSUANCE_PER_BLOCK = toGRT('200') // 200 GRT every block + + async function setupIndexerAllocation() { + // Setup + await epochManager.connect(governor).setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + + before(async function () { + const testAccounts = await graph.getTestAccounts() + curator1 = testAccounts[0] + indexer1 = testAccounts[1] + reclaimWallet = testAccounts[2] + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + grt = contracts.GraphToken as GraphToken + curation = contracts.Curation as Curation + epochManager = contracts.EpochManager + staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager + + // 200 GRT per block + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + + // Distribute test funds + for (const wallet of [indexer1, curator1]) { + await grt.connect(governor).mint(wallet.address, toGRT('1000000')) + await grt.connect(wallet).approve(staking.address, toGRT('1000000')) + await grt.connect(wallet).approve(curation.address, toGRT('1000000')) + } + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('setIndexerEligibilityReclaimAddress', function () { + it('should reject if not governor', async function () { + const tx = rewardsManager.connect(indexer1).setIndexerEligibilityReclaimAddress(reclaimWallet.address) + await expect(tx).revertedWith('Only Controller governor') + }) + + it('should set eligibility reclaim address if governor', async function () { + const tx = rewardsManager.connect(governor).setIndexerEligibilityReclaimAddress(reclaimWallet.address) + await expect(tx) + .emit(rewardsManager, 'EligibilityReclaimAddressSet') + .withArgs(constants.AddressZero, reclaimWallet.address) + + expect(await rewardsManager.indexerEligibilityReclaimAddress()).eq(reclaimWallet.address) + }) + + it('should allow setting to zero address', async function () { + await rewardsManager.connect(governor).setIndexerEligibilityReclaimAddress(reclaimWallet.address) + + const tx = rewardsManager.connect(governor).setIndexerEligibilityReclaimAddress(constants.AddressZero) + await expect(tx) + .emit(rewardsManager, 'EligibilityReclaimAddressSet') + .withArgs(reclaimWallet.address, constants.AddressZero) + + expect(await rewardsManager.indexerEligibilityReclaimAddress()).eq(constants.AddressZero) + }) + + it('should not emit event when setting same address', async function () { + await rewardsManager.connect(governor).setIndexerEligibilityReclaimAddress(reclaimWallet.address) + + const tx = rewardsManager.connect(governor).setIndexerEligibilityReclaimAddress(reclaimWallet.address) + await expect(tx).to.not.emit(rewardsManager, 'EligibilityReclaimAddressSet') + }) + }) + + describe('setSubgraphDeniedReclaimAddress', function () { + it('should reject if not governor', async function () { + const tx = rewardsManager.connect(indexer1).setSubgraphDeniedReclaimAddress(reclaimWallet.address) + await expect(tx).revertedWith('Only Controller governor') + }) + + it('should set subgraph reclaim address if governor', async function () { + const tx = rewardsManager.connect(governor).setSubgraphDeniedReclaimAddress(reclaimWallet.address) + await expect(tx) + .emit(rewardsManager, 'SubgraphReclaimAddressSet') + .withArgs(constants.AddressZero, reclaimWallet.address) + + expect(await rewardsManager.subgraphDeniedReclaimAddress()).eq(reclaimWallet.address) + }) + + it('should allow setting to zero address', async function () { + await rewardsManager.connect(governor).setSubgraphDeniedReclaimAddress(reclaimWallet.address) + + const tx = rewardsManager.connect(governor).setSubgraphDeniedReclaimAddress(constants.AddressZero) + await expect(tx) + .emit(rewardsManager, 'SubgraphReclaimAddressSet') + .withArgs(reclaimWallet.address, constants.AddressZero) + + expect(await rewardsManager.subgraphDeniedReclaimAddress()).eq(constants.AddressZero) + }) + + it('should not emit event when setting same address', async function () { + await rewardsManager.connect(governor).setSubgraphDeniedReclaimAddress(reclaimWallet.address) + + const tx = rewardsManager.connect(governor).setSubgraphDeniedReclaimAddress(reclaimWallet.address) + await expect(tx).to.not.emit(rewardsManager, 'SubgraphReclaimAddressSet') + }) + }) + + describe('reclaim denied rewards - subgraph denylist', function () { + it('should mint to reclaim address when subgraph denied and reclaim address set', async function () { + // Setup reclaim address + await rewardsManager.connect(governor).setSubgraphDeniedReclaimAddress(reclaimWallet.address) + + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Calculate expected rewards + const expectedRewards = toGRT('1400') + + // Check reclaim wallet balance before + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Close allocation - should emit both denial and reclaim events + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) + await expect(tx) + .emit(rewardsManager, 'RewardsReclaimedDueToSubgraphDenylist') + .withArgs(indexer1.address, allocationID1, subgraphDeploymentID1, expectedRewards) + + // Check reclaim wallet received the rewards + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter.sub(balanceBefore)).eq(expectedRewards) + }) + + it('should not mint to reclaim address when reclaim address not set', async function () { + // Do NOT set reclaim address (defaults to zero address) + + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Close allocation - should only emit denial event, not reclaim + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimedDueToSubgraphDenylist') + }) + }) + + describe('reclaim denied rewards - eligibility', function () { + it('should mint to reclaim address when eligibility denied and reclaim address set', async function () { + // Setup reclaim address + await rewardsManager.connect(governor).setIndexerEligibilityReclaimAddress(reclaimWallet.address) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setRewardsEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Calculate expected rewards + const expectedRewards = toGRT('1400') + + // Check reclaim wallet balance before + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Close allocation - should emit both denial and reclaim events + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx) + .emit(rewardsManager, 'RewardsDeniedDueToEligibility') + .withArgs(indexer1.address, allocationID1, expectedRewards) + await expect(tx) + .emit(rewardsManager, 'RewardsReclaimedDueToEligibility') + .withArgs(indexer1.address, allocationID1, expectedRewards) + + // Check reclaim wallet received the rewards + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter.sub(balanceBefore)).eq(expectedRewards) + }) + + it('should not mint to reclaim address when reclaim address not set', async function () { + // Do NOT set reclaim address (defaults to zero address) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setRewardsEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + const expectedRewards = toGRT('1400') + + // Close allocation - should only emit denial event, not reclaim + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx) + .emit(rewardsManager, 'RewardsDeniedDueToEligibility') + .withArgs(indexer1.address, allocationID1, expectedRewards) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimedDueToEligibility') + }) + }) +}) diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol index bd8da3508..4737870da 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol @@ -49,6 +49,20 @@ interface IRewardsManager { */ function setRewardsEligibilityOracle(address newRewardsEligibilityOracle) external; + /** + * @notice Set the indexer eligibility reclaim address + * @dev Address to mint tokens that would be denied due to indexer eligibility. Set to zero to disable. + * @param newEligibilityReclaimAddress The address to receive eligibility-denied tokens + */ + function setIndexerEligibilityReclaimAddress(address newEligibilityReclaimAddress) external; + + /** + * @notice Set the subgraph denied reclaim address + * @dev Address to mint tokens that would be denied due to subgraph denylist. Set to zero to disable. + * @param newSubgraphReclaimAddress The address to receive subgraph-denied tokens + */ + function setSubgraphDeniedReclaimAddress(address newSubgraphReclaimAddress) external; + // -- Denylist -- /** diff --git a/packages/interfaces/contracts/issuance/allocate/IIssuanceAllocationAdministration.sol b/packages/interfaces/contracts/issuance/allocate/IIssuanceAllocationAdministration.sol index 23bc7ea05..4865d96d2 100644 --- a/packages/interfaces/contracts/issuance/allocate/IIssuanceAllocationAdministration.sol +++ b/packages/interfaces/contracts/issuance/allocate/IIssuanceAllocationAdministration.sol @@ -72,6 +72,17 @@ interface IIssuanceAllocationAdministration { */ function forceTargetNoChangeNotificationBlock(address target, uint256 blockNumber) external returns (uint256); + /** + * @notice Set the address that receives the default (unallocated) portion of issuance + * @param newAddress The new default allocation address (can be address(0)) + * @return True if successful + * @dev The default allocation automatically receives the portion of issuance not allocated to other targets + * @dev This maintains the invariant that total allocation is always 100% + * @dev Reverts if attempting to set to an address that has a normal (non-default) allocation + * @dev No-op if setting to the same address + */ + function setDefaultAllocationAddress(address newAddress) external returns (bool); + /** * @notice Distribute any pending accumulated issuance to allocator-minting targets. * @return Block number up to which issuance has been distributed diff --git a/packages/issuance/contracts/allocate/IssuanceAllocator.sol b/packages/issuance/contracts/allocate/IssuanceAllocator.sol index e6e9ba62c..b9b0d977d 100644 --- a/packages/issuance/contracts/allocate/IssuanceAllocator.sol +++ b/packages/issuance/contracts/allocate/IssuanceAllocator.sol @@ -25,6 +25,14 @@ import { ERC165Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/int * of the protocol. It calculates issuance for all targets based on their configured proportions * and handles minting for allocator-minting portions. * + * @dev The contract maintains a 100% allocation invariant through a default allocation mechanism: + * - A default allocation target exists at targetAddresses[0] (initialized to address(0)) + * - The default allocation automatically receives any unallocated portion of issuance + * - Total allocation across all targets always equals 100% (MILLION PPM) + * - The default allocation address can be changed via setDefaultAllocationAddress() + * - When the default address is address(0), the unallocated portion is not minted + * - Regular targets cannot be set as the default allocation address + * * @dev The contract supports two types of allocation for each target: * 1. Allocator-minting allocation: The IssuanceAllocator calculates and mints tokens directly to targets * for this portion of their allocation. @@ -77,10 +85,13 @@ contract IssuanceAllocator is /// @param lastAccumulationBlock Last block when pending issuance was accumulated /// @dev Design invariant: lastDistributionBlock <= lastAccumulationBlock /// @param allocationTargets Mapping of target addresses to their allocation data - /// @param targetAddresses Array of all target addresses with non-zero allocation - /// @param totalAllocatorMintingPPM Total allocator-minting allocation (in PPM) across all targets + /// @param targetAddresses Array of all target addresses (including default allocation at index 0) /// @param totalSelfMintingPPM Total self-minting allocation (in PPM) across all targets /// @param pendingAccumulatedAllocatorIssuance Accumulated but not distributed issuance for allocator-minting from lastDistributionBlock to lastAccumulationBlock + /// @dev Design invariant: Total allocation across all targets always equals MILLION (100%) + /// @dev Design invariant: targetAddresses[0] is always the default allocation address + /// @dev Design invariant: 1 <= targetAddresses.length (default allocation always exists) + /// @dev Design invariant: Default allocation (targetAddresses[0]) is automatically adjusted to maintain 100% total /// @custom:storage-location erc7201:graphprotocol.storage.IssuanceAllocator struct IssuanceAllocatorData { uint256 issuancePerBlock; @@ -88,7 +99,6 @@ contract IssuanceAllocator is uint256 lastAccumulationBlock; mapping(address => AllocationTarget) allocationTargets; address[] targetAddresses; - uint256 totalAllocatorMintingPPM; uint256 totalSelfMintingPPM; uint256 pendingAccumulatedAllocatorIssuance; } @@ -122,6 +132,12 @@ contract IssuanceAllocator is /// @notice Thrown when toBlockNumber is out of valid range for accumulation error ToBlockOutOfRange(); + /// @notice Thrown when attempting to set allocation for the default allocation target + error CannotSetAllocationForDefaultTarget(); + + /// @notice Thrown when attempting to set default allocation address to a normally allocated target + error CannotSetDefaultToAllocatedTarget(); + // -- Events -- /// @notice Emitted when issuance is distributed to a target @@ -143,6 +159,11 @@ contract IssuanceAllocator is event IssuancePerBlockUpdated(uint256 oldIssuancePerBlock, uint256 newIssuancePerBlock); // solhint-disable-line gas-indexed-events // Do not need to index issuance per block values + /// @notice Emitted when the default allocation address is updated + /// @param oldAddress The previous default allocation address + /// @param newAddress The new default allocation address + event DefaultAllocationAddressUpdated(address indexed oldAddress, address indexed newAddress); + // -- Constructor -- /** @@ -159,9 +180,17 @@ contract IssuanceAllocator is /** * @notice Initialize the IssuanceAllocator contract * @param _governor Address that will have the GOVERNOR_ROLE + * @dev Initializes with a default allocation at index 0 set to address(0) with 100% allocation */ function initialize(address _governor) external virtual initializer { __BaseUpgradeable_init(_governor); + + IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage(); + + // Initialize default allocation at index 0 with address(0) and 100% allocator-minting + $.targetAddresses.push(address(0)); + $.allocationTargets[address(0)].allocatorMintingPPM = MILLION; + $.allocationTargets[address(0)].selfMintingPPM = 0; } // -- Core Functionality -- @@ -219,6 +248,10 @@ contract IssuanceAllocator is if (0 < newIssuance) { for (uint256 i = 0; i < $.targetAddresses.length; ++i) { address target = $.targetAddresses[i]; + + // Skip minting to zero address (default allocation when not configured) + if (target == address(0)) continue; + AllocationTarget storage targetData = $.allocationTargets[target]; if (0 < targetData.allocatorMintingPPM) { @@ -275,6 +308,9 @@ contract IssuanceAllocator is * @return True if notification was sent or already sent for this block */ function _notifyTarget(address target) private returns (bool) { + // Skip notification for zero address (default allocation when unset) + if (target == address(0)) return true; + IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage(); AllocationTarget storage targetData = $.allocationTargets[target]; @@ -384,6 +420,45 @@ contract IssuanceAllocator is return _setTargetAllocation(target, allocatorMintingPPM, selfMintingPPM, evenIfDistributionPending); } + /** + * @notice Set the address that receives the default (unallocated) portion of issuance + * @param newAddress The new default allocation address (can be address(0)) + * @return True if successful + * @dev The default allocation automatically receives the portion of issuance not allocated to other targets + * @dev This maintains the invariant that total allocation is always 100% + * @dev Reverts if attempting to set to an address that has a normal (non-default) allocation + * @dev No-op if setting to the same address + */ + function setDefaultAllocationAddress(address newAddress) external onlyRole(GOVERNOR_ROLE) returns (bool) { + IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage(); + + address oldAddress = $.targetAddresses[0]; + + // No-op if setting to same address + if (newAddress == oldAddress) return true; + + // Cannot set default allocation to a normally allocated target + // Check if newAddress is in targetAddresses (excluding index 0 which is the default) + // Note: This is O(n) for the number of targets, which could become expensive as targets increase. + // However, other operations (distribution, notifications) already loop through all targets and + // would encounter gas issues first. Recovery mechanisms exist (pause, per-target notification control). + for (uint256 i = 1; i < $.targetAddresses.length; ++i) { + require($.targetAddresses[i] != newAddress, CannotSetDefaultToAllocatedTarget()); + } + + // Notify both old and new addresses of the allocation change + _notifyTarget(oldAddress); + _notifyTarget(newAddress); + + // Update the default allocation address at index 0 + $.targetAddresses[0] = newAddress; + $.allocationTargets[newAddress] = $.allocationTargets[oldAddress]; + delete $.allocationTargets[oldAddress]; + + emit DefaultAllocationAddressUpdated(oldAddress, newAddress); + return true; + } + /** * @notice Internal implementation for setting target allocation * @param target Address of the target to update @@ -404,8 +479,16 @@ contract IssuanceAllocator is _notifyTarget(target); + // Total allocation calculation and check is delayed until after notifications. + // Distributing and notifying unnecessarily is harmless, but we need to prevent + // reentrancy from looping and changing allocations mid-calculation. + // (Would not be likely to be exploitable due to only governor being able to + // make a call to set target allocation, but better to be paranoid.) + // Validate totals and auto-adjust default allocation BEFORE updating target data + // so we can read the old allocation values _validateAndUpdateTotalAllocations(target, allocatorMintingPPM, selfMintingPPM); + // Then update the target's allocation data _updateTargetAllocationData(target, allocatorMintingPPM, selfMintingPPM); emit TargetAllocationUpdated(target, allocatorMintingPPM, selfMintingPPM); @@ -424,9 +507,11 @@ contract IssuanceAllocator is uint256 allocatorMintingPPM, uint256 selfMintingPPM ) private view returns (bool) { + IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage(); + + require(target != $.targetAddresses[0], CannotSetAllocationForDefaultTarget()); require(target != address(0), TargetAddressCannotBeZero()); - IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage(); AllocationTarget storage targetData = $.allocationTargets[target]; if (targetData.allocatorMintingPPM == allocatorMintingPPM && targetData.selfMintingPPM == selfMintingPPM) @@ -467,10 +552,12 @@ contract IssuanceAllocator is } /** - * @notice Updates global allocation totals and validates they don't exceed maximum + * @notice Updates global allocation totals and auto-adjusts default allocation to maintain 100% invariant * @param target Address of the target being updated * @param allocatorMintingPPM New allocator-minting allocation for the target (in PPM) * @param selfMintingPPM New self-minting allocation for the target (in PPM) + * @dev The default allocation (at targetAddresses[0]) is automatically adjusted to ensure total allocation equals MILLION + * @dev This function is called BEFORE the target's allocation data has been updated so we can read old values */ function _validateAndUpdateTotalAllocations( address target, @@ -479,18 +566,18 @@ contract IssuanceAllocator is ) private { IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage(); AllocationTarget storage targetData = $.allocationTargets[target]; + AllocationTarget storage defaultTarget = $.allocationTargets[$.targetAddresses[0]]; - // Total allocation calculation and check is delayed until after notifications. - // Distributing and notifying unnecessarily is harmless, but we need to prevent - // reentrancy looping changing allocations mid-calculation. - // (Would not be likely to be exploitable due to only governor being able to - // make a call to set target allocation, but better to be paranoid.) - $.totalAllocatorMintingPPM = $.totalAllocatorMintingPPM - targetData.allocatorMintingPPM + allocatorMintingPPM; - $.totalSelfMintingPPM = $.totalSelfMintingPPM - targetData.selfMintingPPM + selfMintingPPM; + // Calculation is done here after notifications to prevent reentrancy issues - // Ensure the new total allocation doesn't exceed MILLION as in PPM. + uint256 availablePPM = defaultTarget.allocatorMintingPPM + + targetData.allocatorMintingPPM + + targetData.selfMintingPPM; // solhint-disable-next-line gas-strict-inequalities - require(($.totalAllocatorMintingPPM + $.totalSelfMintingPPM) <= MILLION, InsufficientAllocationAvailable()); + require(allocatorMintingPPM + selfMintingPPM <= availablePPM, InsufficientAllocationAvailable()); + + defaultTarget.allocatorMintingPPM = availablePPM - allocatorMintingPPM - selfMintingPPM; + $.totalSelfMintingPPM = $.totalSelfMintingPPM - targetData.selfMintingPPM + selfMintingPPM; } /** @@ -498,23 +585,24 @@ contract IssuanceAllocator is * @param target Address of the target being updated * @param allocatorMintingPPM New allocator-minting allocation for the target (in PPM) * @param selfMintingPPM New self-minting allocation for the target (in PPM) + * @dev This function is never called for the default allocation (at index 0), which is handled separately */ function _updateTargetAllocationData(address target, uint256 allocatorMintingPPM, uint256 selfMintingPPM) private { IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage(); AllocationTarget storage targetData = $.allocationTargets[target]; // Internal design invariants: - // - targetAddresses contains all targets with non-zero allocation. - // - targetAddresses does not contain targets with zero allocation. - // - targetAddresses does not contain duplicates. - // - allocationTargets mapping contains all targets in targetAddresses with a non-zero allocation. - // - allocationTargets mapping allocations are zero for targets not in targetAddresses. + // - targetAddresses[0] is always the default allocation and is never removed + // - targetAddresses[1..n] contains all non-default targets with explicitly set non-zero allocations + // - targetAddresses does not contain duplicates + // - allocationTargets mapping contains allocation data for all targets in targetAddresses + // - Default allocation is automatically adjusted by _validateAndUpdateTotalAllocations // - Governance actions can create allocationTarget mappings with lastChangeNotifiedBlock set for targets not in targetAddresses. This is valid. // Therefore: - // - Only add a target to the list if it previously had no allocation. - // - Remove a target from the list when setting both allocations to 0. - // - Delete allocationTargets mapping entry when removing a target from targetAddresses. - // - Do not set lastChangeNotifiedBlock in this function. + // - Only add a non-default target to the list if it previously had no allocation + // - Remove a non-default target from the list when setting both allocations to 0 + // - Delete allocationTargets mapping entry when removing a target from targetAddresses + // - Do not set lastChangeNotifiedBlock in this function if (allocatorMintingPPM != 0 || selfMintingPPM != 0) { // Add to list if previously had no allocation if (targetData.allocatorMintingPPM == 0 && targetData.selfMintingPPM == 0) $.targetAddresses.push(target); @@ -586,10 +674,14 @@ contract IssuanceAllocator is if (pendingAmount == 0) return $.lastDistributionBlock; $.pendingAccumulatedAllocatorIssuance = 0; - if ($.totalAllocatorMintingPPM == 0) return $.lastDistributionBlock; + if ($.totalSelfMintingPPM == MILLION) return $.lastDistributionBlock; for (uint256 i = 0; i < $.targetAddresses.length; ++i) { address target = $.targetAddresses[i]; + + // Skip minting to zero address (default allocation when not configured) + if (target == address(0)) continue; + AllocationTarget storage targetData = $.allocationTargets[target]; if (0 < targetData.allocatorMintingPPM) { @@ -727,13 +819,32 @@ contract IssuanceAllocator is /** * @inheritdoc IIssuanceAllocationStatus + * @dev For reporting purposes, if the default allocation target is address(0), its allocation + * @dev is treated as "unallocated" since address(0) cannot receive minting. + * @dev When default is address(0): returns actual allocated amounts (may be less than 100%) + * @dev When default is a real address: returns 100% total allocation + * @dev Note: Internally, the contract always maintains 100% allocation invariant */ function getTotalAllocation() external view override returns (Allocation memory) { IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage(); + + uint256 totalAllocatorMinting = MILLION - $.totalSelfMintingPPM; + uint256 totalAllocation = MILLION; + + // If default is address(0), exclude its allocation from reported totals + // since it doesn't actually receive minting (effectively unallocated) + address defaultAddress = $.targetAddresses[0]; + if (defaultAddress == address(0)) { + AllocationTarget storage defaultTarget = $.allocationTargets[defaultAddress]; + uint256 defaultAllocation = defaultTarget.allocatorMintingPPM; + totalAllocatorMinting = totalAllocatorMinting - defaultAllocation; + totalAllocation = totalAllocation - defaultAllocation; + } + return Allocation({ - totalAllocationPPM: $.totalAllocatorMintingPPM + $.totalSelfMintingPPM, - allocatorMintingPPM: $.totalAllocatorMintingPPM, + totalAllocationPPM: totalAllocation, + allocatorMintingPPM: totalAllocatorMinting, selfMintingPPM: $.totalSelfMintingPPM }); } diff --git a/packages/issuance/test/tests/allocate/DefaultAllocation.test.ts b/packages/issuance/test/tests/allocate/DefaultAllocation.test.ts new file mode 100644 index 000000000..67dc810a2 --- /dev/null +++ b/packages/issuance/test/tests/allocate/DefaultAllocation.test.ts @@ -0,0 +1,554 @@ +import { expect } from 'chai' +import hre from 'hardhat' +const { ethers } = hre + +import { deployTestGraphToken, getTestAccounts } from '../common/fixtures' +import { deployDirectAllocation, deployIssuanceAllocator } from './fixtures' +import { expectCustomError } from './optimizationHelpers' + +describe('IssuanceAllocator - Default Allocation', () => { + let accounts + let graphToken + let issuanceAllocator + let target1 + let target2 + let target3 + let addresses + + const MILLION = 1_000_000n + const issuancePerBlock = ethers.parseEther('100') + + beforeEach(async () => { + accounts = await getTestAccounts() + + // Deploy fresh contracts for each test + graphToken = await deployTestGraphToken() + const graphTokenAddress = await graphToken.getAddress() + + issuanceAllocator = await deployIssuanceAllocator(graphTokenAddress, accounts.governor, issuancePerBlock) + + target1 = await deployDirectAllocation(graphTokenAddress, accounts.governor) + target2 = await deployDirectAllocation(graphTokenAddress, accounts.governor) + target3 = await deployDirectAllocation(graphTokenAddress, accounts.governor) + + addresses = { + issuanceAllocator: await issuanceAllocator.getAddress(), + target1: await target1.getAddress(), + target2: await target2.getAddress(), + target3: await target3.getAddress(), + graphToken: graphTokenAddress, + } + + // Grant minter role to issuanceAllocator + await (graphToken as any).addMinter(addresses.issuanceAllocator) + }) + + describe('Initialization', () => { + it('should initialize with default allocation at index 0', async () => { + const targetCount = await issuanceAllocator.getTargetCount() + expect(targetCount).to.equal(1n) + + const defaultAddress = await issuanceAllocator.getTargetAt(0) + expect(defaultAddress).to.equal(ethers.ZeroAddress) + }) + + it('should initialize with 100% allocation to default target', async () => { + const defaultAddress = await issuanceAllocator.getTargetAt(0) + const allocation = await issuanceAllocator.getTargetAllocation(defaultAddress) + + expect(allocation.totalAllocationPPM).to.equal(MILLION) + expect(allocation.allocatorMintingPPM).to.equal(MILLION) + expect(allocation.selfMintingPPM).to.equal(0n) + }) + + it('should report total allocation as 0% when default is address(0)', async () => { + const totalAllocation = await issuanceAllocator.getTotalAllocation() + + // When default is address(0), it is treated as unallocated for reporting purposes + expect(totalAllocation.totalAllocationPPM).to.equal(0n) + expect(totalAllocation.allocatorMintingPPM).to.equal(0n) + expect(totalAllocation.selfMintingPPM).to.equal(0n) + }) + }) + + describe('100% Allocation Invariant', () => { + it('should auto-adjust default allocation when setting normal target allocation', async () => { + const allocation1PPM = 300_000n // 30% + + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, allocation1PPM) + + // Check target1 has correct allocation + const target1Allocation = await issuanceAllocator.getTargetAllocation(addresses.target1) + expect(target1Allocation.totalAllocationPPM).to.equal(allocation1PPM) + + // Check default allocation was auto-adjusted + const defaultAddress = await issuanceAllocator.getTargetAt(0) + const defaultAllocation = await issuanceAllocator.getTargetAllocation(defaultAddress) + expect(defaultAllocation.totalAllocationPPM).to.equal(MILLION - allocation1PPM) + + // Check reported total (excludes default since it's address(0)) + const totalAllocation = await issuanceAllocator.getTotalAllocation() + expect(totalAllocation.totalAllocationPPM).to.equal(allocation1PPM) + }) + + it('should maintain 100% invariant with multiple targets', async () => { + const allocation1PPM = 200_000n // 20% + const allocation2PPM = 350_000n // 35% + const allocation3PPM = 150_000n // 15% + + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, allocation1PPM) + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target2, allocation2PPM) + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target3, allocation3PPM) + + // Check default allocation is 30% (100% - 20% - 35% - 15%) + const defaultAddress = await issuanceAllocator.getTargetAt(0) + const defaultAllocation = await issuanceAllocator.getTargetAllocation(defaultAddress) + const expectedDefault = MILLION - allocation1PPM - allocation2PPM - allocation3PPM + expect(defaultAllocation.totalAllocationPPM).to.equal(expectedDefault) + + // Check reported total (excludes default since it's address(0)) + const totalAllocation = await issuanceAllocator.getTotalAllocation() + expect(totalAllocation.totalAllocationPPM).to.equal(allocation1PPM + allocation2PPM + allocation3PPM) + }) + + it('should allow 0% default allocation when all allocation is assigned', async () => { + const allocation1PPM = 600_000n // 60% + const allocation2PPM = 400_000n // 40% + + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, allocation1PPM) + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target2, allocation2PPM) + + // Check default allocation is 0% + const defaultAddress = await issuanceAllocator.getTargetAt(0) + const defaultAllocation = await issuanceAllocator.getTargetAllocation(defaultAddress) + expect(defaultAllocation.totalAllocationPPM).to.equal(0n) + + // Check reported total is 100% (default has 0%, so exclusion doesn't matter) + const totalAllocation = await issuanceAllocator.getTotalAllocation() + expect(totalAllocation.totalAllocationPPM).to.equal(MILLION) + }) + + it('should revert if non-default allocations exceed 100%', async () => { + const allocation1PPM = 600_000n // 60% + const allocation2PPM = 500_000n // 50% (total would be 110%) + + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, allocation1PPM) + + await expectCustomError( + issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target2, allocation2PPM), + issuanceAllocator, + 'InsufficientAllocationAvailable', + ) + }) + + it('should adjust default when removing a target allocation', async () => { + // Set up initial allocations + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, 300_000n) + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target2, 200_000n) + + // Default should be 50% + let defaultAddress = await issuanceAllocator.getTargetAt(0) + let defaultAllocation = await issuanceAllocator.getTargetAllocation(defaultAddress) + expect(defaultAllocation.totalAllocationPPM).to.equal(500_000n) + + // Remove target1 allocation + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256,uint256,bool)'](addresses.target1, 0, 0, false) + + // Default should now be 80% + defaultAddress = await issuanceAllocator.getTargetAt(0) + defaultAllocation = await issuanceAllocator.getTargetAllocation(defaultAddress) + expect(defaultAllocation.totalAllocationPPM).to.equal(800_000n) + + // Reported total excludes default (only target2's 20% is reported) + const totalAllocation = await issuanceAllocator.getTotalAllocation() + expect(totalAllocation.totalAllocationPPM).to.equal(200_000n) + }) + + it('should handle self-minting allocations correctly in 100% invariant', async () => { + const allocator1 = 200_000n + const self1 = 100_000n + const allocator2 = 300_000n + const self2 = 50_000n + + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256,uint256)'](addresses.target1, allocator1, self1) + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256,uint256)'](addresses.target2, allocator2, self2) + + // Total non-default: 20% + 10% + 30% + 5% = 65% + // Default should be: 35% + const defaultAddress = await issuanceAllocator.getTargetAt(0) + const defaultAllocation = await issuanceAllocator.getTargetAllocation(defaultAddress) + expect(defaultAllocation.totalAllocationPPM).to.equal(350_000n) + + // Reported total excludes default (only target1+target2's 65% is reported) + const totalAllocation = await issuanceAllocator.getTotalAllocation() + expect(totalAllocation.totalAllocationPPM).to.equal(allocator1 + self1 + allocator2 + self2) + expect(totalAllocation.selfMintingPPM).to.equal(self1 + self2) + }) + }) + + describe('setDefaultAllocationAddress', () => { + it('should allow governor to change default allocation address', async () => { + const newDefaultAddress = addresses.target1 + + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(newDefaultAddress) + + const defaultAddress = await issuanceAllocator.getTargetAt(0) + expect(defaultAddress).to.equal(newDefaultAddress) + }) + + it('should maintain allocation when changing default address', async () => { + // Set a target allocation first + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target2, 400_000n) + + // Default should be 60% + let defaultAddress = await issuanceAllocator.getTargetAt(0) + let defaultAllocation = await issuanceAllocator.getTargetAllocation(defaultAddress) + expect(defaultAllocation.totalAllocationPPM).to.equal(600_000n) + + // Change default address + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(addresses.target1) + + // Check new address has the same allocation + defaultAddress = await issuanceAllocator.getTargetAt(0) + expect(defaultAddress).to.equal(addresses.target1) + defaultAllocation = await issuanceAllocator.getTargetAllocation(addresses.target1) + expect(defaultAllocation.totalAllocationPPM).to.equal(600_000n) + + // Old address should have zero allocation + const oldAllocation = await issuanceAllocator.getTargetAllocation(ethers.ZeroAddress) + expect(oldAllocation.totalAllocationPPM).to.equal(0n) + }) + + it('should emit DefaultAllocationAddressUpdated event', async () => { + const newDefaultAddress = addresses.target1 + + await expect(issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(newDefaultAddress)) + .to.emit(issuanceAllocator, 'DefaultAllocationAddressUpdated') + .withArgs(ethers.ZeroAddress, newDefaultAddress) + }) + + it('should be no-op when setting to same address', async () => { + const currentAddress = await issuanceAllocator.getTargetAt(0) + + const tx = await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(currentAddress) + const receipt = await tx.wait() + + // Should not emit event when no-op + const events = receipt!.logs.filter((log: any) => { + try { + return issuanceAllocator.interface.parseLog(log)?.name === 'DefaultAllocationAddressUpdated' + } catch { + return false + } + }) + expect(events.length).to.equal(0) + }) + + it('should revert when non-governor tries to change default address', async () => { + await expect( + issuanceAllocator.connect(accounts.user).setDefaultAllocationAddress(addresses.target1), + ).to.be.revertedWithCustomError(issuanceAllocator, 'AccessControlUnauthorizedAccount') + }) + + it('should revert when trying to set default to a normally allocated target', async () => { + // Set target1 as a normal allocation + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, 300_000n) + + // Try to set target1 as default should fail + await expectCustomError( + issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(addresses.target1), + issuanceAllocator, + 'CannotSetDefaultToAllocatedTarget', + ) + }) + + it('should allow changing back to zero address', async () => { + // Change to target1 + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(addresses.target1) + + // Change back to zero address + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(ethers.ZeroAddress) + + const defaultAddress = await issuanceAllocator.getTargetAt(0) + expect(defaultAddress).to.equal(ethers.ZeroAddress) + }) + }) + + describe('setTargetAllocation restrictions', () => { + it('should revert when trying to set allocation for default target', async () => { + const defaultAddress = await issuanceAllocator.getTargetAt(0) + + await expectCustomError( + issuanceAllocator.connect(accounts.governor)['setTargetAllocation(address,uint256)'](defaultAddress, 500_000n), + issuanceAllocator, + 'CannotSetAllocationForDefaultTarget', + ) + }) + + it('should revert when trying to set allocation for changed default target', async () => { + // Change default to target1 + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(addresses.target1) + + // Should not be able to set allocation for target1 now + await expectCustomError( + issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, 500_000n), + issuanceAllocator, + 'CannotSetAllocationForDefaultTarget', + ) + }) + + it('should allow setting allocation for previous default address after it changes', async () => { + // Change default to target1 + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(addresses.target1) + + // Should now be able to set allocation for old default (zero address would fail for other reasons, use target2) + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(addresses.target2) + + // Now target1 is no longer default, should be able to allocate to it + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, 300_000n) + + const allocation = await issuanceAllocator.getTargetAllocation(addresses.target1) + expect(allocation.totalAllocationPPM).to.equal(300_000n) + }) + + it('should revert when trying to set allocation for address(0) when default is not address(0)', async () => { + // Change default to target1 + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(addresses.target1) + + // Try to set allocation for address(0) directly should fail + await expectCustomError( + issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](ethers.ZeroAddress, 300_000n), + issuanceAllocator, + 'TargetAddressCannotBeZero', + ) + }) + }) + + describe('Distribution with default allocation', () => { + it('should not mint to zero address when default is unset', async () => { + // Set a normal target allocation (this is block 1) + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, 400_000n) + + // Distribute (this is block 2, so we distribute for block 1->2 = 1 block since last distribution) + await issuanceAllocator.distributeIssuance() + + // Target1 should receive 40% of issuance for the block between setTargetAllocation and distributeIssuance + const target1Balance = await graphToken.balanceOf(addresses.target1) + const expectedTarget1 = (issuancePerBlock * 400_000n) / MILLION + expect(target1Balance).to.equal(expectedTarget1) + + // Zero address should have nothing (cannot be minted to) + const zeroBalance = await graphToken.balanceOf(ethers.ZeroAddress) + expect(zeroBalance).to.equal(0n) + + // The 60% for default (zero address) is effectively burned/not minted + }) + + it('should mint to default address when it is set', async () => { + // Distribute any pending issuance first to start fresh + await issuanceAllocator.distributeIssuance() + + // Change default to target3 + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(addresses.target3) + + // Set target1 allocation using evenIfDistributionPending to avoid premature distribution + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256,uint256,bool)'](addresses.target1, 300_000n, 0n, true) + + // Distribute once (exactly 1 block with the new allocations) + await issuanceAllocator.distributeIssuance() + + // Target1 should receive 30% for 1 block (from last distributeIssuance call) + const target1Balance = await graphToken.balanceOf(addresses.target1) + const expectedTarget1 = (issuancePerBlock * 300_000n) / MILLION + expect(target1Balance).to.equal(expectedTarget1) + + // Target3 (default) should receive: + // - 100% for 2 blocks (from initial distributeIssuance to setTargetAllocation) + // - 70% for 1 block (from setTargetAllocation to final distributeIssuance) + const target3Balance = await graphToken.balanceOf(addresses.target3) + const expectedTarget3 = issuancePerBlock * 2n + (issuancePerBlock * 700_000n) / MILLION + expect(target3Balance).to.equal(expectedTarget3) + }) + + it('should distribute correctly with multiple targets and default', async () => { + // Distribute any pending issuance first to start fresh + await issuanceAllocator.distributeIssuance() + + // Set default to target3 + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(addresses.target3) + + // Set allocations using evenIfDistributionPending to avoid premature distributions + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256,uint256,bool)'](addresses.target1, 200_000n, 0n, true) // 20% + + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256,uint256,bool)'](addresses.target2, 300_000n, 0n, true) // 30% + // Default (target3) gets 50% + + // Distribute once (exactly 1 block with the final allocations) + await issuanceAllocator.distributeIssuance() + + // Check all balances accounting for block accumulation: + // - target1 gets 20% for 2 blocks (from first setTargetAllocation onwards) + // - target2 gets 30% for 1 block (from second setTargetAllocation onwards) + // - target3 (default) gets 100% for 2 blocks + 80% for 1 block + 50% for 1 block + const target1Balance = await graphToken.balanceOf(addresses.target1) + const target2Balance = await graphToken.balanceOf(addresses.target2) + const target3Balance = await graphToken.balanceOf(addresses.target3) + + const expectedTarget1 = (issuancePerBlock * 200_000n * 2n) / MILLION + const expectedTarget2 = (issuancePerBlock * 300_000n) / MILLION + const expectedTarget3 = + issuancePerBlock * 2n + (issuancePerBlock * 800_000n) / MILLION + (issuancePerBlock * 500_000n) / MILLION + + expect(target1Balance).to.equal(expectedTarget1) + expect(target2Balance).to.equal(expectedTarget2) + expect(target3Balance).to.equal(expectedTarget3) + + // Total minted should equal 4 blocks of issuance + const totalMinted = target1Balance + target2Balance + target3Balance + expect(totalMinted).to.equal(issuancePerBlock * 4n) + }) + + it('should handle distribution when default allocation is 0%', async () => { + // Distribute any pending issuance first to start fresh + await issuanceAllocator.distributeIssuance() + + // Default is address(0), which doesn't receive minting + // Allocate 100% to explicit targets + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, 600_000n) + // At this point target1 has 60%, default has 40% + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target2, 400_000n) + // Now target1 has 60%, target2 has 40%, default has 0% + + // Distribute (1 block since last setTargetAllocation) + await issuanceAllocator.distributeIssuance() + + // Zero address (default) should receive nothing + const zeroBalance = await graphToken.balanceOf(ethers.ZeroAddress) + expect(zeroBalance).to.equal(0n) + + // Target1 receives: 0% (from first distributeIssuance to first setTargetAllocation) + // + 60% (from first setTargetAllocation to second setTargetAllocation) + // + 60% (from second setTargetAllocation to final distributeIssuance) + // = 120% of one block = 60% * 2 blocks + const target1Balance = await graphToken.balanceOf(addresses.target1) + expect(target1Balance).to.equal((issuancePerBlock * 600_000n * 2n) / MILLION) + + // Target2 receives: 40% (from second setTargetAllocation to final distributeIssuance) + const target2Balance = await graphToken.balanceOf(addresses.target2) + expect(target2Balance).to.equal((issuancePerBlock * 400_000n) / MILLION) + + // Default allocation is now 0% + const defaultAddress = await issuanceAllocator.getTargetAt(0) + const defaultAllocation = await issuanceAllocator.getTargetAllocation(defaultAddress) + expect(defaultAllocation.totalAllocationPPM).to.equal(0n) + }) + }) + + describe('View functions', () => { + it('should return correct target count including default', async () => { + let count = await issuanceAllocator.getTargetCount() + expect(count).to.equal(1n) // Just default + + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, 300_000n) + + count = await issuanceAllocator.getTargetCount() + expect(count).to.equal(2n) // Default + target1 + + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target2, 200_000n) + + count = await issuanceAllocator.getTargetCount() + expect(count).to.equal(3n) // Default + target1 + target2 + }) + + it('should include default in getTargets array', async () => { + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, 300_000n) + + const targets = await issuanceAllocator.getTargets() + expect(targets.length).to.equal(2) + expect(targets[0]).to.equal(ethers.ZeroAddress) // Default at index 0 + expect(targets[1]).to.equal(addresses.target1) + }) + + it('should return correct data for default target', async () => { + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, 400_000n) + + const defaultAddress = await issuanceAllocator.getTargetAt(0) + const data = await issuanceAllocator.getTargetData(defaultAddress) + + expect(data.allocatorMintingPPM).to.equal(600_000n) + expect(data.selfMintingPPM).to.equal(0n) + }) + + it('should report 100% total allocation when default is a real address', async () => { + // Set target1 allocation first + await issuanceAllocator + .connect(accounts.governor) + ['setTargetAllocation(address,uint256)'](addresses.target1, 300_000n) + + // Change default to target2 (a real address, not address(0)) + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(addresses.target2) + + // When default is a real address, it should report 100% total allocation + const totalAllocation = await issuanceAllocator.getTotalAllocation() + expect(totalAllocation.totalAllocationPPM).to.equal(MILLION) + expect(totalAllocation.allocatorMintingPPM).to.equal(MILLION) // target1=30% + target2=70% = 100% + expect(totalAllocation.selfMintingPPM).to.equal(0n) + }) + }) +}) diff --git a/packages/issuance/test/tests/allocate/InterfaceIdStability.test.ts b/packages/issuance/test/tests/allocate/InterfaceIdStability.test.ts index e6ee54260..fcb07ea85 100644 --- a/packages/issuance/test/tests/allocate/InterfaceIdStability.test.ts +++ b/packages/issuance/test/tests/allocate/InterfaceIdStability.test.ts @@ -26,7 +26,7 @@ describe('Allocate Interface ID Stability', () => { }) it('IIssuanceAllocationAdministration should have stable interface ID', () => { - expect(IIssuanceAllocationAdministration__factory.interfaceId).to.equal('0x36759695') + expect(IIssuanceAllocationAdministration__factory.interfaceId).to.equal('0x069d5a27') }) it('IIssuanceAllocationStatus should have stable interface ID', () => { diff --git a/packages/issuance/test/tests/allocate/IssuanceAllocator.test.ts b/packages/issuance/test/tests/allocate/IssuanceAllocator.test.ts index 8ecc20509..ad14331b5 100644 --- a/packages/issuance/test/tests/allocate/IssuanceAllocator.test.ts +++ b/packages/issuance/test/tests/allocate/IssuanceAllocator.test.ts @@ -74,11 +74,12 @@ describe('IssuanceAllocator', () => { const { issuanceAllocator } = sharedContracts - // Remove all existing allocations + // Remove all existing allocations (except default at index 0) try { const targetCount = await issuanceAllocator.getTargetCount() - for (let i = 0; i < targetCount; i++) { - const targetAddr = await issuanceAllocator.getTargetAt(0) // Always remove first + // Skip index 0 (default allocation) and remove from index 1 onwards + for (let i = 1; i < targetCount; i++) { + const targetAddr = await issuanceAllocator.getTargetAt(1) // Always remove index 1 await issuanceAllocator .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](targetAddr, 0, 0, false) @@ -305,6 +306,7 @@ describe('IssuanceAllocator', () => { const target1Allocation = await issuanceAllocator.getTargetAllocation(addresses.target1) expect(target1Allocation.totalAllocationPPM).to.equal(allocation) const totalAlloc = await issuanceAllocator.getTotalAllocation() + // With default as address(0), only non-default allocations are reported expect(totalAlloc.totalAllocationPPM).to.equal(allocation) // Remove target by setting allocation to 0 @@ -312,11 +314,11 @@ describe('IssuanceAllocator', () => { .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](addresses.target1, 0, 0, false) - // Verify target is removed + // Verify target is removed (only default remains) const targets = await issuanceAllocator.getTargets() - expect(targets.length).to.equal(0) + expect(targets.length).to.equal(1) // Only default allocation - // Verify total allocation is updated + // Verify reported total is 0% (default has it all, but isn't reported) { const totalAlloc = await issuanceAllocator.getTotalAllocation() expect(totalAlloc.totalAllocationPPM).to.equal(0) @@ -341,24 +343,25 @@ describe('IssuanceAllocator', () => { expect(target2Allocation.totalAllocationPPM).to.equal(400000) { const totalAlloc = await issuanceAllocator.getTotalAllocation() + // With default as address(0), only non-default allocations are reported (70%) expect(totalAlloc.totalAllocationPPM).to.equal(700000) } - // Get initial target addresses + // Get initial target addresses (including default) const initialTargets = await issuanceAllocator.getTargets() - expect(initialTargets.length).to.equal(2) + expect(initialTargets.length).to.equal(3) // default + target1 + target2 // Remove target2 by setting allocation to 0 (tests the swap-and-pop logic in the contract) await issuanceAllocator .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](addresses.target2, 0, 0, false) - // Verify target2 is removed but target1 remains + // Verify target2 is removed but target1 and default remain const remainingTargets = await issuanceAllocator.getTargets() - expect(remainingTargets.length).to.equal(1) - expect(remainingTargets[0]).to.equal(addresses.target1) + expect(remainingTargets.length).to.equal(2) // default + target1 + expect(remainingTargets).to.include(addresses.target1) - // Verify total allocation is updated (only target1's allocation remains) + // Verify reported total excludes default (only target1's 30% is reported) { const totalAlloc = await issuanceAllocator.getTotalAllocation() expect(totalAlloc.totalAllocationPPM).to.equal(300000) @@ -386,7 +389,7 @@ describe('IssuanceAllocator', () => { expect(target1Info.selfMintingPPM).to.equal(0) expect(target2Info.selfMintingPPM).to.equal(0) - // Verify total allocation is updated correctly + // Verify reported total excludes default (only target1+target2's 70% is reported) { const totalAlloc = await issuanceAllocator.getTotalAllocation() expect(totalAlloc.totalAllocationPPM).to.equal(300000) @@ -396,13 +399,14 @@ describe('IssuanceAllocator', () => { it('should validate setTargetAllocation parameters and constraints', async () => { const { issuanceAllocator, addresses } = sharedContracts - // Test 1: Should revert when setting allocation for target with address zero + // Test 1: Should revert when setting allocation for the default target (address zero) + // With the default allocation model, address(0) is the default allocation target await expectCustomError( issuanceAllocator .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](ethers.ZeroAddress, 100000, 0, false), issuanceAllocator, - 'TargetAddressCannotBeZero', + 'CannotSetAllocationForDefaultTarget', ) // Test 2: Should revert when setting non-zero allocation for target that does not support IIssuanceTarget @@ -420,7 +424,7 @@ describe('IssuanceAllocator', () => { .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](addresses.target1, 600_000, 0, false) - // Try to set allocation for target2 to 50%, which would exceed 100% + // Try to set allocation for target2 to 50%, which would exceed 100% (60% + 50% > 100%) await expectCustomError( issuanceAllocator .connect(accounts.governor) @@ -841,13 +845,13 @@ describe('IssuanceAllocator', () => { await issuanceAllocator.connect(accounts.governor).grantRole(PAUSE_ROLE, accounts.governor.address) await issuanceAllocator.connect(accounts.governor).setIssuancePerBlock(ethers.parseEther('100'), false) - // Mix of targets: 30% allocator-minting, 70% self-minting + // Mix of targets: 20% allocator-minting, 5% self-minting (leaving 75% for default, total 95% allocator) await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 300000, 0, false) // 30% allocator-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 200000, 0, false) // 20% allocator-minting await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 0, 700000, false) // 70% self-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 0, 50000, false) // 5% self-minting // Initialize distribution await issuanceAllocator.connect(accounts.governor).distributeIssuance() @@ -859,24 +863,25 @@ describe('IssuanceAllocator', () => { await ethers.provider.send('evm_mine', []) await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 0, 600000, true) // Change self-minting from 70% to 60% + ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 0, 0, true) // Change self-minting from 5% to 0% // Accumulation should happen from lastIssuanceDistributionBlock to current block const blockAfterAccumulation = await ethers.provider.getBlockNumber() const pendingAmount = await issuanceAllocator.pendingAccumulatedAllocatorIssuance() const lastDistributionBlock = await issuanceAllocator.lastIssuanceDistributionBlock() - const allocation = await issuanceAllocator.getTotalAllocation() // Calculate what accumulation SHOULD be from lastDistributionBlock + // During accumulation: 20% (target1) + 75% (default) = 95% allocator-minting, 5% self-minting + // Accumulated issuance is based on the 95% allocator-minting that was active during accumulation const blocksFromDistribution = BigInt(blockAfterAccumulation) - BigInt(lastDistributionBlock) + const allocatorMintingDuringAccumulation = 950000n // 95% in PPM const expectedFromDistribution = calculateExpectedAccumulation( parseEther('100'), blocksFromDistribution, - allocation.allocatorMintingPPM, + allocatorMintingDuringAccumulation, ) - // This will fail, but we can see which calculation matches the actual result expect(pendingAmount).to.equal(expectedFromDistribution) // Now test distribution of pending issuance to cover the self-minter branch @@ -903,20 +908,20 @@ describe('IssuanceAllocator', () => { await issuanceAllocator.connect(accounts.governor).grantRole(PAUSE_ROLE, accounts.governor.address) await issuanceAllocator.connect(accounts.governor).setIssuancePerBlock(ethers.parseEther('1000'), false) - // Mix of targets: 20% and 30% allocator-minting (50% total), 50% self-minting + // Mix of targets: 15% and 25% allocator-minting (40% total), 10% self-minting (leaving 50% for default, total 90% allocator) await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 200000, 0, false) // 20% allocator-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 150000, 0, false) // 15% allocator-minting await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 300000, 0, false) // 30% allocator-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 250000, 0, false) // 25% allocator-minting // Add a self-minting target to create the mixed scenario const MockTarget = await ethers.getContractFactory('MockSimpleTarget') const selfMintingTarget = await MockTarget.deploy() await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await selfMintingTarget.getAddress(), 0, 500000, false) // 50% self-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await selfMintingTarget.getAddress(), 0, 100000, false) // 10% self-minting // Initialize and pause await issuanceAllocator.connect(accounts.governor).distributeIssuance() @@ -930,23 +935,24 @@ describe('IssuanceAllocator', () => { await ethers.provider.send('evm_mine', []) await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await selfMintingTarget.getAddress(), 0, 400000, true) // Change self-minting from 50% to 40% + ['setTargetAllocation(address,uint256,uint256,bool)'](await selfMintingTarget.getAddress(), 0, 0, true) // Change self-minting from 10% to 0% // Calculate actual blocks accumulated (from block 0 since lastIssuanceAccumulationBlock starts at 0) const blockAfterAccumulation = await ethers.provider.getBlockNumber() - // Verify accumulation: 50% allocator-minting allocation (500000 PPM) + // Verify accumulation: 90% allocator-minting allocation (150000 + 250000 + 500000 default = 900000 PPM) // Accumulation should happen from lastIssuanceDistributionBlock to current block const pendingAmount = await issuanceAllocator.pendingAccumulatedAllocatorIssuance() const lastDistributionBlock = await issuanceAllocator.lastIssuanceDistributionBlock() // Calculate expected accumulation from when issuance was last distributed + // During accumulation: 15% (target1) + 25% (target2) + 50% (default) = 90% allocator-minting, 10% self-minting const blocksToAccumulate = BigInt(blockAfterAccumulation) - BigInt(lastDistributionBlock) - const allocation = await issuanceAllocator.getTotalAllocation() + const allocatorMintingDuringAccumulation = 900000n // 90% in PPM const expectedPending = calculateExpectedAccumulation( parseEther('1000'), blocksToAccumulate, - allocation.allocatorMintingPPM, + allocatorMintingDuringAccumulation, ) expect(pendingAmount).to.equal(expectedPending) @@ -959,11 +965,11 @@ describe('IssuanceAllocator', () => { const finalBalance2 = await (graphToken as any).balanceOf(await target2.getAddress()) // Calculate expected distributions: - // Total allocator-minting allocation: 200000 + 300000 = 500000 - // target1 should get: 2000 * (200000 / 500000) = 800 tokens from pending (doubled due to known issue) - // target2 should get: 2000 * (300000 / 500000) = 1200 tokens from pending (doubled due to known issue) - const expectedTarget1Pending = ethers.parseEther('800') - const expectedTarget2Pending = ethers.parseEther('1200') + // Total allocator-minting allocation after change: 150000 + 250000 + 600000 (default) = 1000000 (100%) + // target1 should get: 2000 * (150000 / 1000000) = 300 tokens from pending (doubled due to known issue) + // target2 should get: 2000 * (250000 / 1000000) = 500 tokens from pending (doubled due to known issue) + const expectedTarget1Pending = ethers.parseEther('300') + const expectedTarget2Pending = ethers.parseEther('500') // Account for any additional issuance from the distribution block itself const pendingDistribution1 = finalBalance1 - initialBalance1 @@ -974,10 +980,10 @@ describe('IssuanceAllocator', () => { expect(pendingDistribution1).to.be.gte(expectedTarget1Pending) expect(pendingDistribution2).to.be.gte(expectedTarget2Pending) - // Verify the ratio is correct: target2 should get 1.5x what target1 gets from pending - // (300000 / 200000 = 1.5) + // Verify the ratio is correct: target2 should get 1.67x what target1 gets from pending + // (250000 / 150000 = 1.67) const ratio = (BigInt(pendingDistribution2) * 1000n) / BigInt(pendingDistribution1) // Multiply by 1000 for precision - expect(ratio).to.be.closeTo(1500n, 50n) // Allow small rounding tolerance + expect(ratio).to.be.closeTo(1667n, 100n) // Allow larger tolerance due to default allocation adjustments // Verify pending was reset expect(await issuanceAllocator.pendingAccumulatedAllocatorIssuance()).to.equal(0) @@ -991,13 +997,13 @@ describe('IssuanceAllocator', () => { await issuanceAllocator.connect(accounts.governor).grantRole(PAUSE_ROLE, accounts.governor.address) await issuanceAllocator.connect(accounts.governor).setIssuancePerBlock(ethers.parseEther('1000'), false) - // Allocator-minting targets: 40% and 60%, plus a small self-minting target initially + // Allocator-minting targets: 30% and 50%, plus a small self-minting target initially (leaving 19% for default) await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 400000, 0, false) // 40% allocator-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 300000, 0, false) // 30% allocator-minting await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 590000, 10000, false) // 59% allocator-minting, 1% self-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 500000, 10000, false) // 50% allocator-minting, 1% self-minting // Initialize and pause await issuanceAllocator.connect(accounts.governor).distributeIssuance() @@ -1012,19 +1018,19 @@ describe('IssuanceAllocator', () => { await ethers.provider.send('evm_mine', []) await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 600000, 0, true) // Remove self-minting, now 100% allocator-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 510000, 0, true) // Remove self-minting (now 51% allocator-minting, leaving 19% for default) // Calculate actual blocks accumulated (from block 0 since lastIssuanceAccumulationBlock starts at 0) const blockAfterAccumulation = await ethers.provider.getBlockNumber() - // Verify accumulation: should use the OLD allocation (99% allocator-minting) that was active during pause - // Accumulation happens BEFORE the allocation change, so uses 40% + 59% = 99% + // Verify accumulation: should use the OLD allocation (80% allocator-minting) that was active during pause + // Accumulation happens BEFORE the allocation change, so uses 30% + 50% + 19% default = 99% allocator-minting, 1% self-minting const pendingAmount = await issuanceAllocator.pendingAccumulatedAllocatorIssuance() const lastDistributionBlock = await issuanceAllocator.lastIssuanceDistributionBlock() // Calculate expected accumulation using the OLD allocation (before the change) const blocksToAccumulate = BigInt(blockAfterAccumulation) - BigInt(lastDistributionBlock) - const oldAllocatorMintingPPM = 400000n + 590000n // 40% + 59% = 99% + const oldAllocatorMintingPPM = 300000n + 500000n + 190000n // 30% + 50% + 19% default = 99% const expectedPending = calculateExpectedAccumulation( parseEther('1000'), blocksToAccumulate, @@ -1041,11 +1047,11 @@ describe('IssuanceAllocator', () => { const finalBalance2 = await (graphToken as any).balanceOf(await target2.getAddress()) // Calculate expected distributions: - // Total allocator-minting allocation: 400000 + 600000 = 1000000 (100%) - // target1 should get: 5000 * (400000 / 1000000) = 2000 tokens from pending - // target2 should get: 5000 * (600000 / 1000000) = 3000 tokens from pending - const expectedTarget1Pending = ethers.parseEther('2000') - const expectedTarget2Pending = ethers.parseEther('3000') + // Total allocator-minting allocation: 300000 + 510000 + 190000 = 1000000 (100%) + // target1 should get: 5000 * (300000 / 1000000) = 1500 tokens from pending + // target2 should get: 5000 * (510000 / 1000000) = 2550 tokens from pending + const expectedTarget1Pending = ethers.parseEther('1500') + const expectedTarget2Pending = ethers.parseEther('2550') // Account for any additional issuance from the distribution block itself const pendingDistribution1 = finalBalance1 - initialBalance1 @@ -1055,10 +1061,10 @@ describe('IssuanceAllocator', () => { expect(pendingDistribution1).to.be.gte(expectedTarget1Pending) expect(pendingDistribution2).to.be.gte(expectedTarget2Pending) - // Verify the ratio is correct: target2 should get 1.5x what target1 gets from pending - // (600000 / 400000 = 1.5) + // Verify the ratio is correct: target2 should get 1.7x what target1 gets from pending + // (510000 / 300000 = 1.7) const ratio = (BigInt(pendingDistribution2) * 1000n) / BigInt(pendingDistribution1) // Multiply by 1000 for precision - expect(ratio).to.be.closeTo(1500n, 50n) // Allow small rounding tolerance + expect(ratio).to.be.closeTo(1700n, 50n) // Allow small rounding tolerance // Verify pending was reset expect(await issuanceAllocator.pendingAccumulatedAllocatorIssuance()).to.equal(0) @@ -1076,13 +1082,13 @@ describe('IssuanceAllocator', () => { const MockTarget = await ethers.getContractFactory('MockSimpleTarget') const target3 = await MockTarget.deploy() - // Mix of targets: 30% + 20% + 10% allocator-minting (60% total), 40% self-minting + // Mix of targets: 25% + 15% + 10% allocator-minting (50% total), 20% self-minting (leaving 30% for default, total 80% allocator) await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 300000, 0, false) // 30% allocator-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 250000, 0, false) // 25% allocator-minting await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 200000, 0, false) // 20% allocator-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 150000, 0, false) // 15% allocator-minting await issuanceAllocator .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](await target3.getAddress(), 100000, 0, false) // 10% allocator-minting @@ -1091,7 +1097,7 @@ describe('IssuanceAllocator', () => { const selfMintingTarget = await MockTarget.deploy() await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await selfMintingTarget.getAddress(), 0, 400000, false) // 40% self-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await selfMintingTarget.getAddress(), 0, 200000, false) // 20% self-minting // Initialize and pause await issuanceAllocator.connect(accounts.governor).distributeIssuance() @@ -1107,23 +1113,24 @@ describe('IssuanceAllocator', () => { } await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await selfMintingTarget.getAddress(), 0, 300000, true) // Change self-minting from 40% to 30% + ['setTargetAllocation(address,uint256,uint256,bool)'](await selfMintingTarget.getAddress(), 0, 100000, true) // Change self-minting from 20% to 10% // Calculate actual blocks accumulated (from block 0 since lastIssuanceAccumulationBlock starts at 0) const blockAfterAccumulation = await ethers.provider.getBlockNumber() - // Calculate expected total accumulation: 60% allocator-minting allocation (600000 PPM) + // Calculate expected total accumulation: 80% allocator-minting allocation (25% + 15% + 10% + 30% default = 800000 PPM) // Accumulation should happen from lastIssuanceDistributionBlock to current block const pendingAmount = await issuanceAllocator.pendingAccumulatedAllocatorIssuance() const lastDistributionBlock = await issuanceAllocator.lastIssuanceDistributionBlock() // Calculate expected accumulation from when issuance was last distributed + // During accumulation: 25% (target1) + 15% (target2) + 10% (target3) + 30% (default) = 80% allocator-minting, 20% self-minting const blocksToAccumulate = BigInt(blockAfterAccumulation) - BigInt(lastDistributionBlock) - const allocation = await issuanceAllocator.getTotalAllocation() + const allocatorMintingDuringAccumulation = 800000n // 80% in PPM const expectedPending = calculateExpectedAccumulation( parseEther('1000'), blocksToAccumulate, - allocation.allocatorMintingPPM, + allocatorMintingDuringAccumulation, ) expect(pendingAmount).to.equal(expectedPending) @@ -1142,19 +1149,22 @@ describe('IssuanceAllocator', () => { const totalDistributed = distribution1 + distribution2 + distribution3 // Verify total distributed amount is reasonable - // Should be at least the pending amount (might be more due to additional block issuance) - expect(totalDistributed).to.be.gte(pendingAmount) + // The three explicit targets get 50% of total allocation, default gets 30% + // So they should receive (50/80) = 62.5% of pending allocator-minting issuance + // Plus additional issuance from blocks between accumulation and distribution + const expectedMinimumToThreeTargets = (pendingAmount * 50n) / 80n + expect(totalDistributed).to.be.gte(expectedMinimumToThreeTargets) // Verify proportional distribution within allocator-minting targets - // Total allocator-minting allocation: 300000 + 200000 + 100000 = 600000 - // Expected ratios: target1:target2:target3 = 30:20:10 = 3:2:1 - const ratio12 = (BigInt(distribution1) * 1000n) / BigInt(distribution2) // Should be ~1500 (3/2 * 1000) - const ratio13 = (BigInt(distribution1) * 1000n) / BigInt(distribution3) // Should be ~3000 (3/1 * 1000) - const ratio23 = (BigInt(distribution2) * 1000n) / BigInt(distribution3) // Should be ~2000 (2/1 * 1000) + // Actual allocations: target1=25%, target2=15%, target3=10% + // Expected ratios: target1:target2:target3 = 25:15:10 = 5:3:2 + const ratio12 = (BigInt(distribution1) * 1000n) / BigInt(distribution2) // Should be ~1667 (5/3 * 1000) + const ratio13 = (BigInt(distribution1) * 1000n) / BigInt(distribution3) // Should be ~2500 (5/2 * 1000) + const ratio23 = (BigInt(distribution2) * 1000n) / BigInt(distribution3) // Should be ~1500 (3/2 * 1000) - expect(ratio12).to.be.closeTo(1500n, 100n) // 3:2 ratio with tolerance - expect(ratio13).to.be.closeTo(3000n, 200n) // 3:1 ratio with tolerance - expect(ratio23).to.be.closeTo(2000n, 150n) // 2:1 ratio with tolerance + expect(ratio12).to.be.closeTo(1667n, 100n) // 5:3 ratio with tolerance + expect(ratio13).to.be.closeTo(2500n, 200n) // 5:2 ratio with tolerance + expect(ratio23).to.be.closeTo(1500n, 150n) // 3:2 ratio with tolerance // Verify pending was reset expect(await issuanceAllocator.pendingAccumulatedAllocatorIssuance()).to.equal(0) @@ -2027,11 +2037,11 @@ describe('IssuanceAllocator', () => { .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](nonExistentTarget, 0, 0, false) - // Verify no targets were added + // Verify no non-default targets were added (only default remains) const targets = await issuanceAllocator.getTargets() - expect(targets.length).to.equal(0) + expect(targets.length).to.equal(1) // Only default allocation - // Verify total allocation remains 0 + // Verify reported total is 0% (all in default, which isn't reported) const totalAlloc = await issuanceAllocator.getTotalAllocation() expect(totalAlloc.totalAllocationPPM).to.equal(0) @@ -2040,9 +2050,9 @@ describe('IssuanceAllocator', () => { .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](nonExistentTarget, 0, 0, false) - // Verify still no targets + // Verify still only default target const targetsAfter = await issuanceAllocator.getTargets() - expect(targetsAfter.length).to.equal(0) + expect(targetsAfter.length).to.equal(1) // Only default allocation }) }) @@ -2067,24 +2077,24 @@ describe('IssuanceAllocator', () => { it('should manage target count and array correctly', async () => { const { issuanceAllocator, addresses } = sharedContracts - // Test initial state - expect(await issuanceAllocator.getTargetCount()).to.equal(0) - expect((await issuanceAllocator.getTargets()).length).to.equal(0) + // Test initial state (with default allocation) + expect(await issuanceAllocator.getTargetCount()).to.equal(1) // Default allocation exists + expect((await issuanceAllocator.getTargets()).length).to.equal(1) // Test adding targets await issuanceAllocator .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](addresses.target1, 100000, 0, false) - expect(await issuanceAllocator.getTargetCount()).to.equal(1) + expect(await issuanceAllocator.getTargetCount()).to.equal(2) // Default + target1 await issuanceAllocator .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](addresses.target2, 200000, 0, false) - expect(await issuanceAllocator.getTargetCount()).to.equal(2) + expect(await issuanceAllocator.getTargetCount()).to.equal(3) // Default + target1 + target2 // Test getTargets array content const targetAddresses = await issuanceAllocator.getTargets() - expect(targetAddresses.length).to.equal(2) + expect(targetAddresses.length).to.equal(3) expect(targetAddresses).to.include(addresses.target1) expect(targetAddresses).to.include(addresses.target2) @@ -2092,13 +2102,13 @@ describe('IssuanceAllocator', () => { await issuanceAllocator .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](addresses.target1, 0, 0, false) - expect(await issuanceAllocator.getTargetCount()).to.equal(1) + expect(await issuanceAllocator.getTargetCount()).to.equal(2) // Default + target2 await issuanceAllocator .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](addresses.target2, 0, 0, false) - expect(await issuanceAllocator.getTargetCount()).to.equal(0) - expect((await issuanceAllocator.getTargets()).length).to.equal(0) + expect(await issuanceAllocator.getTargetCount()).to.equal(1) // Only default remains + expect((await issuanceAllocator.getTargets()).length).to.equal(1) }) it('should store targets in the getTargets array in correct order', async () => { @@ -2116,9 +2126,11 @@ describe('IssuanceAllocator', () => { const targetAddresses = await issuanceAllocator.getTargets() // Check that the addresses are in the correct order - expect(targetAddresses[0]).to.equal(addresses.target1) - expect(targetAddresses[1]).to.equal(addresses.target2) - expect(targetAddresses.length).to.equal(2) + // targetAddresses[0] is the default allocation (address(0)) + expect(targetAddresses[0]).to.equal(ethers.ZeroAddress) // Default + expect(targetAddresses[1]).to.equal(addresses.target1) + expect(targetAddresses[2]).to.equal(addresses.target2) + expect(targetAddresses.length).to.equal(3) // Default + target1 + target2 }) it('should return the correct target address by index', async () => { @@ -2140,17 +2152,20 @@ describe('IssuanceAllocator', () => { // Get all target addresses const addresses = await issuanceAllocator.getTargets() - expect(addresses.length).to.equal(3) + expect(addresses.length).to.equal(4) // Default + 3 targets // Check that the addresses are in the correct order - expect(addresses[0]).to.equal(await target1.getAddress()) - expect(addresses[1]).to.equal(await target2.getAddress()) - expect(addresses[2]).to.equal(await target3.getAddress()) + // addresses[0] is the default allocation (address(0)) + expect(addresses[0]).to.equal(ethers.ZeroAddress) // Default + expect(addresses[1]).to.equal(await target1.getAddress()) + expect(addresses[2]).to.equal(await target2.getAddress()) + expect(addresses[3]).to.equal(await target3.getAddress()) // Test getTargetAt method for individual access - expect(await issuanceAllocator.getTargetAt(0)).to.equal(await target1.getAddress()) - expect(await issuanceAllocator.getTargetAt(1)).to.equal(await target2.getAddress()) - expect(await issuanceAllocator.getTargetAt(2)).to.equal(await target3.getAddress()) + expect(await issuanceAllocator.getTargetAt(0)).to.equal(ethers.ZeroAddress) // Default + expect(await issuanceAllocator.getTargetAt(1)).to.equal(await target1.getAddress()) + expect(await issuanceAllocator.getTargetAt(2)).to.equal(await target2.getAddress()) + expect(await issuanceAllocator.getTargetAt(3)).to.equal(await target3.getAddress()) }) it('should return the correct target allocation', async () => { @@ -2958,20 +2973,20 @@ describe('IssuanceAllocator', () => { await issuanceAllocator.connect(accounts.governor).grantRole(PAUSE_ROLE, accounts.governor.address) await issuanceAllocator.connect(accounts.governor).setIssuancePerBlock(ethers.parseEther('1000'), false) - // Test scenario: 25% allocator-minting + 50% self-minting + 25% unallocated + // Test scenario: 20% allocator-minting + 40% self-minting (leaving 40% for default) await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 250000, 0, false) // 25% allocator-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 200000, 0, false) // 20% allocator-minting await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 0, 500000, false) // 50% self-minting - // 25% remains unallocated + ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 0, 400000, false) // 40% self-minting + // 40% goes to default allocation // Verify the setup const totalAllocation = await issuanceAllocator.getTotalAllocation() - expect(totalAllocation.totalAllocationPPM).to.equal(750000) // 75% total - expect(totalAllocation.allocatorMintingPPM).to.equal(250000) // 25% allocator - expect(totalAllocation.selfMintingPPM).to.equal(500000) // 50% self + expect(totalAllocation.totalAllocationPPM).to.equal(600000) // 60% reported (excludes default's 40%) + expect(totalAllocation.allocatorMintingPPM).to.equal(200000) // 20% allocator (excludes default's 40%) + expect(totalAllocation.selfMintingPPM).to.equal(400000) // 40% self // Distribute once to initialize await issuanceAllocator.connect(accounts.governor).distributeIssuance() @@ -3004,9 +3019,9 @@ describe('IssuanceAllocator', () => { expect(distributed2).to.equal(0) // Target1 should receive the correct proportional amount - // The calculation is: (pendingAmount * 250000) / (1000000 - 500000) = (pendingAmount * 250000) / 500000 = pendingAmount * 0.5 - // So target1 should get exactly 50% of the pending amount - const expectedDistribution = pendingBefore / 2n // 50% of pending + // The calculation is: (pendingAmount * 200000) / (1000000 - 400000) = (pendingAmount * 200000) / 600000 = pendingAmount * 1/3 + // So target1 should get exactly 33.33% of the pending amount + const expectedDistribution = (pendingBefore * 200000n) / 600000n // 33.33% of pending expect(distributed1).to.be.closeTo(expectedDistribution, ethers.parseEther('1')) // Verify pending issuance was reset @@ -3021,22 +3036,22 @@ describe('IssuanceAllocator', () => { await issuanceAllocator.connect(accounts.governor).grantRole(PAUSE_ROLE, accounts.governor.address) await issuanceAllocator.connect(accounts.governor).setIssuancePerBlock(ethers.parseEther('1000'), false) - // Test scenario: 15% + 10% allocator-minting + 50% self-minting + 25% unallocated + // Test scenario: 12% + 8% allocator-minting + 40% self-minting (leaving 40% for default) await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 150000, 0, false) // 15% allocator-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target1.getAddress(), 120000, 0, false) // 12% allocator-minting await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 100000, 0, false) // 10% allocator-minting + ['setTargetAllocation(address,uint256,uint256,bool)'](await target2.getAddress(), 80000, 0, false) // 8% allocator-minting await issuanceAllocator .connect(accounts.governor) - ['setTargetAllocation(address,uint256,uint256,bool)'](await target3.getAddress(), 0, 500000, false) // 50% self-minting - // 25% remains unallocated + ['setTargetAllocation(address,uint256,uint256,bool)'](await target3.getAddress(), 0, 400000, false) // 40% self-minting + // 40% goes to default allocation // Verify the setup const totalAllocation = await issuanceAllocator.getTotalAllocation() - expect(totalAllocation.allocatorMintingPPM).to.equal(250000) // 25% total allocator - expect(totalAllocation.selfMintingPPM).to.equal(500000) // 50% self + expect(totalAllocation.allocatorMintingPPM).to.equal(200000) // 12% + 8% = 20% (excludes default's 40%) + expect(totalAllocation.selfMintingPPM).to.equal(400000) // 40% self // Distribute once to initialize await issuanceAllocator.connect(accounts.governor).distributeIssuance() @@ -3072,21 +3087,21 @@ describe('IssuanceAllocator', () => { expect(distributed3).to.equal(0) // Verify proportional distribution between allocator-minting targets - // Target1 should get 15/25 = 60% of the distributed amount - // Target2 should get 10/25 = 40% of the distributed amount + // Target1 should get 12/20 = 60% of the distributed amount + // Target2 should get 8/20 = 40% of the distributed amount if (distributed1 > 0 && distributed2 > 0) { const ratio = (BigInt(distributed1) * 1000n) / BigInt(distributed2) // Multiply by 1000 for precision - expect(ratio).to.be.closeTo(1500n, 50n) // 150000/100000 = 1.5 + expect(ratio).to.be.closeTo(1500n, 50n) // 120000/80000 = 1.5 } // Total distributed should equal the allocator-minting portion of pending - // With 25% total allocator-minting out of 50% allocator-minting space: + // With 20% total allocator-minting (12% + 8%) out of 60% allocator-minting space (20% + 40% default): // Each target gets: (targetPPM / (MILLION - selfMintingPPM)) * pendingAmount - // Target1: (150000 / 500000) * pendingAmount = 30% of pending - // Target2: (100000 / 500000) * pendingAmount = 20% of pending - // Total: 50% of pending + // Target1: (120000 / 600000) * pendingAmount = 20% of pending + // Target2: (80000 / 600000) * pendingAmount = 13.33% of pending + // Total: 33.33% of pending const totalDistributed = distributed1 + distributed2 - const expectedTotal = pendingBefore / 2n // 50% of pending + const expectedTotal = (pendingBefore * 200000n) / 600000n // 33.33% of pending expect(totalDistributed).to.be.closeTo(expectedTotal, ethers.parseEther('1')) }) }) diff --git a/packages/issuance/test/tests/allocate/IssuanceSystem.test.ts b/packages/issuance/test/tests/allocate/IssuanceSystem.test.ts index 5a2de54aa..77645546a 100644 --- a/packages/issuance/test/tests/allocate/IssuanceSystem.test.ts +++ b/packages/issuance/test/tests/allocate/IssuanceSystem.test.ts @@ -84,13 +84,11 @@ describe('Issuance System', () => { // Set up initial allocations using helper await system.helpers.setupStandardAllocations() - // Verify initial total allocation (30% + 40% = 70%) + // Verify initial total allocation (excludes default since it's address(0)) const totalAlloc = await contracts.issuanceAllocator.getTotalAllocation() - expect(totalAlloc.totalAllocationPPM).to.equal( - TestConstants.ALLOCATION_30_PERCENT + TestConstants.ALLOCATION_40_PERCENT, - ) + expect(totalAlloc.totalAllocationPPM).to.equal(700000) // 70% (30% + 40%, excludes default) - // Change allocations: target1 = 50%, target2 = 20% (still 70%) + // Change allocations: target1 = 50%, target2 = 20% (30% goes to default) await contracts.issuanceAllocator .connect(accounts.governor) [ @@ -102,11 +100,9 @@ describe('Issuance System', () => { 'setTargetAllocation(address,uint256,uint256,bool)' ](addresses.target2, TestConstants.ALLOCATION_20_PERCENT, 0, false) - // Verify updated allocations + // Verify updated allocations (excludes default since it's address(0)) const updatedTotalAlloc = await contracts.issuanceAllocator.getTotalAllocation() - expect(updatedTotalAlloc.totalAllocationPPM).to.equal( - TestConstants.ALLOCATION_50_PERCENT + TestConstants.ALLOCATION_20_PERCENT, - ) + expect(updatedTotalAlloc.totalAllocationPPM).to.equal(700000) // 70% (50% + 20%, excludes default) // Verify individual target allocations const target1Info = await contracts.issuanceAllocator.getTargetData(addresses.target1) diff --git a/packages/issuance/test/tests/allocate/optimizedFixtures.ts b/packages/issuance/test/tests/allocate/optimizedFixtures.ts index 22f407f7d..6ded870a1 100644 --- a/packages/issuance/test/tests/allocate/optimizedFixtures.ts +++ b/packages/issuance/test/tests/allocate/optimizedFixtures.ts @@ -73,14 +73,20 @@ export async function setupOptimizedAllocateSystem(customOptions: any = {}) { helpers: { // Helper to reset state without redeploying resetState: async () => { - // Remove all targets + // Remove all targets except the default at index 0 const targets = await issuanceAllocator.getTargets() + const defaultAddress = await issuanceAllocator.getTargetAt(0) for (const targetAddr of targets) { + // Skip the default allocation target + if (targetAddr === defaultAddress) continue await issuanceAllocator .connect(accounts.governor) ['setTargetAllocation(address,uint256,uint256,bool)'](targetAddr, 0, 0, false) } + // Reset default allocation to address(0) with 100% + await issuanceAllocator.connect(accounts.governor).setDefaultAllocationAddress(ethers.ZeroAddress) + // Reset issuance rate await issuanceAllocator.connect(accounts.governor).setIssuancePerBlock(options.issuancePerBlock, false) }, diff --git a/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol b/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol index 8286f2570..773d676f9 100644 --- a/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol +++ b/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol @@ -57,6 +57,12 @@ contract MockRewardsManager is IRewardsManager { function isDenied(bytes32) external view returns (bool) {} + // -- Reclaim -- + + function setSubgraphDeniedReclaimAddress(address) external {} + + function setIndexerEligibilityReclaimAddress(address) external {} + // -- Getters -- function getNewRewardsPerSignal() external view returns (uint256) {}