Skip to content

Commit 9488b9d

Browse files
committed
fixup! refactor: migrate IssuanceAllocator from PPM to absolute rates and add security improvements
1 parent 0ccf381 commit 9488b9d

File tree

2 files changed

+294
-0
lines changed

2 files changed

+294
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import { IIssuanceTarget } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol";
5+
import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
6+
7+
/**
8+
* @title MockNotificationTracker
9+
* @author Edge & Node
10+
* @notice A mock contract that tracks notification calls for testing
11+
* @dev Records when beforeIssuanceAllocationChange is called
12+
*/
13+
contract MockNotificationTracker is IIssuanceTarget, ERC165 {
14+
/// @notice Number of times the contract has been notified
15+
uint256 public notificationCount;
16+
17+
/// @notice Block number of the last notification received
18+
uint256 public lastNotificationBlock;
19+
20+
/// @notice Emitted when a notification is received
21+
/// @param blockNumber The block number when notification was received
22+
/// @param count The total notification count after this notification
23+
event NotificationReceived(uint256 indexed blockNumber, uint256 indexed count); // solhint-disable-line gas-indexed-events
24+
25+
/// @inheritdoc IIssuanceTarget
26+
function beforeIssuanceAllocationChange() external override {
27+
++notificationCount;
28+
lastNotificationBlock = block.number;
29+
emit NotificationReceived(block.number, notificationCount);
30+
}
31+
32+
/// @inheritdoc IIssuanceTarget
33+
function setIssuanceAllocator(address _issuanceAllocator) external pure override {}
34+
35+
/// @inheritdoc ERC165
36+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
37+
return interfaceId == type(IIssuanceTarget).interfaceId || super.supportsInterface(interfaceId);
38+
}
39+
40+
/// @notice Resets the notification counter and last block to zero
41+
function resetNotificationCount() external {
42+
notificationCount = 0;
43+
lastNotificationBlock = 0;
44+
}
45+
}
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import { expect } from 'chai'
2+
import hre from 'hardhat'
3+
4+
const { ethers } = hre
5+
6+
import { getTestAccounts } from '../common/fixtures'
7+
import { deployTestGraphToken } from '../common/fixtures'
8+
import { deployIssuanceAllocator } from './fixtures'
9+
10+
describe('IssuanceAllocator - Target Notification', () => {
11+
let accounts
12+
let addresses: {
13+
target1: string
14+
target2: string
15+
defaultTarget: string
16+
}
17+
18+
let issuanceAllocator
19+
let graphToken
20+
let target1
21+
let target2
22+
let defaultTarget
23+
24+
const issuancePerBlock = ethers.parseEther('100')
25+
26+
beforeEach(async () => {
27+
// Get test accounts
28+
accounts = await getTestAccounts()
29+
30+
// Deploy GraphToken
31+
graphToken = await deployTestGraphToken()
32+
33+
// Deploy IssuanceAllocator
34+
issuanceAllocator = await deployIssuanceAllocator(
35+
await graphToken.getAddress(),
36+
accounts.governor,
37+
issuancePerBlock,
38+
)
39+
40+
// Grant minter role to IssuanceAllocator
41+
await graphToken.addMinter(await issuanceAllocator.getAddress())
42+
43+
// Deploy mock notification trackers
44+
const MockNotificationTracker = await ethers.getContractFactory('MockNotificationTracker')
45+
target1 = await MockNotificationTracker.deploy()
46+
target2 = await MockNotificationTracker.deploy()
47+
defaultTarget = await MockNotificationTracker.deploy()
48+
49+
addresses = {
50+
target1: await target1.getAddress(),
51+
target2: await target2.getAddress(),
52+
defaultTarget: await defaultTarget.getAddress(),
53+
}
54+
})
55+
56+
describe('setTargetAllocation notifications', () => {
57+
it('should notify both target and default target when setting allocation', async () => {
58+
// Set a non-zero default target first
59+
await issuanceAllocator.connect(accounts.governor).setDefaultTarget(addresses.defaultTarget)
60+
61+
// Verify initial state
62+
expect(await target1.notificationCount()).to.equal(0)
63+
expect(await defaultTarget.notificationCount()).to.equal(1) // Notified during setDefaultTarget
64+
65+
// Reset notification count for clean test
66+
await defaultTarget.resetNotificationCount()
67+
68+
// Set allocation for target1 - should notify BOTH target1 and defaultTarget
69+
await issuanceAllocator
70+
.connect(accounts.governor)
71+
['setTargetAllocation(address,uint256)'](addresses.target1, ethers.parseEther('30'))
72+
73+
// Verify both targets were notified
74+
expect(await target1.notificationCount()).to.equal(1)
75+
expect(await defaultTarget.notificationCount()).to.equal(1)
76+
})
77+
78+
it('should notify both targets when changing existing allocation', async () => {
79+
// Set a non-zero default target
80+
await issuanceAllocator.connect(accounts.governor).setDefaultTarget(addresses.defaultTarget)
81+
82+
// Set initial allocation for target1
83+
await issuanceAllocator
84+
.connect(accounts.governor)
85+
['setTargetAllocation(address,uint256)'](addresses.target1, ethers.parseEther('30'))
86+
87+
// Reset counters
88+
await target1.resetNotificationCount()
89+
await defaultTarget.resetNotificationCount()
90+
91+
// Change allocation for target1
92+
await issuanceAllocator
93+
.connect(accounts.governor)
94+
['setTargetAllocation(address,uint256)'](addresses.target1, ethers.parseEther('50'))
95+
96+
// Both should be notified again
97+
expect(await target1.notificationCount()).to.equal(1)
98+
expect(await defaultTarget.notificationCount()).to.equal(1)
99+
})
100+
101+
it('should notify both targets when removing allocation', async () => {
102+
// Set a non-zero default target
103+
await issuanceAllocator.connect(accounts.governor).setDefaultTarget(addresses.defaultTarget)
104+
105+
// Set initial allocation
106+
await issuanceAllocator
107+
.connect(accounts.governor)
108+
['setTargetAllocation(address,uint256)'](addresses.target1, ethers.parseEther('30'))
109+
110+
// Reset counters
111+
await target1.resetNotificationCount()
112+
await defaultTarget.resetNotificationCount()
113+
114+
// Remove allocation (set to 0)
115+
await issuanceAllocator
116+
.connect(accounts.governor)
117+
['setTargetAllocation(address,uint256,uint256)'](addresses.target1, 0, 0)
118+
119+
// Both should be notified
120+
expect(await target1.notificationCount()).to.equal(1)
121+
expect(await defaultTarget.notificationCount()).to.equal(1)
122+
})
123+
124+
it('should notify default target even when it is address(0)', async () => {
125+
// Default is address(0) by default, which should handle notification gracefully
126+
expect(await issuanceAllocator.getTargetAt(0)).to.equal(ethers.ZeroAddress)
127+
128+
// Set allocation for target1 - should not revert even though default is address(0)
129+
await expect(
130+
issuanceAllocator
131+
.connect(accounts.governor)
132+
['setTargetAllocation(address,uint256)'](addresses.target1, ethers.parseEther('30')),
133+
).to.not.be.reverted
134+
135+
// Target1 should be notified
136+
expect(await target1.notificationCount()).to.equal(1)
137+
})
138+
139+
it('should notify correct targets when setting multiple allocations', async () => {
140+
// Set a non-zero default target
141+
await issuanceAllocator.connect(accounts.governor).setDefaultTarget(addresses.defaultTarget)
142+
await defaultTarget.resetNotificationCount()
143+
144+
// Set allocation for target1
145+
await issuanceAllocator
146+
.connect(accounts.governor)
147+
['setTargetAllocation(address,uint256)'](addresses.target1, ethers.parseEther('30'))
148+
149+
expect(await target1.notificationCount()).to.equal(1)
150+
expect(await target2.notificationCount()).to.equal(0)
151+
expect(await defaultTarget.notificationCount()).to.equal(1)
152+
153+
// Reset counters
154+
await target1.resetNotificationCount()
155+
await defaultTarget.resetNotificationCount()
156+
157+
// Set allocation for target2
158+
await issuanceAllocator
159+
.connect(accounts.governor)
160+
['setTargetAllocation(address,uint256)'](addresses.target2, ethers.parseEther('20'))
161+
162+
// Only target2 and default should be notified (not target1)
163+
expect(await target1.notificationCount()).to.equal(0)
164+
expect(await target2.notificationCount()).to.equal(1)
165+
expect(await defaultTarget.notificationCount()).to.equal(1)
166+
})
167+
168+
it('should emit NotificationReceived events for both targets', async () => {
169+
// Set a non-zero default target
170+
await issuanceAllocator.connect(accounts.governor).setDefaultTarget(addresses.defaultTarget)
171+
await defaultTarget.resetNotificationCount()
172+
173+
// Set allocation and check for events from both mock targets
174+
const tx = await issuanceAllocator
175+
.connect(accounts.governor)
176+
['setTargetAllocation(address,uint256)'](addresses.target1, ethers.parseEther('30'))
177+
178+
// Both targets should emit their NotificationReceived events
179+
await expect(tx).to.emit(target1, 'NotificationReceived')
180+
await expect(tx).to.emit(defaultTarget, 'NotificationReceived')
181+
})
182+
})
183+
184+
describe('setIssuancePerBlock notifications', () => {
185+
it('should notify only default target when changing issuance rate', async () => {
186+
// Set a non-zero default target
187+
await issuanceAllocator.connect(accounts.governor).setDefaultTarget(addresses.defaultTarget)
188+
189+
// Add a regular target
190+
await issuanceAllocator
191+
.connect(accounts.governor)
192+
['setTargetAllocation(address,uint256)'](addresses.target1, ethers.parseEther('30'))
193+
194+
// Reset counters
195+
await target1.resetNotificationCount()
196+
await defaultTarget.resetNotificationCount()
197+
198+
// Change issuance rate
199+
await issuanceAllocator.connect(accounts.governor).setIssuancePerBlock(ethers.parseEther('200'))
200+
201+
// Only default should be notified (regular targets keep same absolute rates)
202+
expect(await target1.notificationCount()).to.equal(0)
203+
expect(await defaultTarget.notificationCount()).to.equal(1)
204+
})
205+
})
206+
207+
describe('setDefaultTarget notifications', () => {
208+
it('should notify both old and new default targets', async () => {
209+
// Set first default target
210+
await issuanceAllocator.connect(accounts.governor).setDefaultTarget(addresses.target1)
211+
212+
// Reset counter
213+
await target1.resetNotificationCount()
214+
215+
// Change to new default target - should notify both
216+
await issuanceAllocator.connect(accounts.governor).setDefaultTarget(addresses.target2)
217+
218+
// Both old and new default should be notified
219+
expect(await target1.notificationCount()).to.equal(1)
220+
expect(await target2.notificationCount()).to.equal(1)
221+
})
222+
})
223+
224+
describe('notification deduplication', () => {
225+
it('should not notify target twice in the same block', async () => {
226+
// Set a non-zero default target
227+
await issuanceAllocator.connect(accounts.governor).setDefaultTarget(addresses.defaultTarget)
228+
await defaultTarget.resetNotificationCount()
229+
230+
// Try to set the same allocation twice in same block (second should be no-op)
231+
await issuanceAllocator
232+
.connect(accounts.governor)
233+
['setTargetAllocation(address,uint256)'](addresses.target1, ethers.parseEther('30'))
234+
235+
// Should only be notified once
236+
expect(await target1.notificationCount()).to.equal(1)
237+
expect(await defaultTarget.notificationCount()).to.equal(1)
238+
239+
// Second call with same values should not notify again (no change)
240+
await issuanceAllocator
241+
.connect(accounts.governor)
242+
['setTargetAllocation(address,uint256)'](addresses.target1, ethers.parseEther('30'))
243+
244+
// Counts should remain the same (no new notifications)
245+
expect(await target1.notificationCount()).to.equal(1)
246+
expect(await defaultTarget.notificationCount()).to.equal(1)
247+
})
248+
})
249+
})

0 commit comments

Comments
 (0)