hats-finance / Euro-Dollar-0xa4ccd3b6daa763f729ad59eae75f9cbff7baf2cd

Audit competition repository for Euro-Dollar (0xa4ccd3b6daa763f729ad59eae75f9cbff7baf2cd)
https://hats.finance
MIT License
3 stars 2 forks source link

Insufficient Signature Verification in Token Burn #64

Open hats-bug-reporter[bot] opened 2 weeks ago

hats-bug-reporter[bot] commented 2 weeks ago

Github username: -- Twitter username: -- Submission hash (on-chain): 0xefafa8a3a9fead0f3a0102398da97fd49fc9afb5080b785d17f480f18d36fd1d Severity: high

Description: Description\ The USDE token contract implements a burn function that accepts a signature as authorization.

However, the signature verification is flawed as it only validates that the signature is from the correct address without verifying that the signed message matches the actual burn parameters.

The current implementation allows any signed message from the token holder to authorize a burn of any amount, enabling potential replay attacks and unauthorized burns.

This is particularly dangerous as it could lead to token holders losing more tokens than they authorized.

function burn(
    address from,
    uint256 amount,
    bytes32 h,
    bytes memory signature
) public onlyRole(BURN_ROLE) returns (bool) {
    require(from.isValidSignatureNow(h, signature), "signature/hash does not match");
    _burn(from, amount);
    return true;
}

Attack Scenario\ Alice signs a message authorizing a burn of 100 tokens

Bob, who has the BURN_ROLE, sees this signature

Bob can use this signature to:

Burn more than 100 tokens from Alice's account

Replay the signature multiple times to repeatedly burn tokens

Use the signature on different chains in case of network forks

Alice loses more tokens than she authorized

  1. Revised Code File (Optional)
    
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.21;

contract USDE { // Add nonce tracking for burn signatures mapping(address => uint256) public burnNonces;

// Domain separator for EIP-712
bytes32 private immutable DOMAIN_SEPARATOR;

constructor() {
    DOMAIN_SEPARATOR = keccak256(
        abi.encode(
            keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
            keccak256(bytes("EuroDollar")),
            keccak256(bytes("1")),
            block.chainid,
            address(this)
        )
    );
}

// Struct for burn authorization
struct BurnAuthorization {
    address from;
    uint256 amount;
    uint256 nonce;
    uint256 deadline;
}

// TypeHash for EIP-712
bytes32 private constant BURN_TYPEHASH = keccak256(
    "BurnAuthorization(address from,uint256 amount,uint256 nonce,uint256 deadline)"
);

function burnWithSignature(
    address from,
    uint256 amount,
    uint256 deadline,
    bytes memory signature
) public onlyRole(BURN_ROLE) returns (bool) {
    require(block.timestamp <= deadline, "Signature expired");

    // Recreate the signed message hash
    bytes32 structHash = keccak256(abi.encode(
        BURN_TYPEHASH,
        from,
        amount,
        burnNonces[from],
        deadline
    ));

    bytes32 hash = keccak256(abi.encodePacked(
        "\x19\x01",
        DOMAIN_SEPARATOR,
        structHash
    ));

    // Verify signature
    require(from.isValidSignatureNow(hash, signature), "Invalid signature");

    // Increment nonce before burn
    burnNonces[from]++;

    // Perform burn
    _burn(from, amount);

    return true;
}

}



The revised code implements EIP-712 structured signing with:

Proper message parameter validation

Nonce tracking to prevent replay attacks

Deadline parameter to limit signature validity

Chain ID validation through domain separator
AndreiMVP commented 2 weeks ago

Similar to https://github.com/hats-finance/Euro-Dollar-0xa4ccd3b6daa763f729ad59eae75f9cbff7baf2cd/issues/15