When developing smart contracts on the Ethereum blockchain, it’s essential to consider potential security vulnerabilities in order to make the contract safe. One important vulnerability comes from is signature replay attacks, which can result in malicious actors gaining unauthorized access to contract functions. The aim of this article is to explain what these attacks are and provide possible mitigation techniques in Solidity.
What is Signature Replay?
To begin with, it is important to have a brief sum up of what digital signatures are. They are a way, based on a public key cryptography mathematical scheme, of presenting authenticity, non-repudiation and integrity of digital messages or documents.
In blockchain they have a fundamental role when it comes to authenticating transactions. It enables nodes to verify the validity of each submitted transaction by using the provided signature and the public key of the account that submitted it.
Also, they are often verified in smart contracts directly in order to allow having multiple accounts as the verifiers of certain actions. This is achieved by signing certain messages off-chain (or sometimes signatures are generated by another smart contract) and submitting the signatures as a parameter to some smart contract method. This is the technique used for meta transactions (gas-less transactions).
However, besides being a useful technique, it has an important vulnerability which is the potential of having a signature replay attack. Signature replay occurs when an attacker takes a valid signature from one transaction and uses it to authenticate a different transaction.
Why are Signature Replay Attacks Dangerous?
Attackers can use signature replay to gain unauthorized access to contract functions, such as transferring funds or changing contract state. This can result in significant financial losses and damage to the reputation of the contract and its developers.
Example code of vulnerable contract
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract Vulnerable{
using ECDSA for bytes32;
address public owner;
constructor(){
owner=msg.sender;
}
receive() external payable{}
function transfer(address payable to, uint amount, bytes memory signature) external {
bytes32 hashData = keccak256(abi.encodePacked(to, amount));
address signer = hashData.toEthSignedMessageHash().recover(signature);
require(signer == owner,"Signer is not owner");
to.transfer(amount);
}
}
transfer
method requires owner signature to send the requested amount to the intended recipient.
Attack scenario
The message signed in the above code, using ECDSA algorithm, only contains the recipient and the amount. Thus, there is nothing that could be used to prevent the same signature of being used multiple times. There is no way of checking the uniqueness of the signed message or if it has already been used.
The owner could have signed a transfer of 10 ether to certain address, with the intention of only allowing a singular transaction of that characteristics. Then, once a relayer calls transfer function with owner signature, the associated transaction would be publicly readable on the blockchain. This way, an attacker could copy the signature, submit multiple identical transactions and end up draining contract funds.
Preventing Signature Replay in Solidity
One preventive technique is signing messages with a nonce and the address of the contract. Nonces, acronym for “number used only once”, would make each signed message unique. Also, by including the contract address in the signed message, the signature cannot be used to authenticate transactions for other contracts.
Fixed version of the above contract using nonces
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract Vulnerable{
using ECDSA for bytes32;
address public owner;
uint private currentNonce;
constructor(){
owner=msg.sender;
}
receive() external payable{}
function transfer(address payable to, uint amount, bytes memory signature, uint nonce) external {
require(nonce == currentNonce ++,"Invalid nonce");
bytes32 hashData = keccak256(abi.encodePacked(to, amount,nonce));
address signer = hashData.toEthSignedMessageHash().recover(signature);
require(signer == owner,"Signer is not owner");
currentNonce ++;
to.transfer(amount);
}
}
Now, transfer
function requires a sequence number each time it is called and this unique number is used in the signed message. This way, each signature will only allow one successful transfer execution and any attempt of replaying it will revert with “Invalid nonce”.
Conclusion
Signature replay attacks represents a serious security vulnerability in Solidity smart contracts, but they can be prevented by creating unique signatures through the usage of nonces and contract addresses as it has been explained in this article. It is important that practices for securing contracts are followed in order to offer users safe contracts to interact with and to be a trusted developer
Posted in Blockchain, Ethereum, Smart Contract, Solidity, Technologies