Skip to content

Commit 7b6e509

Browse files
committed
Merge branch 'master' into pablo/acx-3907-fill-test-with-lookup-table
2 parents b0153f5 + 1c4ad22 commit 7b6e509

File tree

15 files changed

+449
-209
lines changed

15 files changed

+449
-209
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);

scripts/svm/fakeFillWithRandomDistribution.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
calculateRelayHashUint8Array,
2121
getSpokePoolProgram,
2222
loadFillRelayParams,
23-
sendTransactionWithLookupTable,
23+
sendTransactionWithLookupTableV1,
2424
} from "../../src/svm/web3-v1";
2525
import { FillDataParams, FillDataValues } from "../../src/types/svm";
2626

@@ -219,7 +219,7 @@ async function fillV3RelayToRandom(): Promise<void> {
219219
.instruction();
220220

221221
// Fill using the ALT.
222-
const { txSignature } = await sendTransactionWithLookupTable(
222+
const { txSignature } = await sendTransactionWithLookupTableV1(
223223
provider.connection,
224224
[approveInstruction, fillInstruction],
225225
signer

src/svm/web3-v1/transactionUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
/**
1313
* Sends a transaction using an Address Lookup Table for large numbers of accounts.
1414
*/
15-
export async function sendTransactionWithLookupTable(
15+
export async function sendTransactionWithLookupTableV1(
1616
connection: Connection,
1717
instructions: TransactionInstruction[],
1818
sender: Keypair

src/svm/web3-v2/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./solanaProgramUtils";
22
export * from "./transactionUtils";
3+
export * from "./types";

src/svm/web3-v2/solanaProgramUtils.ts

Lines changed: 2 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,6 @@
11
import { BorshEventCoder, Idl, utils } from "@coral-xyz/anchor";
2-
import web3, {
3-
Address,
4-
AddressesByLookupTableAddress,
5-
appendTransactionMessageInstruction,
6-
appendTransactionMessageInstructions,
7-
Commitment,
8-
compressTransactionMessageUsingAddressLookupTables as compressTxWithAlt,
9-
getSignatureFromTransaction,
10-
GetSignaturesForAddressApi,
11-
GetTransactionApi,
12-
IInstruction,
13-
KeyPairSigner,
14-
pipe,
15-
Rpc,
16-
RpcSubscriptions,
17-
RpcTransport,
18-
sendAndConfirmTransactionFactory,
19-
Signature,
20-
SignatureNotificationsApi,
21-
signTransactionMessageWithSigners,
22-
SlotNotificationsApi,
23-
SolanaRpcApiFromTransport,
24-
} from "@solana/kit";
25-
26-
import {
27-
fetchAddressLookupTable,
28-
findAddressLookupTablePda,
29-
getCreateLookupTableInstructionAsync,
30-
getExtendLookupTableInstruction,
31-
} from "@solana-program/address-lookup-table";
32-
import { createDefaultTransaction, signAndSendTransaction } from "../../../test/svm/utils";
2+
import web3, { Address, Commitment, GetSignaturesForAddressApi, GetTransactionApi, Signature } from "@solana/kit";
3+
import { RpcClient } from "./types";
334

345
type GetTransactionReturnType = ReturnType<GetTransactionApi["getTransaction"]>;
356

@@ -170,76 +141,3 @@ export async function readFillEventFromFillStatusPda(
170141
const events = await readEvents(client, signatures[signatures.length - 1].signature, programId, programIdl);
171142
return { event: events[0], slot: Number(signatures[signatures.length - 1].slot) };
172143
}
173-
174-
export async function createAlt(client: RpcClient, authority: KeyPairSigner): Promise<Address> {
175-
const recentSlot = await client.rpc.getSlot({ commitment: "finalized" }).send();
176-
177-
const [alt] = await findAddressLookupTablePda({
178-
authority: authority.address,
179-
recentSlot,
180-
});
181-
182-
const createAltIx = await getCreateLookupTableInstructionAsync({
183-
authority,
184-
recentSlot,
185-
});
186-
187-
await pipe(
188-
await createDefaultTransaction(client, authority),
189-
(tx) => appendTransactionMessageInstruction(createAltIx, tx),
190-
(tx) => signAndSendTransaction(client, tx)
191-
);
192-
193-
return alt;
194-
}
195-
196-
export async function extendAlt(client: RpcClient, authority: KeyPairSigner, alt: Address, addresses: Address[]) {
197-
const extendAltIx = getExtendLookupTableInstruction({
198-
address: alt,
199-
authority,
200-
payer: authority,
201-
addresses,
202-
});
203-
204-
await pipe(
205-
await createDefaultTransaction(client, authority),
206-
(tx) => appendTransactionMessageInstruction(extendAltIx, tx),
207-
(tx) => signAndSendTransaction(client, tx)
208-
);
209-
210-
const altAccount = await fetchAddressLookupTable(client.rpc, alt);
211-
212-
const addressesByLookupTableAddress: AddressesByLookupTableAddress = {};
213-
addressesByLookupTableAddress[alt] = altAccount.data.addresses;
214-
215-
// Delay a second here to let lookup table warm up
216-
await sleep(1000);
217-
218-
return addressesByLookupTableAddress;
219-
}
220-
221-
async function sleep(ms: number) {
222-
return new Promise((resolve) => setTimeout(resolve, ms));
223-
}
224-
225-
export async function sendTransactionWithLookupTable(
226-
client: RpcClient,
227-
payer: KeyPairSigner,
228-
instructions: IInstruction[],
229-
addressesByLookupTableAddress: AddressesByLookupTableAddress
230-
) {
231-
return pipe(
232-
await createDefaultTransaction(client, payer),
233-
(tx) => appendTransactionMessageInstructions(instructions, tx),
234-
(tx) => compressTxWithAlt(tx, addressesByLookupTableAddress),
235-
(tx) => signTransactionMessageWithSigners(tx),
236-
async (tx) => {
237-
const signedTx = await tx;
238-
await sendAndConfirmTransactionFactory(client)(signedTx, {
239-
commitment: "confirmed",
240-
skipPreflight: false,
241-
});
242-
return getSignatureFromTransaction(signedTx);
243-
}
244-
);
245-
}

src/svm/web3-v2/transactionUtils.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,53 @@
11
import {
2+
Address,
23
AddressesByLookupTableAddress,
4+
appendTransactionMessageInstruction,
35
appendTransactionMessageInstructions,
6+
Commitment,
7+
CompilableTransactionMessage,
48
compressTransactionMessageUsingAddressLookupTables as compressTxWithAlt,
9+
createTransactionMessage,
510
getSignatureFromTransaction,
611
IInstruction,
712
KeyPairSigner,
813
pipe,
914
sendAndConfirmTransactionFactory,
15+
setTransactionMessageFeePayerSigner,
16+
setTransactionMessageLifetimeUsingBlockhash,
1017
signTransactionMessageWithSigners,
18+
TransactionMessageWithBlockhashLifetime,
19+
TransactionSigner,
1120
} from "@solana/kit";
1221

13-
import { createDefaultTransaction } from "../../../test/svm/utils";
22+
import {
23+
fetchAddressLookupTable,
24+
findAddressLookupTablePda,
25+
getCreateLookupTableInstructionAsync,
26+
getExtendLookupTableInstruction,
27+
} from "@solana-program/address-lookup-table";
28+
import { RpcClient } from "./types";
29+
30+
export const signAndSendTransaction = async (
31+
rpcClient: RpcClient,
32+
transactionMessage: CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime,
33+
commitment: Commitment = "confirmed"
34+
) => {
35+
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
36+
const signature = getSignatureFromTransaction(signedTransaction);
37+
await sendAndConfirmTransactionFactory(rpcClient)(signedTransaction, {
38+
commitment,
39+
});
40+
return signature;
41+
};
1442

43+
export const createDefaultTransaction = async (rpcClient: RpcClient, signer: TransactionSigner) => {
44+
const { value: latestBlockhash } = await rpcClient.rpc.getLatestBlockhash().send();
45+
return pipe(
46+
createTransactionMessage({ version: 0 }),
47+
(tx) => setTransactionMessageFeePayerSigner(signer, tx),
48+
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
49+
);
50+
};
1551
export async function sendTransactionWithLookupTable(
1652
client: RpcClient,
1753
payer: KeyPairSigner,
@@ -33,3 +69,55 @@ export async function sendTransactionWithLookupTable(
3369
}
3470
);
3571
}
72+
73+
export async function createLookupTable(client: RpcClient, authority: KeyPairSigner): Promise<Address> {
74+
const recentSlot = await client.rpc.getSlot({ commitment: "finalized" }).send();
75+
76+
const [alt] = await findAddressLookupTablePda({
77+
authority: authority.address,
78+
recentSlot,
79+
});
80+
81+
const createAltIx = await getCreateLookupTableInstructionAsync({
82+
authority,
83+
recentSlot,
84+
});
85+
86+
await pipe(
87+
await createDefaultTransaction(client, authority),
88+
(tx) => appendTransactionMessageInstruction(createAltIx, tx),
89+
(tx) => signAndSendTransaction(client, tx)
90+
);
91+
92+
return alt;
93+
}
94+
95+
export async function extendLookupTable(
96+
client: RpcClient,
97+
authority: KeyPairSigner,
98+
alt: Address,
99+
addresses: Address[]
100+
) {
101+
const extendAltIx = getExtendLookupTableInstruction({
102+
address: alt,
103+
authority,
104+
payer: authority,
105+
addresses,
106+
});
107+
108+
await pipe(
109+
await createDefaultTransaction(client, authority),
110+
(tx) => appendTransactionMessageInstruction(extendAltIx, tx),
111+
(tx) => signAndSendTransaction(client, tx)
112+
);
113+
114+
const altAccount = await fetchAddressLookupTable(client.rpc, alt);
115+
116+
const addressesByLookupTableAddress: AddressesByLookupTableAddress = {};
117+
addressesByLookupTableAddress[alt] = altAccount.data.addresses;
118+
119+
// Delay a second here to let lookup table warm up
120+
await new Promise((resolve) => setTimeout(resolve, 1000));
121+
122+
return addressesByLookupTableAddress;
123+
}

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)