#100DaysOfSolidity #079 Signature Replay Vulnerability in Solidity Smart Contracts
#100DaysOfSolidity Hacks & Tests 079 : “Signature Replay”
📝 Welcome to the 79th installment of the #100DaysOfSolidity series! In this article, we’re going to delve into a significant vulnerability known as “Signature Replay” that can potentially compromise the security of Ethereum smart contracts. We’ll explore a real-world example, discuss the inherent risks, and provide preventative techniques to mitigate this vulnerability.
🔍 Understanding the Vulnerability
The “Signature Replay” vulnerability arises when a contract accepts signed messages as a means of authorization without considering the uniqueness of these signatures. This can lead to a scenario where an attacker can reuse a valid signature to execute a function multiple times, even if the signer’s intention was to approve the transaction only once. Let’s examine a vulnerable smart contract that demonstrates this issue:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";
contract MultiSigWallet {
using ECDSA for bytes32;
address[2] public owners;
constructor(address[2] memory _owners) payable {
owners = _owners;
}
function deposit() external payable {}
function transfer(address _to, uint _amount, bytes[2] memory _sigs) external {
bytes32 txHash = getTxHash(_to, _amount);
require(_checkSigs(_sigs, txHash), "invalid sig");
(bool sent, ) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
function getTxHash(address _to, uint _amount) public view returns (bytes32) {
return keccak256(abi.encodePacked(_to, _amount));
}
function _checkSigs(
bytes[2] memory _sigs,
bytes32 _txHash
) private view returns (bool) {
bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();
for (uint i = 0; i < _sigs.length; i++) {
address signer = ethSignedHash.recover(_sigs[i]);
bool valid = signer == owners[i];
if (!valid) {
return false;
}
}
return true;
}
}
🚨 Vulnerability Analysis
In the above vulnerable contract, the `transfer` function allows the transfer of Ether to a specified address based on the provided signatures. The contract accepts an array of two signatures (`_sigs`) which are then verified against the owners’ addresses. However, there’s no consideration for the uniqueness of signatures, meaning an attacker can use the same valid signatures multiple times to repeatedly execute the transfer function, leading to potential financial losses.
💡 Preventative Techniques
To mitigate the “Signature Replay” vulnerability, it’s crucial to add nonce and contract address information while generating the transaction hash. Let’s take a look at the improved version of the contract with this preventive technique:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";
contract MultiSigWallet {
using ECDSA for bytes32;
address[2] public owners;
mapping(bytes32 => bool) public executed;
constructor(address[2] memory _owners) payable {
owners = _owners;
}
function deposit() external payable {}
function transfer(
address _to,
uint _amount,
uint _nonce,
bytes[2] memory _sigs
) external {
bytes32 txHash = getTxHash(_to, _amount, _nonce);
require(!executed[txHash], "tx executed");
require(_checkSigs(_sigs, txHash), "invalid sig");
executed[txHash] = true;
(bool sent, ) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
function getTxHash(
address _to,
uint _amount,
uint _nonce
) public view returns (bytes32) {
return keccak256(abi.encodePacked(address(this), _to, _amount, _nonce));
}
function _checkSigs(
bytes[2] memory _sigs,
bytes32 _txHash
) private view returns (bool) {
bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();
for (uint i = 0; i < _sigs.length; i++) {
address signer = ethSignedHash.recover(_sigs[i]);
bool valid = signer == owners[i];
if (!valid) {
return false;
}
}
return true;
}
}
🔐 Enhanced Security Measures
In the improved version, the `transfer` function now requires a nonce value to be provided. This nonce value, along with the contract address and other transaction parameters, is used to generate the transaction hash. Additionally, a `mapping` named `executed` is introduced to keep track of executed transactions based on their hashes. This prevents the reexecution of the same transaction.
🛡️ Conclusion
In this article, we’ve explored the “Signature Replay” vulnerability in Ethereum smart contracts. We’ve analyzed a vulnerable contract that allowed repeated execution of a function with the same valid signatures. To address this issue, we’ve discussed a preventive technique involving the use of nonces and contract addresses to create transaction hashes. By implementing these enhanced security measures, developers can protect their contracts from potential attacks that exploit this vulnerability. Always remember to follow best practices and stay updated on the latest security considerations in smart contract development.
🔗 Join the Solidity Community!
Are you passionate about Solidity and blockchain development? Join the #100DaysOfSolidity movement and stay updated with Medium articles by following @solidity101 .
Let’s build a safer and more secure blockchain ecosystem together!