Skip to content

Commit f3f416c

Browse files
Thegaramzimpha
andauthored
feat: validium contracts and fast withdraw vault (#140)
Co-authored-by: Xi Lin <[email protected]>
1 parent 67c1bde commit f3f416c

16 files changed

+2350
-1
lines changed

src/L1/L1ScrollMessenger.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ contract L1ScrollMessenger is ScrollMessengerBase, IL1ScrollMessenger {
289289
bytes memory _message,
290290
uint256 _gasLimit,
291291
address _refundAddress
292-
) internal nonReentrant {
292+
) internal virtual nonReentrant {
293293
// compute the actual cross domain message calldata.
294294
uint256 _messageNonce = IL1MessageQueueV2(messageQueueV2).nextCrossDomainMessageIndex();
295295
bytes memory _xDomainCalldata = _encodeXDomainCalldata(_msgSender(), _to, _value, _messageNonce, _message);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity =0.8.24;
4+
5+
import {IL1MessageQueueV2} from "../L1/rollup/IL1MessageQueueV2.sol";
6+
7+
import {ScrollChainValidium} from "../validium/ScrollChainValidium.sol";
8+
9+
contract ScrollChainValidiumMock is ScrollChainValidium {
10+
constructor(
11+
uint64 _chainId,
12+
address _messageQueueV2,
13+
address _verifier
14+
) ScrollChainValidium(_chainId, _messageQueueV2, _verifier) {}
15+
16+
/// @dev Internal function to finalize a bundle.
17+
/// @param batchHeader The header of the last batch in this bundle.
18+
/// @param totalL1MessagesPoppedOverall The number of messages processed after this bundle.
19+
function _finalizeBundle(
20+
bytes calldata batchHeader,
21+
uint256 totalL1MessagesPoppedOverall,
22+
bytes calldata
23+
) internal virtual override {
24+
// actions before verification
25+
(, bytes32 batchHash, uint256 batchIndex, ) = _beforeFinalizeBatch(batchHeader);
26+
27+
bytes32 postStateRoot = stateRoots[batchIndex];
28+
bytes32 withdrawRoot = withdrawRoots[batchIndex];
29+
30+
// actions after verification
31+
_afterFinalizeBatch(batchIndex, batchHash, totalL1MessagesPoppedOverall, postStateRoot, withdrawRoot);
32+
}
33+
}
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity =0.8.24;
4+
5+
import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol";
6+
7+
import {StringsUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol";
8+
import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
9+
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
10+
11+
import {IL1ERC20Gateway} from "../../L1/gateways/IL1ERC20Gateway.sol";
12+
import {WrappedEther} from "../../L2/predeploys/WrappedEther.sol";
13+
import {L2StandardERC20Gateway} from "../../L2/gateways/L2StandardERC20Gateway.sol";
14+
import {ScrollStandardERC20} from "../../libraries/token/ScrollStandardERC20.sol";
15+
import {ScrollStandardERC20Factory} from "../../libraries/token/ScrollStandardERC20Factory.sol";
16+
import {IWETH} from "../../interfaces/IWETH.sol";
17+
import {FastWithdrawVault} from "../../validium/FastWithdrawVault.sol";
18+
import {L1ERC20GatewayValidium} from "../../validium/L1ERC20GatewayValidium.sol";
19+
20+
import {ValidiumTestBase} from "./ValidiumTestBase.t.sol";
21+
22+
// Helper contract to access private functions
23+
contract FastWithdrawVaultHelper is FastWithdrawVault {
24+
constructor(address _weth, address _gateway) FastWithdrawVault(_weth, _gateway) {}
25+
26+
function getWithdrawTypehash() public pure returns (bytes32) {
27+
return keccak256("Withdraw(address l1Token,address l2Token,address to,uint256 amount,bytes32 messageHash)");
28+
}
29+
30+
function hashTypedDataV4(bytes32 structHash) public view returns (bytes32) {
31+
return _hashTypedDataV4(structHash);
32+
}
33+
}
34+
35+
contract FastWithdrawVaultTest is ValidiumTestBase {
36+
event Withdraw(address indexed l1Token, address indexed l2Token, address to, uint256 amount, bytes32 messageHash);
37+
38+
L1ERC20GatewayValidium private gateway;
39+
40+
ScrollStandardERC20 private template;
41+
ScrollStandardERC20Factory private factory;
42+
L2StandardERC20Gateway private counterpartGateway;
43+
44+
FastWithdrawVaultHelper private vault;
45+
MockERC20 private l1Token;
46+
WrappedEther private weth;
47+
MockERC20 private l2Token;
48+
49+
address private vaultAdmin;
50+
51+
uint256 private sequencerPrivateKey;
52+
address private sequencer;
53+
54+
uint256 private userPrivateKey;
55+
address private user;
56+
57+
function setUp() public {
58+
__ValidiumTestBase_setUp(1233);
59+
60+
// Setup addresses and keys
61+
vaultAdmin = address(this);
62+
63+
sequencerPrivateKey = 0x1234567890123456789012345678901234567890123456789012345678901234;
64+
userPrivateKey = 0x1234567890123456789012345678901234567890123456789012345678901235;
65+
sequencer = hevm.addr(sequencerPrivateKey);
66+
user = hevm.addr(userPrivateKey);
67+
68+
// Deploy tokens
69+
weth = new WrappedEther();
70+
l1Token = new MockERC20("Mock", "M", 18);
71+
72+
// Deploy L2 contracts
73+
template = new ScrollStandardERC20();
74+
factory = new ScrollStandardERC20Factory(address(template));
75+
counterpartGateway = new L2StandardERC20Gateway(address(1), address(1), address(1), address(factory));
76+
77+
// Deploy L1 contracts
78+
gateway = _deployGateway(address(l1Messenger));
79+
vault = _deployVault();
80+
81+
// Initialize L1 contracts
82+
gateway.initialize();
83+
vault.initialize(vaultAdmin, sequencer);
84+
85+
// Setup token balances
86+
l1Token.mint(address(vault), 100 ether);
87+
weth.deposit{value: 100 ether}();
88+
weth.transfer(address(vault), 100 ether);
89+
}
90+
91+
function testInitialize() public {
92+
// Test that the vault was initialized correctly in setUp
93+
assertTrue(vault.hasRole(vault.DEFAULT_ADMIN_ROLE(), vaultAdmin));
94+
assertTrue(vault.hasRole(vault.SEQUENCER_ROLE(), sequencer));
95+
96+
assertEq(vault.weth(), address(weth));
97+
assertEq(vault.gateway(), address(gateway));
98+
99+
// Test role constants
100+
assertEq(vault.SEQUENCER_ROLE(), keccak256("SEQUENCER_ROLE"));
101+
}
102+
103+
function testClaimFastWithdrawERC20(
104+
address to,
105+
uint256 amount,
106+
bytes32 messageHash
107+
) public {
108+
hevm.assume(to != address(0));
109+
hevm.assume(to.code.length == 0);
110+
111+
amount = bound(amount, 1, 100 ether);
112+
l2Token = MockERC20(gateway.getL2ERC20Address(address(l1Token)));
113+
114+
// Create the struct hash
115+
bytes32 structHash = keccak256(
116+
abi.encode(
117+
vault.getWithdrawTypehash(),
118+
address(l1Token),
119+
address(l2Token), // l2Token
120+
to,
121+
amount,
122+
messageHash
123+
)
124+
);
125+
126+
// Create the typed data hash
127+
bytes32 hash = vault.hashTypedDataV4(structHash);
128+
129+
// revert when the signature is invalid
130+
hevm.expectRevert("ECDSA: invalid signature length");
131+
vault.claimFastWithdraw(address(l1Token), to, amount, messageHash, bytes("invalid"));
132+
hevm.expectRevert("ECDSA: invalid signature");
133+
vault.claimFastWithdraw(address(l1Token), to, amount, messageHash, new bytes(65));
134+
135+
// revert when signer mismatch is not sequencer
136+
bytes memory invalidSignature;
137+
{
138+
(uint8 v, bytes32 r, bytes32 s) = hevm.sign(userPrivateKey, hash);
139+
invalidSignature = abi.encodePacked(r, s, v);
140+
}
141+
hevm.expectRevert(
142+
abi.encodePacked(
143+
"AccessControl: account ",
144+
StringsUpgradeable.toHexString(user),
145+
" is missing role ",
146+
StringsUpgradeable.toHexString(uint256(vault.SEQUENCER_ROLE()), 32)
147+
)
148+
);
149+
vault.claimFastWithdraw(address(l1Token), to, amount, messageHash, invalidSignature);
150+
151+
// Sign the hash with sequencer's private key
152+
bytes memory signature;
153+
{
154+
(uint8 v, bytes32 r, bytes32 s) = hevm.sign(sequencerPrivateKey, hash);
155+
signature = abi.encodePacked(r, s, v);
156+
}
157+
158+
// Call claimFastWithdraw and Expect the Withdraw event
159+
uint256 toBalanceBefore = l1Token.balanceOf(to);
160+
uint256 vaultBalanceBefore = l1Token.balanceOf(address(vault));
161+
hevm.expectEmit(true, true, true, true);
162+
emit Withdraw(address(l1Token), address(l2Token), to, amount, messageHash);
163+
vault.claimFastWithdraw(address(l1Token), to, amount, messageHash, signature);
164+
uint256 toBalanceAfter = l1Token.balanceOf(to);
165+
uint256 vaultBalanceAfter = l1Token.balanceOf(address(vault));
166+
167+
// Verify token transfer
168+
assertEq(toBalanceAfter - toBalanceBefore, amount);
169+
assertEq(vaultBalanceBefore - vaultBalanceAfter, amount);
170+
171+
// Verify the withdraw is marked as processed
172+
assertTrue(vault.isWithdrawn(structHash));
173+
174+
// revert when claim again on the same struct hash
175+
hevm.expectRevert(FastWithdrawVault.ErrorWithdrawAlreadyProcessed.selector);
176+
hevm.startPrank(sequencer);
177+
vault.claimFastWithdraw(address(l1Token), to, amount, messageHash, signature);
178+
hevm.stopPrank();
179+
}
180+
181+
/*
182+
function testClaimFastWithdrawWETH() public {
183+
address wethAddr = address(weth);
184+
address from = user;
185+
address to = recipient;
186+
uint256 amount = 50 ether;
187+
bytes32 messageHash = keccak256("test_weth_message_hash");
188+
189+
// Create the struct hash
190+
bytes32 structHash = keccak256(
191+
abi.encode(
192+
vault.getWithdrawTypehash(),
193+
wethAddr,
194+
address(l2Token), // l2Token
195+
to,
196+
amount,
197+
messageHash
198+
)
199+
);
200+
201+
// Create the typed data hash
202+
bytes32 hash = vault.hashTypedDataV4(structHash);
203+
204+
// Sign the hash with sequencer's private key
205+
(uint8 v, bytes32 r, bytes32 s) = hevm.sign(sequencerPrivateKey, hash);
206+
bytes memory signature = abi.encodePacked(r, s, v);
207+
208+
// Mock the gateway to return l2Token address
209+
hevm.mockCall(
210+
address(0x123), // gateway address
211+
abi.encodeWithSelector(IL1ERC20Gateway.getL2ERC20Address.selector, wethAddr),
212+
abi.encode(address(l2Token))
213+
);
214+
215+
// Mock WETH withdraw function
216+
hevm.mockCall(wethAddr, abi.encodeWithSelector(IWETH.withdraw.selector, amount), abi.encode());
217+
218+
// Expect the Withdraw event
219+
hevm.expectEmit(true, true, true, true);
220+
emit Withdraw(wethAddr, address(l2Token), to, amount, messageHash);
221+
222+
// Call claimFastWithdraw
223+
vault.claimFastWithdraw(wethAddr, to, amount, messageHash, signature);
224+
225+
// Verify the withdraw is marked as processed
226+
assertTrue(vault.isWithdrawn(structHash));
227+
}
228+
*/
229+
230+
function testWithdrawByAdmin(address recipient, uint256 amount) public {
231+
hevm.assume(recipient != address(0));
232+
hevm.assume(recipient.code.length == 0);
233+
amount = bound(amount, 1, 100 ether);
234+
235+
// revert when caller is not admin
236+
hevm.expectRevert(
237+
abi.encodePacked(
238+
"AccessControl: account ",
239+
StringsUpgradeable.toHexString(user),
240+
" is missing role ",
241+
StringsUpgradeable.toHexString(uint256(vault.DEFAULT_ADMIN_ROLE()), 32)
242+
)
243+
);
244+
hevm.prank(user);
245+
vault.withdraw(address(l1Token), recipient, amount);
246+
247+
// Admin should be able to withdraw
248+
uint256 balanceBefore = l1Token.balanceOf(recipient);
249+
uint256 vaultBalanceBefore = l1Token.balanceOf(address(vault));
250+
hevm.prank(vaultAdmin);
251+
vault.withdraw(address(l1Token), recipient, amount);
252+
uint256 balanceAfter = l1Token.balanceOf(recipient);
253+
uint256 vaultBalanceAfter = l1Token.balanceOf(address(vault));
254+
255+
// Verify token transfer
256+
assertEq(balanceAfter - balanceBefore, amount);
257+
assertEq(vaultBalanceBefore - vaultBalanceAfter, amount);
258+
}
259+
260+
function _deployGateway(address messenger) internal returns (L1ERC20GatewayValidium _gateway) {
261+
_gateway = L1ERC20GatewayValidium(_deployProxy(address(0)));
262+
263+
admin.upgrade(
264+
ITransparentUpgradeableProxy(address(_gateway)),
265+
address(
266+
new L1ERC20GatewayValidium(
267+
address(counterpartGateway),
268+
address(messenger),
269+
address(template),
270+
address(factory)
271+
)
272+
)
273+
);
274+
}
275+
276+
function _deployVault() internal returns (FastWithdrawVaultHelper _vault) {
277+
_vault = FastWithdrawVaultHelper(payable(_deployProxy(address(0))));
278+
279+
admin.upgrade(
280+
ITransparentUpgradeableProxy(address(_vault)),
281+
address(new FastWithdrawVaultHelper(address(weth), address(gateway)))
282+
);
283+
}
284+
}

0 commit comments

Comments
 (0)