Skip to content

Commit c59eb4a

Browse files
authored
Merge pull request #69 from OffchainLabs/gambit-gaps
Fix gaps found by mutation testing
2 parents 85a3aa0 + 1ae2f9a commit c59eb4a

12 files changed

+2314
-725
lines changed

test-foundry/L1ArbitrumExtendedGateway.t.sol

Lines changed: 54 additions & 262 deletions
Large diffs are not rendered by default.
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
pragma solidity ^0.8.0;
4+
5+
import "forge-std/Test.sol";
6+
import "contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol";
7+
import {TestERC20} from "contracts/tokenbridge/test/TestERC20.sol";
8+
import {InboxMock} from "contracts/tokenbridge/test/InboxMock.sol";
9+
10+
abstract contract L1ArbitrumGatewayTest is Test {
11+
IL1ArbitrumGateway public l1Gateway;
12+
IERC20 public token;
13+
14+
address public l2Gateway = makeAddr("l2Gateway");
15+
address public router = makeAddr("router");
16+
address public inbox;
17+
address public user = makeAddr("user");
18+
19+
// retryable params
20+
uint256 public maxSubmissionCost;
21+
uint256 public maxGas = 1_000_000_000;
22+
uint256 public gasPriceBid = 100_000_000;
23+
uint256 public retryableCost;
24+
address public creditBackAddress = makeAddr("creditBackAddress");
25+
26+
// fuzzer behaves weirdly when it picks up this address which is used internally for issuing cheatcodes
27+
address internal constant FOUNDRY_CHEATCODE_ADDRESS = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D;
28+
29+
/* solhint-disable func-name-mixedcase */
30+
31+
function test_finalizeInboundTransfer() public virtual {
32+
// fund gateway with tokens being withdrawn
33+
vm.prank(address(l1Gateway));
34+
TestERC20(address(token)).mint();
35+
36+
// snapshot state before
37+
uint256 userBalanceBefore = token.balanceOf(user);
38+
uint256 l1GatewayBalanceBefore = token.balanceOf(address(l1Gateway));
39+
40+
// withdrawal params
41+
address from = address(3000);
42+
uint256 withdrawalAmount = 25;
43+
uint256 exitNum = 7;
44+
bytes memory callHookData = "";
45+
bytes memory data = abi.encode(exitNum, callHookData);
46+
47+
InboxMock(address(inbox)).setL2ToL1Sender(l2Gateway);
48+
49+
// trigger withdrawal
50+
vm.prank(address(IInbox(l1Gateway.inbox()).bridge()));
51+
l1Gateway.finalizeInboundTransfer(address(token), from, user, withdrawalAmount, data);
52+
53+
// check tokens are properly released
54+
uint256 userBalanceAfter = token.balanceOf(user);
55+
assertEq(userBalanceAfter - userBalanceBefore, withdrawalAmount, "Wrong user balance");
56+
57+
uint256 l1GatewayBalanceAfter = token.balanceOf(address(l1Gateway));
58+
assertEq(
59+
l1GatewayBalanceBefore - l1GatewayBalanceAfter,
60+
withdrawalAmount,
61+
"Wrong l1 gateway balance"
62+
);
63+
}
64+
65+
function test_finalizeInboundTransfer_revert_NotFromBridge() public {
66+
address notBridge = address(300);
67+
vm.prank(notBridge);
68+
vm.expectRevert("NOT_FROM_BRIDGE");
69+
l1Gateway.finalizeInboundTransfer(address(token), user, user, 100, "");
70+
}
71+
72+
function test_finalizeInboundTransfer_revert_OnlyCounterpartGateway() public {
73+
address notCounterPartGateway = address(400);
74+
InboxMock(address(inbox)).setL2ToL1Sender(notCounterPartGateway);
75+
76+
// trigger withdrawal
77+
vm.prank(address(IInbox(l1Gateway.inbox()).bridge()));
78+
vm.expectRevert("ONLY_COUNTERPART_GATEWAY");
79+
l1Gateway.finalizeInboundTransfer(address(token), user, user, 100, "");
80+
}
81+
82+
function test_finalizeInboundTransfer_revert_NoSender() public {
83+
InboxMock(address(inbox)).setL2ToL1Sender(address(0));
84+
85+
// trigger withdrawal
86+
vm.prank(address(IInbox(l1Gateway.inbox()).bridge()));
87+
vm.expectRevert("NO_SENDER");
88+
l1Gateway.finalizeInboundTransfer(address(token), user, user, 100, "");
89+
}
90+
91+
function test_getExternalCall() public {
92+
L1ArbitrumGatewayMock mockGateway = new L1ArbitrumGatewayMock();
93+
94+
uint256 exitNum = 7;
95+
address initialDestination = makeAddr("initialDestination");
96+
bytes memory initialData = bytes("1234");
97+
(address target, bytes memory data) =
98+
mockGateway.getExternalCall(exitNum, initialDestination, initialData);
99+
100+
assertEq(target, initialDestination, "Wrong target");
101+
assertEq(data, initialData, "Wrong data");
102+
}
103+
104+
function test_getOutboundCalldata() public virtual {
105+
bytes memory outboundCalldata = l1Gateway.getOutboundCalldata({
106+
_token: address(token),
107+
_from: user,
108+
_to: address(800),
109+
_amount: 355,
110+
_data: abi.encode("doStuff()")
111+
});
112+
113+
bytes memory expectedCalldata = abi.encodeWithSelector(
114+
ITokenGateway.finalizeInboundTransfer.selector,
115+
address(token),
116+
user,
117+
address(800),
118+
355,
119+
abi.encode("", abi.encode("doStuff()"))
120+
);
121+
122+
assertEq(outboundCalldata, expectedCalldata, "Invalid outboundCalldata");
123+
}
124+
125+
function test_outboundTransfer() public virtual {}
126+
127+
function test_outboundTransferCustomRefund_revert_ExtraDataDisabled() public {
128+
bytes memory callHookData = abi.encodeWithSignature("doSomething()");
129+
bytes memory routerEncodedData = buildRouterEncodedData(callHookData);
130+
131+
vm.prank(router);
132+
vm.expectRevert("EXTRA_DATA_DISABLED");
133+
l1Gateway.outboundTransferCustomRefund(
134+
address(token), user, user, 400, 0.1 ether, 0.01 ether, routerEncodedData
135+
);
136+
}
137+
138+
function test_outboundTransferCustomRefund_revert_L1NotContract() public {
139+
address invalidTokenAddress = address(70);
140+
141+
vm.prank(router);
142+
vm.expectRevert("L1_NOT_CONTRACT");
143+
l1Gateway.outboundTransferCustomRefund(
144+
address(invalidTokenAddress),
145+
user,
146+
user,
147+
400,
148+
0.1 ether,
149+
0.01 ether,
150+
buildRouterEncodedData("")
151+
);
152+
}
153+
154+
function test_outboundTransferCustomRefund_revert_NotFromRouter() public {
155+
vm.expectRevert("NOT_FROM_ROUTER");
156+
l1Gateway.outboundTransferCustomRefund(
157+
address(token), user, user, 400, 0.1 ether, 0.01 ether, ""
158+
);
159+
}
160+
161+
function test_postUpgradeInit() public {
162+
address proxyAdmin = makeAddr("proxyAdmin");
163+
vm.store(
164+
address(l1Gateway),
165+
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103,
166+
bytes32(uint256(uint160(proxyAdmin)))
167+
);
168+
vm.prank(proxyAdmin);
169+
170+
L1ArbitrumGateway(address(l1Gateway)).postUpgradeInit();
171+
}
172+
173+
function test_postUpgradeInit_revert_NotFromAdmin() public {
174+
address proxyAdmin = makeAddr("proxyAdmin");
175+
vm.store(
176+
address(l1Gateway),
177+
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103,
178+
bytes32(uint256(uint160(proxyAdmin)))
179+
);
180+
181+
vm.expectRevert("NOT_FROM_ADMIN");
182+
L1ArbitrumGateway(address(l1Gateway)).postUpgradeInit();
183+
}
184+
185+
function test_supportsInterface() public {
186+
bytes4 iface = type(IERC165).interfaceId;
187+
assertEq(l1Gateway.supportsInterface(iface), true, "Interface should be supported");
188+
189+
iface = IL1ArbitrumGateway.outboundTransferCustomRefund.selector;
190+
assertEq(l1Gateway.supportsInterface(iface), true, "Interface should be supported");
191+
192+
iface = bytes4(0);
193+
assertEq(l1Gateway.supportsInterface(iface), false, "Interface shouldn't be supported");
194+
195+
iface = IL1ArbitrumGateway.inbox.selector;
196+
assertEq(l1Gateway.supportsInterface(iface), false, "Interface shouldn't be supported");
197+
}
198+
199+
////
200+
// Helper functions
201+
////
202+
function buildRouterEncodedData(bytes memory callHookData)
203+
internal
204+
view
205+
virtual
206+
returns (bytes memory)
207+
{
208+
bytes memory userEncodedData = abi.encode(maxSubmissionCost, callHookData);
209+
bytes memory routerEncodedData = abi.encode(user, userEncodedData);
210+
211+
return routerEncodedData;
212+
}
213+
}
214+
215+
contract L1ArbitrumGatewayMock is L1ArbitrumGateway {
216+
function calculateL2TokenAddress(address x)
217+
public
218+
view
219+
override(ITokenGateway, TokenGateway)
220+
returns (address)
221+
{
222+
return x;
223+
}
224+
}
225+
226+
contract MockReentrantInbox {
227+
function createRetryableTicket(
228+
address,
229+
uint256,
230+
uint256,
231+
address,
232+
address,
233+
uint256,
234+
uint256,
235+
bytes memory
236+
) external payable returns (uint256) {
237+
// re-enter
238+
L1ArbitrumGateway(msg.sender).outboundTransferCustomRefund{value: msg.value}(
239+
address(100), address(100), address(100), 2, 2, 2, bytes("")
240+
);
241+
}
242+
}
243+
244+
contract MockReentrantERC20 {
245+
function balanceOf(address) external returns (uint256) {
246+
// re-enter
247+
L1ArbitrumGateway(msg.sender).outboundTransferCustomRefund(
248+
address(100), address(100), address(100), 2, 2, 3, bytes("")
249+
);
250+
return 5;
251+
}
252+
253+
function bridgeBurn(address, uint256) external {
254+
// re-enter
255+
L1ArbitrumGateway(msg.sender).outboundTransferCustomRefund(
256+
address(100), address(100), address(100), 2, 2, 3, bytes("")
257+
);
258+
}
259+
}

0 commit comments

Comments
 (0)