Skip to content

bug(anvil): invalid balance updates for CELO token on Anvil fork #10416

Open
@0xwetzo

Description

@0xwetzo

Component

Anvil

Have you ensured that all of these are up to date?

  • Foundry
  • Foundryup

What version of Foundry are you on?

forge Version: 1.1.0-nightly

What version of Foundryup are you on?

no output

What command(s) is the bug in?

No response

Operating System

macOS (Intel)

Describe the bug

I created a small repo for the code: https://github.com/0xwetzo/Anvil-Celo
The justfile helps to run the code step by step and make changes.

The issue seems to be with balance updates for native tokens like CELO or METIS that are both gas token and ERC20 at the same time.
The balances are not properly updating during the transaction, as seen in the traces, and are incorrect at the end of it too, using cast balance.

The function receives native CELO and calls a mock swap router that will swap ERC20 CELO for USDC, then sends remaining balance to caller as ERC20.

    function test(address router, bytes calldata data) external payable {
        uint256 balance = address(this).balance;

        NATIVE_TOKEN.approve(router, balance);
        
        (bool success, ) = router.call(data);

        require(success);

        uint256 newBalance = NATIVE_TOKEN.balanceOf(address(this));

        NATIVE_TOKEN.transfer(msg.sender, newBalance);
    }

The expected and observed balances are:

# Expected balances (CELO)  Init        Test called     Router called   Final transfer
# Owner                     10,000      9,999           9,999           9.999.5
# Contract                  0           1               0.5             0
# Router                    0           0               0.5             0.5

# Trace balances            Init        Test called     Router called   Final transfer
# Owner                     >= 1        >= 0            >= 0            >= 0.5
# Contract                  0           1               1 (wrong)       0
# Router                    0           0               0.5             0.5

# Cast balances             Init        Test called     Router called   Final transfer
# Owner                     10,000      ?               ?               9,999
# Contract                  0           ?               ?               1 (wrong)
# Router                    0           ?               ?               0 (wrong)

And the traces:

Traces:
  [97908] 0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7::test{value: 1000000000000000000}(0x0355B7B8cb128fA5692729Ab3AAa199C1753f726, 0xfe029156000000000000000000000000471ece3750da237f93b8e339c536989b8978a438000000000000000000000000ceba9300f2b948710d2653dd7b07f33a8b32118c00000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000000000075bcd15)
    ├─ [29958] 0x471EcE3750Da237f93B8E339c536989b8978a438::approve(0x0355B7B8cb128fA5692729Ab3AAa199C1753f726, 1000000000000000000 [1e18])
    │   ├─ [24552] 0xFa77529cD8F0dD86df93CC0d53ed9Bb5Db0ad6D5::approve(0x0355B7B8cb128fA5692729Ab3AAa199C1753f726, 1000000000000000000 [1e18]) [delegatecall]
    │   │   ├─ emit Approval(param0: 0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7, param1: 0x0355B7B8cb128fA5692729Ab3AAa199C1753f726, param2: 1000000000000000000 [1e18])
    │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    ├─ [53790] 0x0355B7B8cb128fA5692729Ab3AAa199C1753f726::swap(0x471EcE3750Da237f93B8E339c536989b8978a438, 0xcebA9300f2b948710d2653dD7B07f33A8B32118C, 500000000000000000 [5e17], 123456789 [1.234e8])
    │   ├─ [7907] 0x471EcE3750Da237f93B8E339c536989b8978a438::transferFrom(0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7, 0x0355B7B8cb128fA5692729Ab3AAa199C1753f726, 500000000000000000 [5e17])
    │   │   ├─ [6995] 0xFa77529cD8F0dD86df93CC0d53ed9Bb5Db0ad6D5::transferFrom(0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7, 0x0355B7B8cb128fA5692729Ab3AAa199C1753f726, 500000000000000000 [5e17]) [delegatecall]
    │   │   │   ├─ [0] 0x00000000000000000000000000000000000000fd::00000000(00000000000000008198f5d8f8cffe8f9c413d98a0a55aeb8ab9fbb70000000000000000000000000355b7b8cb128fa5692729ab3aaa199c1753f72600000000000000000000000000000000000000000000000006f05b59d3b20000)
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ emit Transfer(param0: 0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7, param1: 0x0355B7B8cb128fA5692729Ab3AAa199C1753f726, param2: 500000000000000000 [5e17])
    │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   ├─ [40563] 0xcebA9300f2b948710d2653dD7B07f33A8B32118C::transfer(0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7, 123456789 [1.234e8])
    │   │   ├─ [33363] 0xdA06D4e3F59fE2C8ff3077A9D50D5BE5E231BEcD::transfer(0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7, 123456789 [1.234e8]) [delegatecall]
    │   │   │   ├─ emit Transfer(param0: 0x0355B7B8cb128fA5692729Ab3AAa199C1753f726, param1: 0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7, param2: 123456789 [1.234e8])
    │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   └─ ← [Stop]
    ├─ [1423] 0x471EcE3750Da237f93B8E339c536989b8978a438::balanceOf(0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7) [staticcall]
    │   ├─ [523] 0xFa77529cD8F0dD86df93CC0d53ed9Bb5Db0ad6D5::balanceOf(0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7) [delegatecall]
    │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000de0b6b3a7640000
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000de0b6b3a7640000
    ├─ [4299] 0x471EcE3750Da237f93B8E339c536989b8978a438::transfer(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, 1000000000000000000 [1e18])
    │   ├─ [3393] 0xFa77529cD8F0dD86df93CC0d53ed9Bb5Db0ad6D5::transfer(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, 1000000000000000000 [1e18]) [delegatecall]
    │   │   ├─ [0] 0x00000000000000000000000000000000000000fd::00000000(00000000000000008198f5d8f8cffe8f9c413d98a0a55aeb8ab9fbb7000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000de0b6b3a7640000)
    │   │   │   └─ ← [Stop]
    │   │   ├─ emit Transfer(param0: 0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7, param1: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, param2: 1000000000000000000 [1e18])
    │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    └─ ← [Stop]


Transaction successfully executed.

I presume that the bug comes from Anvil and the special management of the hybrid token CELO.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions