Skip to content

Commit c5f845f

Browse files
committed
burn collector split
1 parent 70c8d3a commit c5f845f

2 files changed

Lines changed: 146 additions & 75 deletions

File tree

contracts/src/BurnCollector.sol

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,18 @@ contract BurnCollector {
1818
uint256 public minimumAmount;
1919
uint256 public callFee;
2020

21+
// Split accounting
22+
uint256 public otherBucketBalance;
23+
uint256 public burnShareBps; // Basis points (0-10000) for burn share
24+
2125
event SentToCelestia(uint256 amount, bytes32 recipient, bytes32 messageId);
2226
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
2327
event RecipientUpdated(uint32 destinationDomain, bytes32 recipientAddress);
2428
event MinimumAmountUpdated(uint256 minimumAmount);
2529
event CallFeeUpdated(uint256 callFee);
30+
event BurnShareUpdated(uint256 burnShareBps);
31+
event OtherWithdrawn(address indexed recipient, uint256 amount);
32+
event FundsSplit(uint256 totalNew, uint256 burnAmount, uint256 otherAmount);
2633

2734
modifier onlyOwner() {
2835
require(msg.sender == owner, "BurnCollector: caller is not the owner");
@@ -32,6 +39,7 @@ contract BurnCollector {
3239
constructor(address _hypNativeMinter, address _owner) {
3340
hypNativeMinter = IHypNativeMinter(_hypNativeMinter);
3441
owner = _owner;
42+
burnShareBps = 10000; // Default to 100% burn
3543
emit OwnershipTransferred(address(0), _owner);
3644
}
3745

@@ -40,16 +48,31 @@ contract BurnCollector {
4048
function sendToCelestia() external payable {
4149
require(msg.value >= callFee, "BurnCollector: insufficient fee");
4250

43-
uint256 balance = address(this).balance;
44-
require(balance >= minimumAmount, "BurnCollector: minimum amount not met");
51+
// Calculate new funds available for splitting
52+
// Total Balance - Already Allocated to Other Bucket
53+
uint256 currentBalance = address(this).balance;
54+
require(currentBalance >= otherBucketBalance, "BurnCollector: accounting error");
55+
56+
uint256 newFunds = currentBalance - otherBucketBalance;
57+
58+
// Calculate split
59+
uint256 burnAmount = (newFunds * burnShareBps) / 10000;
60+
uint256 otherAmount = newFunds - burnAmount;
4561

46-
bytes32 messageId = hypNativeMinter.transferRemote{value: balance}(
62+
require(burnAmount >= minimumAmount, "BurnCollector: minimum amount not met");
63+
64+
// Update accounting
65+
otherBucketBalance += otherAmount;
66+
emit FundsSplit(newFunds, burnAmount, otherAmount);
67+
68+
// Bridge the burn amount
69+
bytes32 messageId = hypNativeMinter.transferRemote{value: burnAmount}(
4770
destinationDomain,
4871
recipientAddress,
49-
balance
72+
burnAmount
5073
);
5174

52-
emit SentToCelestia(balance, recipientAddress, messageId);
75+
emit SentToCelestia(burnAmount, recipientAddress, messageId);
5376
}
5477

5578
// Admin functions
@@ -75,4 +98,20 @@ contract BurnCollector {
7598
callFee = _callFee;
7699
emit CallFeeUpdated(_callFee);
77100
}
101+
102+
function setBurnShare(uint256 _burnShareBps) external onlyOwner {
103+
require(_burnShareBps <= 10000, "BurnCollector: invalid bps");
104+
burnShareBps = _burnShareBps;
105+
emit BurnShareUpdated(_burnShareBps);
106+
}
107+
108+
function withdrawOther(address payable _recipient, uint256 _amount) external onlyOwner {
109+
require(_amount <= otherBucketBalance, "BurnCollector: insufficient other balance");
110+
otherBucketBalance -= _amount;
111+
112+
(bool success, ) = _recipient.call{value: _amount}("");
113+
require(success, "BurnCollector: transfer failed");
114+
115+
emit OtherWithdrawn(_recipient, _amount);
116+
}
78117
}

contracts/test/BurnCollector.t.sol

Lines changed: 102 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ contract BurnCollectorTest is Test {
3939
burnCollector.setRecipient(destination, recipient);
4040
burnCollector.setMinimumAmount(minAmount);
4141
burnCollector.setCallFee(fee);
42+
// Default burn share is 10000 (100%)
4243
}
4344

4445
function test_Receive() public {
@@ -48,108 +49,139 @@ contract BurnCollectorTest is Test {
4849
assertEq(address(burnCollector).balance, amount, "Balance mismatch");
4950
}
5051

51-
function test_SendToCelestia() public {
52-
// Fund the collector with enough to meet minAmount (excluding fee which is added on top)
53-
// Actually, the check is address(this).balance >= minAmount.
54-
// If we send fee, it adds to balance.
55-
// Let's fund it with minAmount first.
52+
function test_SendToCelestia_100PercentBurn() public {
53+
// Fund with minAmount
5654
(bool success, ) = address(burnCollector).call{value: minAmount}("");
57-
require(success, "Funding failed");
55+
require(success);
5856

59-
// Expect the call to the mock minter
60-
// Total sent = existing balance (minAmount) + fee sent in call
6157
uint256 totalAmount = minAmount + fee;
6258

6359
vm.expectEmit(true, true, true, true, address(mockMinter));
6460
emit MockHypNativeMinter.TransferRemoteCalled(destination, recipient, totalAmount);
6561

66-
// Expect the event from BurnCollector
67-
vm.expectEmit(true, true, true, true, address(burnCollector));
68-
emit BurnCollector.SentToCelestia(totalAmount, recipient, bytes32(uint256(1)));
69-
70-
// Call as user with fee
7162
vm.prank(user);
7263
vm.deal(user, fee);
7364
burnCollector.sendToCelestia{value: fee}();
7465

7566
assertEq(address(burnCollector).balance, 0, "Collector should be empty");
76-
assertEq(address(mockMinter).balance, totalAmount, "Minter should have received funds");
67+
assertEq(burnCollector.otherBucketBalance(), 0, "Other bucket should be empty");
7768
}
7869

79-
function test_SendToCelestia_InsufficientFee() public {
70+
function test_SendToCelestia_Split5050() public {
71+
// Set split to 50%
72+
burnCollector.setBurnShare(5000);
73+
74+
// Fund with 2 ether.
75+
// Fee is 0.1 ether.
76+
// Total new funds = 2.1 ether.
77+
// Burn = 1.05 ether. Other = 1.05 ether.
78+
// Min amount is 1 ether, so 1.05 >= 1.0 is OK.
79+
uint256 fundAmount = 2 ether;
80+
(bool success, ) = address(burnCollector).call{value: fundAmount}("");
81+
require(success);
82+
83+
uint256 totalNew = fundAmount + fee;
84+
uint256 expectedBurn = totalNew / 2;
85+
uint256 expectedOther = totalNew - expectedBurn;
86+
87+
vm.expectEmit(true, true, true, true, address(mockMinter));
88+
emit MockHypNativeMinter.TransferRemoteCalled(destination, recipient, expectedBurn);
89+
8090
vm.prank(user);
8191
vm.deal(user, fee);
82-
// Send less than fee
83-
vm.expectRevert("BurnCollector: insufficient fee");
84-
burnCollector.sendToCelestia{value: fee - 1}();
92+
burnCollector.sendToCelestia{value: fee}();
93+
94+
assertEq(address(burnCollector).balance, expectedOther, "Collector should hold other funds");
95+
assertEq(burnCollector.otherBucketBalance(), expectedOther, "Other bucket accounting incorrect");
8596
}
8697

87-
function test_SendToCelestia_BelowMinAmount() public {
88-
// Fund with less than minAmount
89-
uint256 amount = minAmount - 0.5 ether;
90-
(bool success, ) = address(burnCollector).call{value: amount}("");
91-
require(success, "Funding failed");
98+
function test_SendToCelestia_AccumulateOther() public {
99+
burnCollector.setBurnShare(5000); // 50%
92100

93-
// Even with fee, if logic checks total balance, we need to be careful.
94-
// Logic: require(address(this).balance >= minimumAmount)
95-
// If we send fee, balance increases.
96-
// Let's assume we want to test when TOTAL balance is still low.
97-
// If minAmount is 1 ether. Funding is 0.5. Fee is 0.1. Total 0.6 < 1.0.
101+
// First call: 2 ether + 0.1 fee = 2.1 total. 1.05 burn, 1.05 other.
102+
(bool success, ) = address(burnCollector).call{value: 2 ether}("");
103+
require(success);
98104

99-
// Reset to clean state for clarity
100-
burnCollector = new BurnCollector(address(mockMinter), owner);
101-
burnCollector.setRecipient(destination, recipient);
102-
burnCollector.setMinimumAmount(1 ether);
103-
burnCollector.setCallFee(0.1 ether);
105+
vm.prank(user);
106+
vm.deal(user, fee);
107+
burnCollector.sendToCelestia{value: fee}();
108+
109+
uint256 firstOther = 1.05 ether;
110+
assertEq(burnCollector.otherBucketBalance(), firstOther);
104111

105-
(success, ) = address(burnCollector).call{value: 0.5 ether}("");
112+
// Second call: 2 ether + 0.1 fee = 2.1 total NEW funds.
113+
// Previous balance: 1.05. New balance before split: 1.05 + 2.1 = 3.15.
114+
// Logic: newFunds = balance (3.15) - otherBucket (1.05) = 2.1. Correct.
115+
(success, ) = address(burnCollector).call{value: 2 ether}("");
106116
require(success);
107117

108118
vm.prank(user);
109-
vm.deal(user, 1 ether);
110-
vm.expectRevert("BurnCollector: minimum amount not met");
111-
burnCollector.sendToCelestia{value: 0.1 ether}();
112-
}
119+
vm.deal(user, fee);
120+
burnCollector.sendToCelestia{value: fee}();
113121

114-
function test_AdminFunctions() public {
115-
// Test setRecipient
116-
burnCollector.setRecipient(5678, bytes32(uint256(0xbeef)));
117-
assertEq(burnCollector.destinationDomain(), 5678);
118-
assertEq(burnCollector.recipientAddress(), bytes32(uint256(0xbeef)));
119-
120-
// Test setMinimumAmount
121-
burnCollector.setMinimumAmount(5 ether);
122-
assertEq(burnCollector.minimumAmount(), 5 ether);
123-
124-
// Test setCallFee
125-
burnCollector.setCallFee(1 ether);
126-
assertEq(burnCollector.callFee(), 1 ether);
127-
128-
// Test transferOwnership
129-
address newOwner = address(0x2);
130-
burnCollector.transferOwnership(newOwner);
131-
assertEq(burnCollector.owner(), newOwner);
132-
133-
// Old owner cannot call anymore
134-
vm.expectRevert("BurnCollector: caller is not the owner");
135-
burnCollector.setCallFee(2 ether);
122+
uint256 secondOther = 1.05 ether;
123+
assertEq(burnCollector.otherBucketBalance(), firstOther + secondOther);
124+
assertEq(address(burnCollector).balance, firstOther + secondOther);
136125
}
137126

138-
function test_AdminAccessControl() public {
139-
vm.prank(user);
140-
vm.expectRevert("BurnCollector: caller is not the owner");
141-
burnCollector.setRecipient(1, bytes32(0));
127+
function test_WithdrawOther() public {
128+
burnCollector.setBurnShare(5000);
129+
(bool success, ) = address(burnCollector).call{value: 2 ether}("");
130+
require(success);
142131

143132
vm.prank(user);
144-
vm.expectRevert("BurnCollector: caller is not the owner");
145-
burnCollector.setMinimumAmount(1);
133+
vm.deal(user, fee);
134+
burnCollector.sendToCelestia{value: fee}();
135+
136+
uint256 otherAmount = 1.05 ether;
137+
assertEq(burnCollector.otherBucketBalance(), otherAmount);
138+
139+
// Withdraw half
140+
uint256 withdrawAmount = 0.5 ether;
141+
address payable recipientAddr = payable(address(0x99));
142+
143+
burnCollector.withdrawOther(recipientAddr, withdrawAmount);
144+
145+
assertEq(burnCollector.otherBucketBalance(), otherAmount - withdrawAmount);
146+
assertEq(recipientAddr.balance, withdrawAmount);
147+
assertEq(address(burnCollector).balance, otherAmount - withdrawAmount);
148+
}
146149

150+
function test_WithdrawOther_Insufficient() public {
151+
burnCollector.setBurnShare(5000);
152+
(bool success, ) = address(burnCollector).call{value: 2 ether}("");
153+
require(success);
154+
147155
vm.prank(user);
148-
vm.expectRevert("BurnCollector: caller is not the owner");
149-
burnCollector.setCallFee(1);
156+
vm.deal(user, fee);
157+
burnCollector.sendToCelestia{value: fee}();
158+
159+
uint256 otherAmount = 1.05 ether;
160+
161+
vm.expectRevert("BurnCollector: insufficient other balance");
162+
burnCollector.withdrawOther(payable(owner), otherAmount + 1);
163+
}
164+
165+
function test_SendToCelestia_BelowMinAmount_AfterSplit() public {
166+
burnCollector.setBurnShare(1000); // 10% burn
167+
168+
// Fund with 2 ether. Total 2.1.
169+
// Burn = 0.21. Other = 1.89.
170+
// Min amount is 1.0. 0.21 < 1.0. Should revert.
171+
(bool success, ) = address(burnCollector).call{value: 2 ether}("");
172+
require(success);
150173

151174
vm.prank(user);
152-
vm.expectRevert("BurnCollector: caller is not the owner");
153-
burnCollector.transferOwnership(user);
175+
vm.deal(user, fee);
176+
vm.expectRevert("BurnCollector: minimum amount not met");
177+
burnCollector.sendToCelestia{value: fee}();
178+
}
179+
180+
function test_AdminFunctions() public {
181+
burnCollector.setBurnShare(5000);
182+
assertEq(burnCollector.burnShareBps(), 5000);
183+
184+
vm.expectRevert("BurnCollector: invalid bps");
185+
burnCollector.setBurnShare(10001);
154186
}
155187
}

0 commit comments

Comments
 (0)