Repository navigation
Expand file tree
/
Copy pathRouterCommon.sol
More file actions
331 lines (285 loc) · 14 KB
/
Copy pathRouterCommon.sol
File metadata and controls
331 lines (285 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;
import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol";
import { IRouterCommon } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterCommon.sol";
import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol";
import { IAllowanceTransfer } from "permit2/src/interfaces/IAllowanceTransfer.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import { StorageSlotExtension } from "@balancer-labs/v3-solidity-utils/contracts/openzeppelin/StorageSlotExtension.sol";
import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol";
import { RevertCodec } from "@balancer-labs/v3-solidity-utils/contracts/helpers/RevertCodec.sol";
import {
ReentrancyGuardTransient
} from "@balancer-labs/v3-solidity-utils/contracts/openzeppelin/ReentrancyGuardTransient.sol";
import { Version } from "@balancer-labs/v3-solidity-utils/contracts/helpers/Version.sol";
import {
TransientStorageHelpers
} from "@balancer-labs/v3-solidity-utils/contracts/helpers/TransientStorageHelpers.sol";
import { RouterWethLib } from "./lib/RouterWethLib.sol";
import { SenderGuard } from "./SenderGuard.sol";
import { VaultGuard } from "./VaultGuard.sol";
/**
* @notice Abstract base contract for functions shared among all Routers.
* @dev Common functionality includes access to the sender (which would normally be obscured, since msg.sender in the
* Vault is the Router contract itself, not the account that invoked the Router), versioning, and the external
* invocation functions (`permitBatchAndCall` and `multicall`).
*/
abstract contract RouterCommon is IRouterCommon, SenderGuard, VaultGuard, ReentrancyGuardTransient, Version {
using Address for address payable;
using StorageSlotExtension for *;
using RouterWethLib for IWETH;
using SafeCast for *;
// NOTE: If you use a constant, then it is simply replaced everywhere when this constant is used by what is written
// after =. If you use immutable, the value is first calculated and then replaced everywhere. That means that if a
// constant has executable variables, they will be executed every time the constant is used.
// solhint-disable-next-line var-name-mixedcase
bytes32 private immutable _IS_RETURN_ETH_LOCKED_SLOT =
TransientStorageHelpers.calculateSlot(type(RouterCommon).name, "isReturnEthLocked");
// solhint-disable-next-line var-name-mixedcase
IWETH internal immutable _weth;
IPermit2 internal immutable _permit2;
// If Permit2 is the zero address, set this flag to indicate it's a "prepaid" router.
// Prepaid routers are designed to be used by contracts (e.g., aggregators), which send funds to the Vault
// in advance. "Retail" routers do Permit2 token transfers from the user during the operation.
bool internal immutable _isPrepaid;
/**
* @notice Locks the return of excess ETH to the sender until the end of the function.
* @dev This also encompasses the `saveSender` functionality.
*/
modifier saveSenderAndManageEth() {
bool isExternalSender = _saveSender(msg.sender);
// Revert if a function with this modifier is called recursively (e.g., multicall).
if (_isReturnEthLockedSlot().tload()) {
revert ReentrancyGuardReentrantCall();
}
// Lock the return of ETH during execution
_isReturnEthLockedSlot().tstore(true);
_;
_isReturnEthLockedSlot().tstore(false);
address sender = _getSenderSlot().tload();
_discardSenderIfRequired(isExternalSender);
_returnEth(sender);
}
constructor(
IVault vault,
IWETH weth,
IPermit2 permit2,
string memory routerVersion
) SenderGuard() VaultGuard(vault) Version(routerVersion) {
_weth = weth;
_permit2 = permit2;
_isPrepaid = permit2 == IPermit2(address(0));
}
/// @inheritdoc IRouterCommon
function getWeth() external view returns (IWETH) {
return _weth;
}
/// @inheritdoc IRouterCommon
function getPermit2() external view returns (IPermit2) {
return _permit2;
}
/// @inheritdoc IRouterCommon
function getVault() external view virtual returns (IVault) {
return _vault;
}
/*******************************************************************************
Utilities
*******************************************************************************/
struct SignatureParts {
bytes32 r;
bytes32 s;
uint8 v;
}
/// @inheritdoc IRouterCommon
function permitBatchAndCall(
PermitApproval[] calldata permitBatch,
bytes[] calldata permitSignatures,
IAllowanceTransfer.PermitBatch calldata permit2Batch,
bytes calldata permit2Signature,
bytes[] calldata multicallData
) external payable virtual returns (bytes[] memory) {
if (_isPrepaid) {
revert OperationNotSupported();
}
_permitBatch(permitBatch, permitSignatures, permit2Batch, permit2Signature);
// Execute all the required operations once permissions have been granted.
return multicall(multicallData);
}
function _permitBatch(
PermitApproval[] calldata permitBatch,
bytes[] calldata permitSignatures,
IAllowanceTransfer.PermitBatch calldata permit2Batch,
bytes calldata permit2Signature
) internal nonReentrant {
InputHelpers.ensureInputLengthMatch(permitBatch.length, permitSignatures.length);
// Use Permit (ERC-2612) to grant allowances to Permit2 for tokens to swap,
// and grant allowances to Vault for BPT tokens.
for (uint256 i = 0; i < permitBatch.length; ++i) {
bytes memory signature = permitSignatures[i];
SignatureParts memory signatureParts = _getSignatureParts(signature);
PermitApproval memory permitApproval = permitBatch[i];
try
IERC20Permit(permitApproval.token).permit(
permitApproval.owner,
address(this),
permitApproval.amount,
permitApproval.deadline,
signatureParts.v,
signatureParts.r,
signatureParts.s
)
{
// solhint-disable-previous-line no-empty-blocks
// OK; carry on.
} catch (bytes memory returnData) {
// Did it fail because the permit was executed (possible DoS attack to make the transaction revert),
// or was it something else (e.g., deadline, invalid signature)?
if (
IERC20(permitApproval.token).allowance(permitApproval.owner, address(this)) != permitApproval.amount
) {
// It was something else, or allowance was used, so we should revert. Bubble up the revert reason.
RevertCodec.bubbleUpRevert(returnData);
}
}
}
// Only call permit2 if there's something to do.
if (permit2Batch.details.length > 0) {
// Use Permit2 for tokens that are swapped and added into the Vault. Note that this call on Permit2 is
// theoretically also vulnerable to the same DoS attack as above. This edge case was not mitigated
// on-chain, mainly due to the increased complexity and cost of protecting the batch call.
//
// If this is a concern, we recommend submitting through a private node to avoid front-running the public
// mempool. In any case, best practice is to always use expiring, limited approvals, and only with known
// and trusted contracts.
//
// See https://www.immunebytes.com/blog/permit2-erc-20-token-approvals-and-associated-risks/.
_permit2.permit(msg.sender, permit2Batch, permit2Signature);
}
}
/// @inheritdoc IRouterCommon
function multicall(
bytes[] calldata data
) public payable virtual saveSenderAndManageEth returns (bytes[] memory results) {
// Though theoretically these calls could be batched, the normal use case for multicall involves some
// combination of operation and token transfers (either permit2 or direct to Vault), which cannot be
// done with multicall alone.
if (_isPrepaid) {
revert OperationNotSupported();
}
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; ++i) {
results[i] = Address.functionDelegateCall(address(this), data[i]);
}
}
function _getSignatureParts(bytes memory signature) private pure returns (SignatureParts memory signatureParts) {
bytes32 r;
bytes32 s;
uint8 v;
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
signatureParts.r = r;
signatureParts.s = s;
signatureParts.v = v;
}
/**
* @dev Returns excess ETH back to the contract caller. Checks for sufficient ETH balance are made right before
* each deposit, ensuring it will revert with a friendly custom error. If there is any balance remaining when
* `_returnEth` is called, return it to the sender.
*
* Because the caller might not know exactly how much ETH a Vault action will require, they may send extra.
* Note that this excess value is returned *to the contract caller* (msg.sender). If caller and e.g. swap sender
* are not the same (because the caller is a relayer for the sender), then it is up to the caller to manage this
* returned ETH.
*/
function _returnEth(address sender) internal {
// It's cheaper to check the balance and return early than checking a transient variable.
// Moreover, most operations will not have ETH to return.
uint256 excess = address(this).balance;
if (excess == 0) {
return;
}
// If the return of ETH is locked, then don't return it,
// because _returnEth will be called again at the end of the call.
if (_isReturnEthLockedSlot().tload()) {
return;
}
payable(sender).sendValue(excess);
}
/**
* @dev Returns an array with `amountGiven` at `tokenIndex`, and 0 for every other index.
* The returned array length matches the number of tokens in the pool.
* Reverts if the given index is greater than or equal to the pool number of tokens.
*/
function _getSingleInputArrayAndTokenIndex(
address pool,
IERC20 token,
uint256 amountGiven
) internal view returns (uint256[] memory amountsGiven, uint256 tokenIndex) {
uint256 numTokens;
(numTokens, tokenIndex) = _vault.getPoolTokenCountAndIndexOfToken(pool, token);
amountsGiven = new uint256[](numTokens);
amountsGiven[tokenIndex] = amountGiven;
}
function _takeTokenIn(address sender, IERC20 tokenIn, uint256 amountIn, bool wethIsEth) internal {
// If the tokenIn is ETH, then wrap `amountIn` into WETH.
if (wethIsEth && tokenIn == _weth) {
_weth.wrapEthAndSettle(_vault, amountIn);
} else {
if (amountIn > 0) {
// Send the tokenIn amount to the Vault.
_permit2.transferFrom(sender, address(_vault), amountIn.toUint160(), address(tokenIn));
_vault.settle(tokenIn, amountIn);
}
}
}
function _sendTokenOut(address sender, IERC20 tokenOut, uint256 amountOut, bool wethIsEth) internal {
if (amountOut == 0) {
return;
}
// If the tokenOut is ETH, then unwrap `amountOut` into ETH.
if (wethIsEth && tokenOut == _weth) {
_weth.unwrapWethAndTransferToSender(_vault, sender, amountOut);
} else {
// Receive the tokenOut amountOut.
_vault.sendTo(tokenOut, sender, amountOut);
}
}
function _maxTokenLimits(address pool) internal view returns (uint256[] memory maxLimits) {
uint256 numTokens = _vault.getPoolTokens(pool).length;
maxLimits = new uint256[](numTokens);
for (uint256 i = 0; i < numTokens; ++i) {
maxLimits[i] = _MAX_AMOUNT;
}
}
function _isReturnEthLockedSlot() internal view returns (StorageSlotExtension.BooleanSlotType) {
return _IS_RETURN_ETH_LOCKED_SLOT.asBoolean();
}
/**
* @dev Enables the Router to receive ETH. This is required for it to be able to unwrap WETH, which sends ETH to the
* caller.
*
* The Router is not meant to hold ETH between transactions, so this rejects transfers from anyone but the WETH
* contract. A user who sends ETH here by mistake keeps it, since the transfer reverts. This check only exists as
* an attempt to prevent user error.
*
* Two routes still leave a balance behind, and this check cannot stop either: delivery with no function call at
* all (contract self-destruction, or receiving the block mining reward), and value attached to a payable entry
* point that neither spends nor refunds it. Such a balance is not stuck, but it does not stay with whoever left
* it either. `_returnEth` sends the contract's entire balance, whatever its source, to the sender of the next
* operation that reaches it. Nothing else is affected, as no accounting entry is ever kept against a stray
* balance.
*/
receive() external payable virtual {
if (msg.sender != address(_weth)) {
revert EthTransfer();
}
}
}