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.
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;
function setUp() public {
metaTxContract = new NativeMetaTransaction();
userAddress = address(0x123);
initialNonce = metaTxContract.getNonce(userAddress);
}
function testNonceIncreasesAfterFailedCall() public {
uint256 nonceBefore = nonces[userAddress];
assertEq(nonceBefore, initialNonce, "Initial nonce should match the setup value");
bytes memory functionSignature = abi.encodeWithSignature(
"nonExistentFunction()"
);
bytes32 sigR;
bytes32 sigS;
uint8 sigV;
vm.startPrank(userAddress);
try
metaTxContract.executeMetaTransaction(
userAddress,
functionSignature,
sigR,
sigS,
sigV
)
{
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();
uint256 nonceAfter = nonces[userAddress];
assertEq(
nonceAfter,
nonceBefore + 1,
"Nonce should have increased by 1 after failed call"
);
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)`
As a result, the same tx can be replayed by anyone using the same signature.
txs should still increase the nonce anyway.