Skip to content

Commit 435b5db

Browse files
authored
fix: enable native LP deposits with payable overload (#7147)
1 parent 1d46a82 commit 435b5db

File tree

3 files changed

+238
-2
lines changed

3 files changed

+238
-2
lines changed

solidity/contracts/token/HypNative.sol

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
// SPDX-License-Identifier: Apache-2.0
22
pragma solidity >=0.8.0;
33

4+
// ============ Internal Imports ============
45
import {LpCollateralRouter} from "./libs/LpCollateralRouter.sol";
56
import {Quote, ITokenBridge} from "../interfaces/ITokenBridge.sol";
67
import {NativeCollateral} from "./libs/TokenCollateral.sol";
78
import {TokenRouter} from "./libs/TokenRouter.sol";
89

10+
// ============ External Imports ============
11+
import {ERC4626Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
912
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
1013

1114
/**
@@ -36,10 +39,20 @@ contract HypNative is LpCollateralRouter {
3639
_LpCollateralRouter_initialize();
3740
}
3841

42+
/**
43+
* Replacement for ERC4626Upgradeable.deposit that allows for native token deposits.
44+
* @dev msg.value will be used as the amount to deposit.
45+
* @param receiver The address to deposit the native token to.
46+
* @return shares The number of shares minted.
47+
*/
48+
function deposit(address receiver) public payable returns (uint256 shares) {
49+
return ERC4626Upgradeable.deposit(msg.value, receiver);
50+
}
51+
3952
/**
4053
* @inheritdoc TokenRouter
4154
*/
42-
function token() public view override returns (address) {
55+
function token() public pure override returns (address) {
4356
return address(0);
4457
}
4558

solidity/contracts/token/libs/LpCollateralRouter.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ abstract contract LpCollateralRouter is
9292
}
9393

9494
// can be used to distribute rewards to LPs pro rata
95-
function donate(uint256 amount) public {
95+
function donate(uint256 amount) public payable {
9696
// checks
9797
_transferFromSender(amount);
9898

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >=0.8.0;
3+
4+
import "forge-std/Test.sol";
5+
import {HypNative} from "../../contracts/token/HypNative.sol";
6+
import {MockMailbox} from "../../contracts/mock/MockMailbox.sol";
7+
8+
contract HypNativeLpTest is Test {
9+
event Donation(address sender, uint256 amount);
10+
event Deposit(
11+
address indexed sender,
12+
address indexed owner,
13+
uint256 assets,
14+
uint256 shares
15+
);
16+
event Withdraw(
17+
address indexed sender,
18+
address indexed receiver,
19+
address indexed owner,
20+
uint256 assets,
21+
uint256 shares
22+
);
23+
24+
HypNative internal router;
25+
address internal alice = address(0x1);
26+
address internal bob = address(0x2);
27+
uint256 internal constant DEPOSIT_AMOUNT = 100e18;
28+
uint256 internal constant DONATE_AMOUNT = 50e18;
29+
30+
function setUp() public {
31+
MockMailbox mailbox = new MockMailbox(1);
32+
router = new HypNative(1, address(mailbox));
33+
router.initialize(address(0), address(0), address(this));
34+
35+
vm.label(alice, "Alice");
36+
vm.label(bob, "Bob");
37+
vm.deal(alice, 1000e18);
38+
vm.deal(bob, 1000e18);
39+
}
40+
41+
function testDepositIncreasesBalances() public {
42+
uint256 shares = router.previewDeposit(DEPOSIT_AMOUNT);
43+
vm.prank(alice);
44+
router.deposit{value: DEPOSIT_AMOUNT}(alice);
45+
assertEq(router.balanceOf(alice), shares);
46+
assertEq(router.totalAssets(), DEPOSIT_AMOUNT);
47+
}
48+
49+
function testDepositEmitsEvent() public {
50+
uint256 shares = router.previewDeposit(DEPOSIT_AMOUNT);
51+
vm.expectEmit(true, true, true, true);
52+
emit Deposit(alice, alice, DEPOSIT_AMOUNT, shares);
53+
vm.prank(alice);
54+
router.deposit{value: DEPOSIT_AMOUNT}(alice);
55+
}
56+
57+
function testDepositWithZeroValue() public {
58+
vm.prank(alice);
59+
uint256 shares = router.deposit(alice);
60+
assertEq(shares, 0);
61+
assertEq(router.balanceOf(alice), 0);
62+
}
63+
64+
function testDepositToReceiverCreditsCorrectAccount() public {
65+
uint256 shares = router.previewDeposit(DEPOSIT_AMOUNT);
66+
vm.prank(alice);
67+
router.deposit{value: DEPOSIT_AMOUNT}(bob);
68+
assertEq(router.balanceOf(bob), shares);
69+
assertEq(router.balanceOf(alice), 0);
70+
}
71+
72+
function testWithdrawDecreasesBalances() public {
73+
uint256 shares = router.previewDeposit(DEPOSIT_AMOUNT);
74+
vm.prank(alice);
75+
router.deposit{value: DEPOSIT_AMOUNT}(alice);
76+
vm.prank(alice);
77+
router.withdraw(DEPOSIT_AMOUNT, bob, alice);
78+
assertEq(router.balanceOf(alice), 0);
79+
assertEq(router.totalAssets(), 0);
80+
assertEq(bob.balance, 1000e18 + DEPOSIT_AMOUNT);
81+
}
82+
83+
function testWithdrawEmitsEvent() public {
84+
vm.prank(alice);
85+
router.deposit{value: DEPOSIT_AMOUNT}(alice);
86+
uint256 shares = router.balanceOf(alice);
87+
vm.expectEmit(true, true, true, true);
88+
emit Withdraw(alice, bob, alice, DEPOSIT_AMOUNT, shares);
89+
vm.prank(alice);
90+
router.withdraw(DEPOSIT_AMOUNT, bob, alice);
91+
}
92+
93+
function testTotalSupplyTracksShares() public {
94+
assertEq(router.totalSupply(), 0);
95+
vm.prank(alice);
96+
router.deposit{value: DEPOSIT_AMOUNT}(alice);
97+
assertEq(router.totalSupply(), router.balanceOf(alice));
98+
}
99+
100+
function testTotalAssetsTracksDepositsAndWithdrawals() public {
101+
assertEq(router.totalAssets(), 0);
102+
vm.prank(alice);
103+
router.deposit{value: DEPOSIT_AMOUNT}(alice);
104+
assertEq(router.totalAssets(), DEPOSIT_AMOUNT);
105+
vm.prank(alice);
106+
router.withdraw(DEPOSIT_AMOUNT, bob, alice);
107+
assertEq(router.totalAssets(), 0);
108+
}
109+
110+
function testDonateIncreasesTotalAssets() public {
111+
assertEq(router.totalAssets(), 0);
112+
vm.prank(alice);
113+
router.donate{value: DONATE_AMOUNT}(DONATE_AMOUNT);
114+
assertEq(router.totalAssets(), DONATE_AMOUNT);
115+
}
116+
117+
function testDonateEmitsEvent() public {
118+
vm.expectEmit(true, true, true, true);
119+
emit Donation(alice, DONATE_AMOUNT);
120+
vm.prank(alice);
121+
router.donate{value: DONATE_AMOUNT}(DONATE_AMOUNT);
122+
}
123+
124+
function testDonateIsNotWithdrawable() public {
125+
vm.prank(alice);
126+
router.donate{value: DONATE_AMOUNT}(DONATE_AMOUNT);
127+
vm.prank(alice);
128+
vm.expectRevert();
129+
router.withdraw(DONATE_AMOUNT, bob, alice);
130+
}
131+
132+
function testWithdrawMoreThanBalanceReverts() public {
133+
vm.prank(alice);
134+
router.deposit{value: DEPOSIT_AMOUNT}(alice);
135+
vm.prank(alice);
136+
vm.expectRevert();
137+
router.withdraw(DEPOSIT_AMOUNT + 1, bob, alice);
138+
}
139+
140+
function testDonateDistributesToAllHolders() public {
141+
uint256 aliceDeposit = 100e18;
142+
uint256 bobDeposit = 200e18;
143+
uint256 donation = DONATE_AMOUNT;
144+
145+
// Alice deposits
146+
vm.prank(alice);
147+
router.deposit{value: aliceDeposit}(alice);
148+
149+
// Bob deposits
150+
vm.prank(bob);
151+
router.deposit{value: bobDeposit}(bob);
152+
153+
// Record balances before donation
154+
uint256 aliceWithdrawBefore = router.maxWithdraw(alice);
155+
uint256 bobWithdrawBefore = router.maxWithdraw(bob);
156+
157+
// Donate to the vault
158+
vm.deal(address(this), donation);
159+
router.donate{value: donation}(donation);
160+
161+
// After donation, both should be able to withdraw more
162+
assertGt(router.maxWithdraw(alice), aliceWithdrawBefore);
163+
assertGt(router.maxWithdraw(bob), bobWithdrawBefore);
164+
165+
// Alice should get 1/3 of donation, Bob should get 2/3
166+
assertEq(router.maxWithdraw(alice), aliceDeposit + donation / 3);
167+
assertEq(router.maxWithdraw(bob), bobDeposit + (donation * 2) / 3);
168+
}
169+
170+
function testReceiveCallsDonate() public {
171+
assertEq(router.totalAssets(), 0);
172+
vm.expectEmit(true, true, true, true);
173+
emit Donation(alice, DONATE_AMOUNT);
174+
vm.prank(alice);
175+
(bool success, ) = address(router).call{value: DONATE_AMOUNT}("");
176+
assertTrue(success);
177+
assertEq(router.totalAssets(), DONATE_AMOUNT);
178+
}
179+
180+
function testMultipleDepositsAndWithdrawals() public {
181+
// Alice deposits
182+
vm.prank(alice);
183+
uint256 aliceShares = router.deposit{value: DEPOSIT_AMOUNT}(alice);
184+
185+
// Bob deposits
186+
vm.prank(bob);
187+
uint256 bobShares = router.deposit{value: DEPOSIT_AMOUNT * 2}(bob);
188+
189+
assertEq(router.totalAssets(), DEPOSIT_AMOUNT * 3);
190+
assertEq(router.totalSupply(), aliceShares + bobShares);
191+
192+
// Alice withdraws half
193+
vm.prank(alice);
194+
router.withdraw(DEPOSIT_AMOUNT / 2, alice, alice);
195+
196+
assertEq(router.totalAssets(), DEPOSIT_AMOUNT * 3 - DEPOSIT_AMOUNT / 2);
197+
assertEq(alice.balance, 1000e18 - DEPOSIT_AMOUNT + DEPOSIT_AMOUNT / 2);
198+
199+
// Bob withdraws all
200+
uint256 bobMaxWithdraw = router.maxWithdraw(bob);
201+
vm.prank(bob);
202+
router.withdraw(bobMaxWithdraw, bob, bob);
203+
204+
assertEq(bob.balance, 1000e18);
205+
}
206+
207+
function testDepositAfterDonationGetsCorrectShares() public {
208+
// Alice deposits initially
209+
vm.prank(alice);
210+
uint256 aliceShares = router.deposit{value: DEPOSIT_AMOUNT}(alice);
211+
212+
// Someone donates
213+
vm.deal(address(this), DONATE_AMOUNT);
214+
router.donate{value: DONATE_AMOUNT}(DONATE_AMOUNT);
215+
216+
// Bob deposits same amount
217+
vm.prank(bob);
218+
uint256 bobShares = router.deposit{value: DEPOSIT_AMOUNT}(bob);
219+
220+
// Bob should get fewer shares since totalAssets increased from donation
221+
assertLt(bobShares, aliceShares);
222+
}
223+
}

0 commit comments

Comments
 (0)