Skip to content

Commit b008199

Browse files
committed
Merge branch 'master' into mrice32/foundry
2 parents deab7e8 + 60a79c3 commit b008199

File tree

135 files changed

+11187
-750
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

135 files changed

+11187
-750
lines changed

contracts/Lens_SpokePool.sol

+13-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,19 @@ contract Lens_SpokePool is ZkSync_SpokePool {
1212
/// @custom:oz-upgrades-unsafe-allow constructor
1313
constructor(
1414
address _wrappedNativeTokenAddress,
15+
IERC20 _circleUSDC,
16+
ZkBridgeLike _zkUSDCBridge,
17+
ITokenMessenger _cctpTokenMessenger,
1518
uint32 _depositQuoteTimeBuffer,
1619
uint32 _fillDeadlineBuffer
17-
) ZkSync_SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer) {} // solhint-disable-line no-empty-blocks
20+
)
21+
ZkSync_SpokePool(
22+
_wrappedNativeTokenAddress,
23+
_circleUSDC,
24+
_zkUSDCBridge,
25+
_cctpTokenMessenger,
26+
_depositQuoteTimeBuffer,
27+
_fillDeadlineBuffer
28+
)
29+
{}
1830
}

contracts/Polygon_SpokePool.sol

+16-4
Original file line numberDiff line numberDiff line change
@@ -178,17 +178,22 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter
178178

