// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/access/Ownable.sol";
contract RewardSystem is Ownable {
mapping(address => uint256) public rewards;
mapping(address => uint256) public nonces;
constructor() Ownable(msg.sender) {}
// For PoC simplicity, we don't use real signatures
function claimReward(address user, uint256 amount, uint256 nonce, bytes memory signature) external {
require(rewards[user] >= amount, "Insufficient reward balance");
require(nonces[user] == nonce, "Invalid nonce");
// Vulnerability: Signature can be replayed once contract has funds
bytes32 messageHash = keccak256(abi.encode(
user,
amount,
nonce
));
// For PoC, use a simple signature check
bytes32 signedHash = abi.decode(signature, (bytes32));
require(signedHash == messageHash, "Invalid signature");
// Attempt to transfer reward
rewards[user] -= amount;
(bool success,) = msg.sender.call{value: amount}("");
// Vulnerability: Nonce is only incremented if transfer succeeds
if (success) {
nonces[user]++;
} else {
revert("Transfer failed");
}
}
// Helper function to add rewards - only owner can call
function addReward(address user, uint256 amount) external onlyOwner {
rewards[user] += amount;
}
// Helper function to receive ETH
receive() external payable {}
}
Функция claimReward увеличивает значение nonce пользователя только в случае успешного перевода ETH. Если в контракте нет ETH, msg.sender.call завершится с ошибкой, транзакция будет отменена, а значение nonces[user] не будет увеличено. Подпись пользователя, которая была действительна для этого значения nonce, остается действительной.
Злоумышленник может отслеживать эти неудачные заявки. Как только контракт RewardSystem будет профинансирован, злоумышленник может повторить исходную транзакцию пользователя, направив вознаграждение на свой собственный адрес.
Как исправить: используйте nonce в каждом пути выполнения!
Убедитесь, что nonce используется (помечается как использованный) в каждом пути выполнения, независимо от того, завершилось ли оно успешно или неудачно. Лучшая практика заключается в том, чтобы использовать nonce сразу после его проверки и убедиться, что транзакция не будет отменена после этого момента. Таким образом, даже если транзакция завершится неудачно, nonce все равно будет увеличен, что предотвратит атаки повторного воспроизведения.
// Corrected claimReward function (inside RewardSystem)
function claimReward(address user, uint256 amount, uint256 nonce, bytes memory signature) external {
require(rewards[user] >= amount, "Insufficient reward balance");
require(nonces[user] == nonce, "Invalid nonce");
// For PoC, use a simple signature check
bytes32 messageHash = keccak256(abi.encode(user, amount, nonce));
bytes32 signedHash = abi.decode(signature, (bytes32));
require(signedHash == messageHash, "Invalid signature");
// REMEDIATION 1: Consume nonce right after validating it
nonces[user]++;
rewards[user] -= amount;
(bool success,) = msg.sender.call{value: amount}("");
if (!success) {
// Revert the reward deduction if transfer failed
rewards[user] += amount;
}
// REMEDIATION 2: No revert to ensure nonce is consumed
}
С этим исправлением, даже если передача не удалась, nonce становится недействительным. Злоумышленник не может повторно использовать подпись, поскольку контракт теперь будет ожидать nonce + 1.
#replay