Project

One World
NFTDeFi
15,000 USDC
View results
Submission Details
Severity: medium
Valid

`executeMetaTransaction()` failed txs are open to replay attacks

Vulnerability Details

Any transactions that fail based on some conditions that may change in the future are not safe to execute again later (e.g. transactions that are based on other actions, or time-dependent, etc.).

In the current implementation, if the low-level call fails, the whole tx is rolled back, leaving nonces[userAddress] unchanged.

https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/meta-transaction/NativeMetaTransaction.sol#L33-L68

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "lib/forge-std/src/Test.sol";
import "contracts/meta-transaction/NativeMetaTransaction.sol";
contract MetaTransactionTest is Test {
NativeMetaTransaction public metaTxContract;
address public userAddress;
uint256 public initialNonce;
struct MetaTransaction {
uint256 nonce;
address from;
bytes functionSignature;
}
mapping(address => uint256) nonces;
// Set up contract and initial state
function setUp() public {
metaTxContract = new NativeMetaTransaction();
userAddress = address(0x123); // Используем произвольный адрес для теста
// Устанавливаем начальный nonce для пользователя
initialNonce = metaTxContract.getNonce(userAddress);
}
// Test: Does the nonce increase after a failed call
function testNonceIncreasesAfterFailedCall() public {
// Getting the user's nonce before performing a meta-transaction
uint256 nonceBefore = nonces[userAddress];
// Checking that the user's initial nonce matches what is expected
assertEq(nonceBefore, initialNonce, "Initial nonce should match the setup value");
// Creating a signature for a non-existent function
bytes memory functionSignature = abi.encodeWithSignature(
"nonExistentFunction()"
);
bytes32 sigR;
bytes32 sigS;
uint8 sigV;
// Пробуем выполнить executeMetaTransaction (ожидаем ошибку)
vm.startPrank(userAddress);
try
metaTxContract.executeMetaTransaction(
userAddress,
functionSignature,
sigR,
sigS,
sigV
)
{
// We expect the transaction to complete with an error
revert("Expected the transaction to fail");
} catch (bytes memory reason) {
// Убедимся, что ошибка действительно произошла, например, по причине неуспешного вызова
assertTrue(reason.length > 0, "Error message should be present in case of failure");
}
vm.stopPrank();
// Getting a nonce after performing a meta-transaction
uint256 nonceAfter = nonces[userAddress];
// Проверяем, что nonce увеличился на 1
assertEq(
nonceAfter,
nonceBefore + 1,
"Nonce should have increased by 1 after failed call"
);
//Additional check: if the call failed, the nonce should have been incremented
//Make sure the increment happens even if the transaction fails
assertEq(nonceAfter, initialNonce + 1, "Nonce should have increased by 1 from initial value");
}
}
`forge test --match-contract "MetaTransactionTes" --match-test "testNonceIncreasesAfterFailedCall" -vvvv
[⠊] Compiling...
[⠆] Compiling 1 files with Solc 0.8.22
[⠰] Solc 0.8.22 finished in 1.23s
Compiler run successful!
Ran 1 test for test/MetaTransactionTest.t.sol:MetaTransactionTest
[FAIL: Nonce should have increased by 1 after failed call: 0 != 1] testNonceIncreasesAfterFailedCall() (gas: 27963)
Traces:
[504155] MetaTransactionTest::setUp()
├─ [439037] → new NativeMetaTransaction@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 2079 bytes of code
├─ [2581] NativeMetaTransaction::getNonce(0x0000000000000000000000000000000000000123) [staticcall]
│ └─ ← [Return] 0
└─ ← [Stop]
[27963] MetaTransactionTest::testNonceIncreasesAfterFailedCall()
├─ [0] VM::assertEq(0, 0, "Initial nonce should match the setup value") [staticcall]
│ └─ ← [Return]
├─ [0] VM::startPrank(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [9405] NativeMetaTransaction::executeMetaTransaction(0x0000000000000000000000000000000000000123, 0x15667403, 0x0000000000000000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000000, 0)
│ ├─ [3000] PRECOMPILES::ecrecover(0x5dd320a1f748b4a5208005c389e087339bc95ea1cb49e2a4fd88aa7d229dbbc3, 0, 0, 0) [staticcall]
│ │ └─ ← [Return]
│ └─ ← [Revert] revert: Signer and signature do not match
├─ [0] VM::assertTrue(true, "Error message should be present in case of failure") [staticcall]
│ └─ ← [Return]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::assertEq(0, 1, "Nonce should have increased by 1 after failed call") [staticcall]
│ └─ ← [Revert] Nonce should have increased by 1 after failed call: 0 != 1
└─ ← [Revert] Nonce should have increased by 1 after failed call: 0 != 1
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 6.39ms (1.42ms CPU time)
Ran 1 test suite in 1.03s (6.39ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/MetaTransactionTest.t.sol:MetaTransactionTest
[FAIL: Nonce should have increased by 1 after failed call: 0 != 1] testNonceIncreasesAfterFailedCall() (gas: 27963)`

Impact

As a result, the same tx can be replayed by anyone using the same signature.

Tools Used

foundry

Recommendations

txs should still increase the nonce anyway.

Updates

Lead Judging Commences

0xbrivan2 Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

hajime Submitter
about 1 year ago
0xbrivan2 Lead Judge
about 1 year ago
0xbrivan2 Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

failed meta transactions are replayable

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!