179179
/**
180180
* @notice Override multicall so that it cannot include executeRelayerRefundLeaf
181-
* as one of the calls combined with other public function calls.
181+
* as one of the calls combined with other public function calls and blocks nested multicalls in general, which
182+
* don't have any practical use case. We also block nested multicalls which could be used to bypass
183+
* this check and there are no practical use cases for nested multicalls.
182184
* @dev Multicalling a single transaction will always succeed.
183185
* @dev Multicalling execute functions without combining other public function calls will succeed.
186+
* @dev Nested multicalls will always fail.
184187
* @dev Multicalling public function calls without combining execute functions will succeed.
185188
*/
186189
function _validateMulticallData(bytes[] calldata data) internal pure override {
187190
bool hasOtherPublicFunctionCall = false;
188191
bool hasExecutedLeafCall = false;
189192
for (uint256 i = 0; i < data.length; i++) {
190193
bytes4 selector = bytes4(data[i][:4]);
191-
if (selector == SpokePoolInterface.executeRelayerRefundLeaf.selector) {
194+
if (selector == MultiCallerUpgradeable.multicall.selector) {
195+
revert MulticallExecuteLeaf();
196+
} else if (selector == SpokePoolInterface.executeRelayerRefundLeaf.selector) {
192197
if (hasOtherPublicFunctionCall) revert MulticallExecuteLeaf();
193198
hasExecutedLeafCall = true;
194199
} else {
@@ -211,9 +216,11 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter
211216
) public payable override {
212217
// AddressLibUpgradeable.isContract isn't a sufficient check because it checks the contract code size of
213218
// msg.sender which is 0 if called from a constructor function on msg.sender. This is why we check if
214-
// msg.sender is equal to tx.origin which is fine as long as Polygon supports the tx.origin opcode.
219+
// msg.sender is equal to tx.origin which is fine as long as Polygon supports the tx.origin opcode. We also
220+
// check if the msg.sender has delegated their code to a contract via EIP7702.
215221
// solhint-disable-next-line avoid-tx-origin
216-
if (relayerRefundLeaf.amountToReturn > 0 && msg.sender != tx.origin) revert NotEOA();
222+
if (relayerRefundLeaf.amountToReturn > 0 && (msg.sender != tx.origin || msg.sender.code.length > 0))
223+
revert NotEOA();
217224
super.executeRelayerRefundLeaf(rootBundleId, relayerRefundLeaf, proof);
218225
}
219226

@@ -238,6 +245,11 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter
238245
}
239246

240247
function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal override {
248+
// WARNING: Withdrawing MATIC can result in the L1 PolygonTokenBridger.startExitWithBurntTokens() failing
249+
// due to a MAX_LOGS constraint imposed by the ERC20Predicate, so if this SpokePool will be used to withdraw
250+
// MATIC then additional constraints need to be imposed to limit the # of logs produed by the L2 withdrawal
251+
// transaction. Currently, MATIC is not a supported token in Across for this SpokePool.
252+
241253
// If the token is USDC, we need to use the CCTP bridge to transfer it to the hub pool.
242254
if (_isCCTPEnabled() && l2TokenAddress == address(usdcToken)) {
243255
_transferUsdc(withdrawalRecipient, amountToReturn);

contracts/SpokePool.sol

+1-23
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ abstract contract SpokePool is
7575
RootBundle[] public rootBundles;
7676

7777
// Origin token to destination token routings can be turned on or off, which can enable or disable deposits.
78-
mapping(address => mapping(uint256 => bool)) public enabledDepositRoutes;
78+
mapping(address => mapping(uint256 => bool)) private DEPRECATED_enabledDepositRoutes;
7979

8080
// Each relay is associated with the hash of parameters that uniquely identify the original deposit and a relay
8181
// attempt for that deposit. The relay itself is just represented as the amount filled so far. The total amount to
@@ -309,21 +309,6 @@ abstract contract SpokePool is
309309
_setWithdrawalRecipient(newWithdrawalRecipient);
310310
}
311311

312-
/**
313-
* @notice Enable/Disable an origin token => destination chain ID route for deposits. Callable by admin only.
314-
* @param originToken Token that depositor can deposit to this contract.
315-
* @param destinationChainId Chain ID for where depositor wants to receive funds.
316-
* @param enabled True to enable deposits, False otherwise.
317-
*/
318-
function setEnableRoute(
319-
address originToken,
320-
uint256 destinationChainId,
321-
bool enabled
322-
) public override onlyAdmin nonReentrant {
323-
enabledDepositRoutes[originToken][destinationChainId] = enabled;
324-
emit EnabledDepositRoute(originToken, destinationChainId, enabled);
325-
}
326-
327312
/**
328313
* @notice This method stores a new root bundle in this contract that can be executed to refund relayers, fulfill
329314
* slow relays, and send funds back to the HubPool on L1. This method can only be called by the admin and is
@@ -1309,10 +1294,6 @@ abstract contract SpokePool is
13091294
// Verify depositor is a valid EVM address.
13101295
params.depositor.checkAddress();
13111296

1312-
// Check that deposit route is enabled for the input token. There are no checks required for the output token
1313-
// which is pulled from the relayer at fill time and passed through this contract atomically to the recipient.
1314-
if (!enabledDepositRoutes[params.inputToken.toAddress()][params.destinationChainId]) revert DisabledRoute();
1315-
13161297
// Require that quoteTimestamp has a maximum age so that depositors pay an LP fee based on recent HubPool usage.
13171298
// It is assumed that cross-chain timestamps are normally loosely in-sync, but clock drift can occur. If the
13181299
// SpokePool time stalls or lags significantly, it is still possible to make deposits by setting quoteTimestamp
@@ -1398,9 +1379,6 @@ abstract contract SpokePool is
13981379
uint32 quoteTimestamp,
13991380
bytes memory message
14001381
) internal {
1401-
// Check that deposit route is enabled.
1402-
if (!enabledDepositRoutes[originToken][destinationChainId]) revert DisabledRoute();
1403-
14041382
// We limit the relay fees to prevent the user spending all their funds on fees.
14051383
if (SignedMath.abs(relayerFeePct) >= 0.5e18) revert InvalidRelayerFeePct();
14061384
if (amount > MAX_TRANSFER_SIZE) revert MaxTransferSizeExceeded();

contracts/SpokePoolVerifier.sol

+2-3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ contract SpokePoolVerifier {
4747
bytes32 recipient,
4848
bytes32 inputToken,
4949
uint256 inputAmount,
50+
bytes32 outputToken,
5051
uint256 outputAmount,
5152
uint256 destinationChainId,
5253
bytes32 exclusiveRelayer,
@@ -62,9 +63,7 @@ contract SpokePoolVerifier {
6263
msg.sender.toBytes32(),
6364
recipient,
6465
inputToken,
65-
// @dev Setting outputToken to 0x0 to instruct fillers to use the equivalent token
66-
// as the originToken on the destination chain.
67-
bytes32(0),
66+
outputToken,
6867
inputAmount,
6968
outputAmount,
7069
destinationChainId,

contracts/Universal_SpokePool.sol

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
5+
6+
import { IHelios } from "./external/interfaces/IHelios.sol";
7+
import "./libraries/CircleCCTPAdapter.sol";
8+
9+
import "./SpokePool.sol";
10+
11+
/**
12+
* @notice Spoke pool capable of executing calldata stored in L1 state via storage proof + Helios light client.
13+
* @dev This contract has one onlyOwner function to be used as an emergency fallback to execute a message to
14+
* this SpokePool in the case where the light-client is not functioning correctly. The owner is designed to be set
15+
* to a multisig contract on this chain.
16+
* @custom:security-contact [email protected]
17+
*/
18+
contract Universal_SpokePool is OwnableUpgradeable, SpokePool, CircleCCTPAdapter {
19+
/// @notice The data store contract that only the HubPool can write to. This spoke pool can only act on
20+
/// data that has been written to this store.
21+
address public immutable hubPoolStore;
22+
23+
/// @notice Slot index of the HubPoolStore's relayMessageCallData mapping.
24+
uint256 public constant HUB_POOL_STORE_CALLDATA_MAPPING_SLOT_INDEX = 0;
25+
26+
/// @notice The address of the Helios L1 light client contract.
27+
address public immutable helios;
28+
29+
/// @notice The owner of this contract must wait until this amount of seconds have passed since the latest
30+
/// helios light client update to emergency execute a message. This prevents the owner from executing a message
31+
/// in the happy case where the light client is being regularly updated. Therefore, this value should be
32+
/// set to a very high value, like 24 hours.
33+
uint256 public immutable ADMIN_UPDATE_BUFFER;
34+
35+
/// @notice Stores nonces of calldata stored in HubPoolStore that gets executed via executeMessage()
36+
/// to prevent replay attacks.
37+
mapping(uint256 => bool) public executedMessages;
38+
39+
// Warning: this variable should _never_ be touched outside of this contract. It is intentionally set to be
40+
// private. Leaving it set to true can permanently disable admin calls.
41+
bool private _adminCallValidated;
42+
43+
/// @notice Event emitted after off-chain agent sees HubPoolStore's emitted StoredCallData event and calls
44+
/// executeMessage() on this contract to relay the stored calldata.
45+
event RelayedCallData(uint256 indexed nonce, address caller);
46+
47+
error NotTarget();
48+
error AdminCallAlreadySet();
49+
error SlotValueMismatch();
50+
error AdminCallNotValidated();
51+
error DelegateCallFailed();
52+
error AlreadyExecuted();
53+
error NotImplemented();
54+
error AdminUpdateTooCloseToLastHeliosUpdate();
55+
56+
// All calls that have admin privileges must be fired from within the executeMessage method that validates that
57+
// the input data was published on L1 by the HubPool. This input data is then executed on this contract.
58+
// This modifier sets the adminCallValidated variable so this condition can be checked in _requireAdminSender().
59+
modifier validateInternalCalls() {
60+
// Make sure adminCallValidated is set to True only once at beginning of the function, which prevents
61+
// the function from being re-entered.
62+
if (_adminCallValidated) {
63+
revert AdminCallAlreadySet();
64+
}
65+
66+
// This sets a variable indicating that we're now inside a validated call.
67+
// Note: this is used by other methods to ensure that this call has been validated by this method and is not
68+
// spoofed.
69+
_adminCallValidated = true;
70+
71+
_;
72+
73+
// Reset adminCallValidated to false to disallow admin calls after this method exits.
74+
_adminCallValidated = false;
75+
}
76+
77+
/// @custom:oz-upgrades-unsafe-allow constructor
78+
constructor(
79+
uint256 _adminUpdateBufferSeconds,
80+
address _helios,
81+
address _hubPoolStore,
82+
address _wrappedNativeTokenAddress,
83+
uint32 _depositQuoteTimeBuffer,
84+
uint32 _fillDeadlineBuffer,
85+
IERC20 _l2Usdc,
86+
ITokenMessenger _cctpTokenMessenger
87+
)
88+
SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer)
89+
CircleCCTPAdapter(_l2Usdc, _cctpTokenMessenger, CircleDomainIds.Ethereum)
90+
{
91+
ADMIN_UPDATE_BUFFER = _adminUpdateBufferSeconds;
92+
helios = _helios;
93+
hubPoolStore = _hubPoolStore;
94+
}
95+
96+
function initialize(
97+
uint32 _initialDepositId,
98+
address _crossDomainAdmin,
99+
address _withdrawalRecipient
100+
) public initializer {
101+
__Ownable_init();
102+
__SpokePool_init(_initialDepositId, _crossDomainAdmin, _withdrawalRecipient);
103+
}
104+
105+
/**
106+
* @notice Relays calldata stored by the HubPool on L1 into this contract.
107+
* @dev Replay attacks are possible with this _message if this contract has the same address on another chain.
108+
* @param _messageNonce Nonce of message stored in HubPoolStore.
109+
* @param _message Message stored in HubPoolStore's relayMessageCallData mapping. Compared against raw value
110+
* in Helios light client for slot key corresponding to _messageNonce at block number.
111+
* @param _blockNumber Block number in light client we use to check slot value of slot key
112+
*/
113+
function executeMessage(
114+
uint256 _messageNonce,
115+
bytes calldata _message,
116+
uint256 _blockNumber
117+
) external validateInternalCalls {
118+
bytes32 slotKey = getSlotKey(_messageNonce);
119+
// The expected slot value corresponds to the hash of the L2 calldata and its target,
120+
// as originally stored in the HubPoolStore's relayMessageCallData mapping.
121+
bytes32 expectedSlotValue = keccak256(_message);
122+
123+
// Verify Helios light client has expected slot value.
124+
bytes32 slotValue = IHelios(helios).getStorageSlot(_blockNumber, hubPoolStore, slotKey);
125+
if (expectedSlotValue != slotValue) {
126+
revert SlotValueMismatch();
127+
}
128+
129+
// Validate state is intended to be sent to this contract. The target could have been set to the zero address
130+
// which is used by the StorageProof_Adapter to denote messages that can be sent to any target.
131+
(address target, bytes memory message) = abi.decode(_message, (address, bytes));
132+
if (target != address(0) && target != address(this)) {
133+
revert NotTarget();
134+
}
135+
136+
// Prevent replay attacks. The slot key should be a hash of the nonce associated with this calldata in the
137+
// HubPoolStore, which maps the nonce to the _value.
138+
if (executedMessages[_messageNonce]) {
139+
revert AlreadyExecuted();
140+
}
141+
executedMessages[_messageNonce] = true;
142+
emit RelayedCallData(_messageNonce, msg.sender);
143+
144+
_executeCalldata(message);
145+
}
146+
147+
/**
148+
* @notice This function is only callable by the owner and is used as an emergency fallback to execute
149+
* calldata to this SpokePool in the case where the light-client is not able to be updated.
150+
* @dev This function will revert if the last Helios update was less than ADMIN_UPDATE_BUFFER seconds ago.
151+
* @param _message The calldata to execute on this contract.
152+
*/
153+
function adminExecuteMessage(bytes memory _message) external onlyOwner validateInternalCalls {
154+
uint256 heliosHeadTimestamp = IHelios(helios).headTimestamp();
155+
if (heliosHeadTimestamp > block.timestamp || block.timestamp - heliosHeadTimestamp < ADMIN_UPDATE_BUFFER) {
156+
revert AdminUpdateTooCloseToLastHeliosUpdate();
157+
}
158+
_executeCalldata(_message);
159+
}
160+
161+
/**
162+
* @notice Computes the EVM storage slot key for a message nonce using the formula keccak256(key, slotIndex)
163+
* to find the storage slot for a value within a mapping(key=>value) at a slot index. We already know the
164+
* slot index of the relayMessageCallData mapping in the HubPoolStore.
165+
* @param _nonce The nonce associated with the message.
166+
* @return The computed storage slot key.
167+
*/
168+
function getSlotKey(uint256 _nonce) public pure returns (bytes32) {
169+
return keccak256(abi.encode(_nonce, HUB_POOL_STORE_CALLDATA_MAPPING_SLOT_INDEX));
170+
}
171+
172+
function _executeCalldata(bytes memory _calldata) internal {
173+
/// @custom:oz-upgrades-unsafe-allow delegatecall
174+
(bool success, ) = address(this).delegatecall(_calldata);
175+
if (!success) {
176+
revert DelegateCallFailed();
177+
}
178+
}
179+
180+
function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal override {
181+
if (_isCCTPEnabled() && l2TokenAddress == address(usdcToken)) {
182+
_transferUsdc(withdrawalRecipient, amountToReturn);
183+
} else {
184+
revert NotImplemented();
185+
}
186+
}
187+
188+
// Check that the admin call is only triggered by a executeMessage() call.
189+
function _requireAdminSender() internal view override {
190+
if (!_adminCallValidated) {
191+
revert AdminCallNotValidated();
192+
}
193+
}
194+
}

0 commit comments

Comments
 (0)