Skip to content

Commit c2a953d

Browse files
committed
feat: implement dynamic lifetime fee and withdrawal functionality for deployer
1 parent 558ff91 commit c2a953d

File tree

2 files changed

+104
-6
lines changed

2 files changed

+104
-6
lines changed

contracts/src/FeeGate.sol

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ contract FeeGate is ISchemaResolver, EIP712, ReentrancyGuard {
3636
using ECDSA for bytes32;
3737

3838
// Constants
39-
uint256 public constant LIFETIME_FEE = 100 ether; // 100 GLMR
4039
uint256 public constant FEE_THRESHOLD = 3; // Fee charged on 3rd attestation
40+
41+
// Mutable state
42+
uint256 public lifetimeFee = 100 ether; // 100 GLMR (can be updated by deployer)
4143
bytes32 public constant ATTESTATION_TYPEHASH =
4244
keccak256(
4345
'Attestation(address issuer,address recipient,bytes32 refUID,bool revocable,uint64 expirationTime,'
@@ -48,6 +50,7 @@ contract FeeGate is ISchemaResolver, EIP712, ReentrancyGuard {
4850
// Immutable state
4951
IEAS public immutable eas;
5052
bytes32 public immutable schemaUID;
53+
address public immutable deployer;
5154

5255
// Per-issuer state
5356
mapping(address => uint256) public attestCount;
@@ -62,6 +65,7 @@ contract FeeGate is ISchemaResolver, EIP712, ReentrancyGuard {
6265
event FeeCharged(address indexed issuer, uint256 amount, uint256 count);
6366
event NonceIncremented(address indexed issuer, uint256 newNonce);
6467
event LastUIDAnchorSet(address indexed issuer, string cubidId, bytes32 uid);
68+
event FeeUpdated(uint256 oldFee, uint256 newFee);
6569

6670
// Errors
6771
error InvalidNonce();
@@ -73,6 +77,9 @@ contract FeeGate is ISchemaResolver, EIP712, ReentrancyGuard {
7377
error InvalidRecipient();
7478
error InvalidIssuer();
7579
error NotImplemented();
80+
error OnlyDeployer();
81+
error WithdrawalFailed();
82+
error InvalidFee();
7683

7784
/**
7885
* @param _eas Address of the EAS contract
@@ -82,6 +89,7 @@ contract FeeGate is ISchemaResolver, EIP712, ReentrancyGuard {
8289
if (address(_eas) == address(0)) revert InvalidEAS();
8390
eas = _eas;
8491
schemaUID = _schemaUID;
92+
deployer = msg.sender;
8593
}
8694

8795
/**
@@ -290,6 +298,32 @@ contract FeeGate is ISchemaResolver, EIP712, ReentrancyGuard {
290298
return '1.0.0';
291299
}
292300

301+
/**
302+
* @notice Withdraw all accumulated fees to the deployer
303+
* @dev Only callable by the deployer address
304+
*/
305+
function withdraw() external nonReentrant {
306+
if (msg.sender != deployer) revert OnlyDeployer();
307+
308+
uint256 balance = address(this).balance;
309+
(bool success, ) = deployer.call{value: balance}('');
310+
if (!success) revert WithdrawalFailed();
311+
}
312+
313+
/**
314+
* @notice Update the lifetime fee amount
315+
* @dev Only callable by the deployer address
316+
* @param newFee The new lifetime fee amount in wei
317+
*/
318+
function setLifetimeFee(uint256 newFee) external {
319+
if (msg.sender != deployer) revert OnlyDeployer();
320+
if (newFee == 0) revert InvalidFee();
321+
322+
uint256 oldFee = lifetimeFee;
323+
lifetimeFee = newFee;
324+
emit FeeUpdated(oldFee, newFee);
325+
}
326+
293327
receive() external payable {}
294328

295329
// Internal helpers
@@ -327,7 +361,7 @@ contract FeeGate is ISchemaResolver, EIP712, ReentrancyGuard {
327361
if (newCount < FEE_THRESHOLD) {
328362
if (supplied != 0) revert UnexpectedValue();
329363
} else if (newCount == FEE_THRESHOLD) {
330-
if (supplied != LIFETIME_FEE) revert InsufficientFee();
364+
if (supplied != lifetimeFee) revert InsufficientFee();
331365
} else {
332366
revert InsufficientFee();
333367
}
@@ -346,7 +380,7 @@ contract FeeGate is ISchemaResolver, EIP712, ReentrancyGuard {
346380
bool paid = lifetimeFeePaid[issuer];
347381
if (!paid && newCount == FEE_THRESHOLD) {
348382
lifetimeFeePaid[issuer] = true;
349-
emit FeeCharged(issuer, LIFETIME_FEE, newCount);
383+
emit FeeCharged(issuer, lifetimeFee, newCount);
350384
}
351385
}
352386
}

contracts/test/FeeGate.t.sol

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ contract FeeGateTest is Test {
1919
address public issuer = vm.addr(ISSUER_KEY);
2020
address public recipient = address(0xBEEF);
2121

22+
receive() external payable {}
23+
2224
string constant SCHEMA =
2325
'string cubidId,uint8 trustLevel,bool human,bytes32 circle,uint64 issuedAt,uint64 expiry,uint256 nonce';
2426

@@ -113,13 +115,13 @@ contract FeeGateTest is Test {
113115
vm.stopPrank();
114116

115117
vm.startPrank(issuer);
116-
bytes32 uid = feeGate.attestDirect{value: feeGate.LIFETIME_FEE()}(_payload('cubidC', 6));
118+
bytes32 uid = feeGate.attestDirect{value: feeGate.lifetimeFee()}(_payload('cubidC', 6));
117119
vm.stopPrank();
118120

119121
assertEq(feeGate.attestCount(issuer), 3);
120122
assertTrue(feeGate.hasPaidFee(issuer));
121123
assertEq(feeGate.getLastUID(issuer, 'cubidC'), uid);
122-
assertEq(address(feeGate).balance, feeGate.LIFETIME_FEE());
124+
assertEq(address(feeGate).balance, feeGate.lifetimeFee());
123125

124126
// fourth attestation requires no additional fee
125127
AttestationPayload memory payloadFourth = _payload('cubidD', 7);
@@ -136,7 +138,7 @@ contract FeeGateTest is Test {
136138

137139
assertEq(feeGate.attestCount(issuer), 4);
138140
assertTrue(feeGate.hasPaidFee(issuer));
139-
assertEq(address(feeGate).balance, feeGate.LIFETIME_FEE());
141+
assertEq(address(feeGate).balance, feeGate.lifetimeFee());
140142
}
141143

142144
function testInsufficientFeeDelegatedReverts() public {
@@ -169,6 +171,68 @@ contract FeeGateTest is Test {
169171
assertEq(feeGate.getLastUID(issuer, 'cubidX'), latestUID);
170172
}
171173

174+
function testSetLifetimeFeeByDeployer() public {
175+
uint256 oldFee = feeGate.lifetimeFee();
176+
assertEq(oldFee, 100 ether);
177+
178+
uint256 newFee = 50 ether;
179+
feeGate.setLifetimeFee(newFee);
180+
181+
assertEq(feeGate.lifetimeFee(), newFee);
182+
183+
// Verify new fee is enforced
184+
vm.startPrank(issuer);
185+
feeGate.attestDirect(_payload('cubid1', 5));
186+
feeGate.attestDirect(_payload('cubid2', 5));
187+
188+
// Third attestation should now require the new fee amount
189+
vm.expectRevert(FeeGate.InsufficientFee.selector);
190+
feeGate.attestDirect{value: oldFee}(_payload('cubid3', 6));
191+
192+
// Should succeed with new fee
193+
feeGate.attestDirect{value: newFee}(_payload('cubid3', 6));
194+
vm.stopPrank();
195+
196+
assertTrue(feeGate.hasPaidFee(issuer));
197+
assertEq(address(feeGate).balance, newFee);
198+
}
199+
200+
function testSetLifetimeFeeRevertsForNonDeployer() public {
201+
vm.prank(issuer);
202+
vm.expectRevert(FeeGate.OnlyDeployer.selector);
203+
feeGate.setLifetimeFee(50 ether);
204+
}
205+
206+
function testSetLifetimeFeeRevertsForZero() public {
207+
vm.expectRevert(FeeGate.InvalidFee.selector);
208+
feeGate.setLifetimeFee(0);
209+
}
210+
211+
function testWithdrawByDeployer() public {
212+
// Setup: get issuer to pay fee
213+
vm.startPrank(issuer);
214+
feeGate.attestDirect(_payload('cubid1', 5));
215+
feeGate.attestDirect(_payload('cubid2', 5));
216+
feeGate.attestDirect{value: feeGate.lifetimeFee()}(_payload('cubid3', 6));
217+
vm.stopPrank();
218+
219+
uint256 contractBalance = address(feeGate).balance;
220+
assertEq(contractBalance, 100 ether);
221+
222+
uint256 deployerBalanceBefore = address(this).balance;
223+
feeGate.withdraw();
224+
uint256 deployerBalanceAfter = address(this).balance;
225+
226+
assertEq(address(feeGate).balance, 0);
227+
assertEq(deployerBalanceAfter - deployerBalanceBefore, contractBalance);
228+
}
229+
230+
function testWithdrawRevertsForNonDeployer() public {
231+
vm.prank(issuer);
232+
vm.expectRevert(FeeGate.OnlyDeployer.selector);
233+
feeGate.withdraw();
234+
}
235+
172236
function _payload(
173237
string memory cubidId,
174238
uint8 trustLevel

0 commit comments

Comments
 (0)