Skip to content

Commit ffae635

Browse files
authored
use call - not transfer - to enable Gnosis Safes (#72)
* use call, not transfer, to include Gnosis Safes * use Address library (with uncapped gas) * add explicit reentrancy protection everywhere * snapshot
1 parent b307211 commit ffae635

File tree

7 files changed

+89
-53
lines changed

7 files changed

+89
-53
lines changed

.gas-snapshot

+34-33
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,40 @@
1-
OrderOriginPermit2Test:test_fillPermit2() (gas: 225275)
2-
OrderOriginPermit2Test:test_fillPermit2_multi() (gas: 1019098)
3-
OrderOriginPermit2Test:test_initiatePermit2() (gas: 235756)
4-
OrderOriginPermit2Test:test_initiatePermit2_multi() (gas: 992082)
5-
OrdersTest:test_fill_ERC20() (gas: 71615)
6-
OrdersTest:test_fill_ETH() (gas: 68524)
7-
OrdersTest:test_fill_both() (gas: 167851)
8-
OrdersTest:test_fill_multiETH() (gas: 132145)
9-
OrdersTest:test_fill_underflowETH() (gas: 115425)
10-
OrdersTest:test_initiate_ERC20() (gas: 82688)
11-
OrdersTest:test_initiate_ETH() (gas: 45150)
12-
OrdersTest:test_initiate_both() (gas: 119963)
13-
OrdersTest:test_initiate_multiERC20() (gas: 724742)
14-
OrdersTest:test_initiate_multiETH() (gas: 75538)
15-
OrdersTest:test_orderExpired() (gas: 28106)
16-
OrdersTest:test_sweepERC20() (gas: 60713)
17-
OrdersTest:test_sweepETH() (gas: 82348)
18-
OrdersTest:test_underflowETH() (gas: 63690)
19-
PassagePermit2Test:test_disallowedEnterPermit2() (gas: 696817)
20-
PassagePermit2Test:test_enterTokenPermit2() (gas: 145435)
21-
PassageTest:test_configureEnter() (gas: 128989)
22-
PassageTest:test_disallowedEnter() (gas: 57692)
1+
GnosisSafeTest:test_gnosis_receive() (gas: 15927)
2+
OrderOriginPermit2Test:test_fillPermit2() (gas: 225741)
3+
OrderOriginPermit2Test:test_fillPermit2_multi() (gas: 1019564)
4+
OrderOriginPermit2Test:test_initiatePermit2() (gas: 236222)
5+
OrderOriginPermit2Test:test_initiatePermit2_multi() (gas: 992548)
6+
OrdersTest:test_fill_ERC20() (gas: 72081)
7+
OrdersTest:test_fill_ETH() (gas: 69103)
8+
OrdersTest:test_fill_both() (gas: 168430)
9+
OrdersTest:test_fill_multiETH() (gas: 132837)
10+
OrdersTest:test_fill_underflowETH() (gas: 115826)
11+
OrdersTest:test_initiate_ERC20() (gas: 83154)
12+
OrdersTest:test_initiate_ETH() (gas: 45616)
13+
OrdersTest:test_initiate_both() (gas: 120429)
14+
OrdersTest:test_initiate_multiERC20() (gas: 725208)
15+
OrdersTest:test_initiate_multiETH() (gas: 76004)
16+
OrdersTest:test_orderExpired() (gas: 28394)
17+
OrdersTest:test_sweepERC20() (gas: 61179)
18+
OrdersTest:test_sweepETH() (gas: 83384)
19+
OrdersTest:test_underflowETH() (gas: 63978)
20+
PassagePermit2Test:test_disallowedEnterPermit2() (gas: 699905)
21+
PassagePermit2Test:test_enterTokenPermit2() (gas: 145901)
22+
PassageTest:test_configureEnter() (gas: 130009)
23+
PassageTest:test_disallowedEnter() (gas: 57980)
2324
PassageTest:test_enter() (gas: 25519)
24-
PassageTest:test_enterToken() (gas: 65469)
25-
PassageTest:test_enterToken_defaultChain() (gas: 64051)
25+
PassageTest:test_enterToken() (gas: 65935)
26+
PassageTest:test_enterToken_defaultChain() (gas: 64517)
2627
PassageTest:test_enter_defaultChain() (gas: 24055)
27-
PassageTest:test_fallback() (gas: 21533)
28-
PassageTest:test_onlyTokenAdmin() (gas: 16881)
29-
PassageTest:test_receive() (gas: 21383)
30-
PassageTest:test_setUp() (gas: 17011)
31-
PassageTest:test_withdraw() (gas: 60183)
32-
RollupPassagePermit2Test:test_exitTokenPermit2() (gas: 129388)
28+
PassageTest:test_fallback() (gas: 22170)
29+
PassageTest:test_onlyTokenAdmin() (gas: 17169)
30+
PassageTest:test_receive() (gas: 21487)
31+
PassageTest:test_setUp() (gas: 17000)
32+
PassageTest:test_withdraw() (gas: 60649)
33+
RollupPassagePermit2Test:test_exitTokenPermit2() (gas: 129854)
3334
RollupPassageTest:test_exit() (gas: 22403)
34-
RollupPassageTest:test_exitToken() (gas: 51071)
35-
RollupPassageTest:test_fallback() (gas: 19949)
36-
RollupPassageTest:test_receive() (gas: 19844)
35+
RollupPassageTest:test_exitToken() (gas: 51444)
36+
RollupPassageTest:test_fallback() (gas: 20586)
37+
RollupPassageTest:test_receive() (gas: 19948)
3738
TransactTest:test_configureGas() (gas: 22828)
3839
TransactTest:test_enterTransact() (gas: 103973)
3940
TransactTest:test_onlyGasAdmin() (gas: 8810)

src/orders/OrderDestination.sol

+7-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import {OrdersPermit2} from "./OrdersPermit2.sol";
55
import {IOrders} from "./IOrders.sol";
66
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
77
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
8+
import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol";
9+
import {ReentrancyGuardTransient} from "openzeppelin-contracts/contracts/utils/ReentrancyGuardTransient.sol";
810

911
/// @notice Contract capable of processing fulfillment of intent-based Orders.
10-
abstract contract OrderDestination is IOrders, OrdersPermit2 {
12+
abstract contract OrderDestination is IOrders, OrdersPermit2, ReentrancyGuardTransient {
1113
using SafeERC20 for IERC20;
14+
using Address for address payable;
1215

1316
/// @notice Emitted when Order Outputs are sent to their recipients.
1417
/// @dev NOTE that here, Output.chainId denotes the *origin* chainId.
@@ -19,7 +22,7 @@ abstract contract OrderDestination is IOrders, OrdersPermit2 {
1922
/// @dev NOTE that here, Output.chainId denotes the *origin* chainId.
2023
/// @param outputs - The Outputs to be transferred.
2124
/// @custom:emits Filled
22-
function fill(Output[] memory outputs) external payable {
25+
function fill(Output[] memory outputs) external payable nonReentrant {
2326
// transfer outputs
2427
_transferOutputs(outputs);
2528

@@ -37,7 +40,7 @@ abstract contract OrderDestination is IOrders, OrdersPermit2 {
3740
/// @param outputs - The Outputs to be transferred. signed over via permit2 witness.
3841
/// @param permit2 - the permit2 details, signer, and signature.
3942
/// @custom:emits Filled
40-
function fillPermit2(Output[] memory outputs, OrdersPermit2.Permit2Batch calldata permit2) external {
43+
function fillPermit2(Output[] memory outputs, OrdersPermit2.Permit2Batch calldata permit2) external nonReentrant {
4144
// transfer all tokens to the Output recipients via permit2 (includes check on nonce & deadline)
4245
_permitWitnessTransferFrom(
4346
outputWitness(outputs), _fillTransferDetails(outputs, permit2.permit.permitted), permit2
@@ -54,7 +57,7 @@ abstract contract OrderDestination is IOrders, OrdersPermit2 {
5457
if (outputs[i].token == address(0)) {
5558
// this line should underflow if there's an attempt to spend more ETH than is attached to the transaction
5659
value -= outputs[i].amount;
57-
payable(outputs[i].recipient).transfer(outputs[i].amount);
60+
payable(outputs[i].recipient).sendValue(outputs[i].amount);
5861
} else {
5962
IERC20(outputs[i].token).safeTransferFrom(msg.sender, outputs[i].recipient, outputs[i].amount);
6063
}

src/orders/OrderOrigin.sol

+8-5
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import {OrdersPermit2} from "./OrdersPermit2.sol";
55
import {IOrders} from "./IOrders.sol";
66
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
77
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
8+
import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol";
9+
import {ReentrancyGuardTransient} from "openzeppelin-contracts/contracts/utils/ReentrancyGuardTransient.sol";
810

911
/// @notice Contract capable of registering initiation of intent-based Orders.
10-
abstract contract OrderOrigin is IOrders, OrdersPermit2 {
12+
abstract contract OrderOrigin is IOrders, OrdersPermit2, ReentrancyGuardTransient {
1113
using SafeERC20 for IERC20;
14+
using Address for address payable;
1215

1316
/// @notice Thrown when an Order is submitted with a deadline that has passed.
1417
error OrderExpired();
@@ -36,7 +39,7 @@ abstract contract OrderOrigin is IOrders, OrdersPermit2 {
3639
/// @param outputs - The token amounts that must be received on their target chain(s) in order for the Order to be executed.
3740
/// @custom:reverts OrderExpired if the deadline has passed.
3841
/// @custom:emits Order if the transaction mines.
39-
function initiate(uint256 deadline, Input[] memory inputs, Output[] memory outputs) external payable {
42+
function initiate(uint256 deadline, Input[] memory inputs, Output[] memory outputs) external payable nonReentrant {
4043
// check that the deadline hasn't passed
4144
if (block.timestamp > deadline) revert OrderExpired();
4245

@@ -59,7 +62,7 @@ abstract contract OrderOrigin is IOrders, OrdersPermit2 {
5962
address tokenRecipient,
6063
Output[] memory outputs,
6164
OrdersPermit2.Permit2Batch calldata permit2
62-
) external {
65+
) external nonReentrant {
6366
// transfer all tokens to the tokenRecipient via permit2 (includes check on nonce & deadline)
6467
_permitWitnessTransferFrom(
6568
outputWitness(outputs), _initiateTransferDetails(tokenRecipient, permit2.permit.permitted), permit2
@@ -77,10 +80,10 @@ abstract contract OrderOrigin is IOrders, OrdersPermit2 {
7780
/// @param token - The token to transfer.
7881
/// @custom:emits Sweep
7982
/// @custom:reverts OnlyBuilder if called by non-block builder
80-
function sweep(address recipient, address token, uint256 amount) external {
83+
function sweep(address recipient, address token, uint256 amount) external nonReentrant {
8184
// send ETH or tokens
8285
if (token == address(0)) {
83-
payable(recipient).transfer(amount);
86+
payable(recipient).sendValue(amount);
8487
} else {
8588
IERC20(token).safeTransfer(recipient, amount);
8689
}

src/passage/Passage.sol

+11-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import {PassagePermit2} from "./PassagePermit2.sol";
55
import {UsesPermit2} from "../UsesPermit2.sol";
66
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
77
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
8+
import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol";
9+
import {ReentrancyGuardTransient} from "openzeppelin-contracts/contracts/utils/ReentrancyGuardTransient.sol";
810

911
/// @notice A contract deployed to Host chain that allows tokens to enter the rollup.
10-
contract Passage is PassagePermit2 {
12+
contract Passage is PassagePermit2, ReentrancyGuardTransient {
1113
using SafeERC20 for IERC20;
14+
using Address for address payable;
1215

1316
/// @notice The chainId of rollup that Ether will be sent to by default when entering the rollup via fallback() or receive().
1417
uint256 public immutable defaultRollupChainId;
@@ -91,7 +94,10 @@ contract Passage is PassagePermit2 {
9194
/// @param rollupRecipient - The recipient of tokens on the rollup.
9295
/// @param token - The host chain address of the token entering the rollup.
9396
/// @param amount - The amount of tokens entering the rollup.
94-
function enterToken(uint256 rollupChainId, address rollupRecipient, address token, uint256 amount) public {
97+
function enterToken(uint256 rollupChainId, address rollupRecipient, address token, uint256 amount)
98+
public
99+
nonReentrant
100+
{
95101
// transfer tokens to this contract
96102
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
97103
// check and emit
@@ -110,6 +116,7 @@ contract Passage is PassagePermit2 {
110116
/// @param permit2 - The Permit2 information, including token & amount.
111117
function enterTokenPermit2(uint256 rollupChainId, address rollupRecipient, PassagePermit2.Permit2 calldata permit2)
112118
external
119+
nonReentrant
113120
{
114121
// transfer tokens to this contract via permit2
115122
_permitWitnessTransferFrom(enterWitness(rollupChainId, rollupRecipient), permit2);
@@ -125,10 +132,10 @@ contract Passage is PassagePermit2 {
125132

126133
/// @notice Allows the admin to withdraw ETH or ERC20 tokens from the contract.
127134
/// @dev Only the admin can call this function.
128-
function withdraw(address token, address recipient, uint256 amount) external {
135+
function withdraw(address token, address recipient, uint256 amount) external nonReentrant {
129136
if (msg.sender != tokenAdmin) revert OnlyTokenAdmin();
130137
if (token == address(0)) {
131-
payable(recipient).transfer(amount);
138+
payable(recipient).sendValue(amount);
132139
} else {
133140
IERC20(token).safeTransfer(recipient, amount);
134141
}

src/passage/RollupPassage.sol

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import {UsesPermit2} from "../UsesPermit2.sol";
66
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
77
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
88
import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol";
9+
import {ReentrancyGuardTransient} from "openzeppelin-contracts/contracts/utils/ReentrancyGuardTransient.sol";
910

1011
/// @notice Enables tokens to Exit the rollup.
11-
contract RollupPassage is PassagePermit2 {
12+
contract RollupPassage is PassagePermit2, ReentrancyGuardTransient {
1213
using SafeERC20 for IERC20;
1314

1415
/// @notice Emitted when native Ether exits the rollup.
@@ -47,7 +48,7 @@ contract RollupPassage is PassagePermit2 {
4748
/// @param token - The rollup address of the token exiting the rollup.
4849
/// @param amount - The amount of tokens exiting the rollup.
4950
/// @custom:emits ExitToken
50-
function exitToken(address hostRecipient, address token, uint256 amount) external {
51+
function exitToken(address hostRecipient, address token, uint256 amount) external nonReentrant {
5152
// transfer tokens to this contract
5253
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
5354
// burn and emit
@@ -58,7 +59,7 @@ contract RollupPassage is PassagePermit2 {
5859
/// @param hostRecipient - The *requested* recipient of tokens on the host chain.
5960
/// @param permit2 - The Permit2 information, including token & amount.
6061
/// @custom:emits ExitToken
61-
function exitTokenPermit2(address hostRecipient, PassagePermit2.Permit2 calldata permit2) external {
62+
function exitTokenPermit2(address hostRecipient, PassagePermit2.Permit2 calldata permit2) external nonReentrant {
6263
// transfer tokens to this contract
6364
_permitWitnessTransferFrom(exitWitness(hostRecipient), permit2);
6465
// burn and emit

test/Passage.t.sol

+9-4
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import {RollupPassage} from "../src/passage/RollupPassage.sol";
88
import {TestERC20} from "./Helpers.t.sol";
99
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
1010
import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol";
11+
import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol";
1112
import {Test, console2} from "forge-std/Test.sol";
1213

1314
contract PassageTest is Test {
15+
using Address for address payable;
16+
1417
Passage public target;
1518
address token;
1619
address newToken;
@@ -113,13 +116,13 @@ contract PassageTest is Test {
113116
function test_receive() public {
114117
vm.expectEmit();
115118
emit Enter(target.defaultRollupChainId(), address(this), amount);
116-
address(target).call{value: amount}("");
119+
payable(address(target)).sendValue(amount);
117120
}
118121

119122
function test_fallback() public {
120123
vm.expectEmit();
121124
emit Enter(target.defaultRollupChainId(), address(this), amount);
122-
address(target).call{value: amount}("0xabcd");
125+
payable(address(target)).functionCallWithValue("0xabcd", amount);
123126
}
124127

125128
function test_enter() public {
@@ -163,6 +166,8 @@ contract PassageTest is Test {
163166
}
164167

165168
contract RollupPassageTest is Test {
169+
using Address for address payable;
170+
166171
RollupPassage public target;
167172
address token;
168173
address recipient = address(0x123);
@@ -185,13 +190,13 @@ contract RollupPassageTest is Test {
185190
function test_receive() public {
186191
vm.expectEmit();
187192
emit Exit(address(this), amount);
188-
address(target).call{value: amount}("");
193+
payable(address(target)).sendValue(amount);
189194
}
190195

191196
function test_fallback() public {
192197
vm.expectEmit();
193198
emit Exit(address(this), amount);
194-
address(target).call{value: amount}("0xabcd");
199+
payable(address(target)).functionCallWithValue("0xabcd", amount);
195200
}
196201

197202
function test_exit() public {

test/Safe.t.sol

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.24;
3+
4+
// utils
5+
import {Test, console2} from "forge-std/Test.sol";
6+
7+
contract GnosisSafeTest is Test {
8+
function setUp() public {
9+
vm.createSelectFork("https://ethereum-rpc.publicnode.com");
10+
}
11+
12+
// NOTE: this test fails if 4000 gas is provided. seems 4100 is approx the minimum.
13+
function test_gnosis_receive() public {
14+
payable(address(0x7c68c42De679ffB0f16216154C996C354cF1161B)).call{value: 1 ether, gas: 4100}("");
15+
}
16+
}

0 commit comments

Comments
 (0)