Skip to content

Commit 1712840

Browse files
authored
Squashed commit of the following: (#980)
1 parent cfe639d commit 1712840

File tree

2 files changed

+89
-4
lines changed

2 files changed

+89
-4
lines changed

contracts/Polygon_SpokePool.sol

Lines changed: 16 additions & 4 deletions
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);

test/evm/hardhat/chain-specific-spokepools/Polygon_SpokePool.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,79 @@ describe("Polygon Spoke Pool", function () {
377377
polygonSpokePool.connect(relayer).multicall([executeLeafData[0], fillData[0], executeLeafData[1], fillData[1]])
378378
).to.be.reverted;
379379
});
380+
it("Cannot use nested multicalls", async function () {
381+
// In this test we attempt to stuff the `executeRelayerRefundLeaf` call inside a nested multicall to bypass
382+
// the _validateMulticallData check in PolygonSpokePool.sol. This should not be possible.
383+
const l2ChainId = await owner.getChainId();
384+
const leaves = buildRelayerRefundLeaves(
385+
[l2ChainId, l2ChainId], // Destination chain ID.
386+
[ethers.constants.Zero, ethers.constants.Zero], // amountToReturn.
387+
[dai.address, dai.address], // l2Token.
388+
[[], []], // refundAddresses.
389+
[[], []] // refundAmounts.
390+
);
391+
const tree = await buildRelayerRefundTree(leaves);
392+
393+
// Relay leaves to Spoke
394+
const relayRootBundleData = polygonSpokePool.interface.encodeFunctionData("relayRootBundle", [
395+
tree.getHexRoot(),
396+
mockTreeRoot,
397+
]);
398+
await polygonSpokePool.connect(fxChild).processMessageFromRoot(0, owner.address, relayRootBundleData);
399+
400+
// Deploy message handler and create fill with message that should succeed in isolation:
401+
const acrossMessageHandler = await createFake("AcrossMessageHandlerMock");
402+
await seedWallet(relayer, [dai], weth, toWei("2"));
403+
await dai.connect(relayer).approve(polygonSpokePool.address, toWei("2"));
404+
405+
const executeLeafData = [
406+
polygonSpokePool.interface.encodeFunctionData("executeRelayerRefundLeaf", [
407+
0,
408+
leaves[0],
409+
tree.getHexProof(leaves[0]),
410+
]),
411+
polygonSpokePool.interface.encodeFunctionData("executeRelayerRefundLeaf", [
412+
0,
413+
leaves[1],
414+
tree.getHexProof(leaves[1]),
415+
]),
416+
];
417+
const currentTime = (await polygonSpokePool.getCurrentTime()).toNumber();
418+
const relayData: V3RelayData = {
419+
depositor: addressToBytes(owner.address),
420+
recipient: addressToBytes(acrossMessageHandler.address),
421+
exclusiveRelayer: addressToBytes(zeroAddress),
422+
inputToken: addressToBytes(dai.address),
423+
outputToken: addressToBytes(dai.address),
424+
inputAmount: toWei("1"),
425+
outputAmount: toWei("1"),
426+
originChainId: originChainId,
427+
depositId: toBN(0),
428+
fillDeadline: currentTime + 7200,
429+
exclusivityDeadline: 0,
430+
message: "0x1234",
431+
};
432+
const fillData = [
433+
polygonSpokePool.interface.encodeFunctionData("fillRelay", [
434+
relayData,
435+
repaymentChainId,
436+
addressToBytes(relayer.address),
437+
]),
438+
polygonSpokePool.interface.encodeFunctionData("fillRelay", [
439+
{ ...relayData, depositId: 1 },
440+
repaymentChainId,
441+
addressToBytes(relayer.address),
442+
]),
443+
];
444+
445+
// Fills and execute leaf should succeed in isolation:
446+
await expect(polygonSpokePool.connect(relayer).estimateGas.multicall([...fillData])).to.not.be.reverted;
447+
await expect(polygonSpokePool.connect(relayer).estimateGas.multicall([...executeLeafData])).to.not.be.reverted;
448+
449+
const nestedMulticallData = [polygonSpokePool.interface.encodeFunctionData("multicall", [executeLeafData])];
450+
await expect(polygonSpokePool.connect(relayer).estimateGas.multicall([...fillData, ...nestedMulticallData])).to.be
451+
.reverted;
452+
});
380453
it("PolygonTokenBridger retrieves and unwraps tokens correctly", async function () {
381454
const l1ChainId = await owner.getChainId();
382455

0 commit comments

Comments
 (0)