Closed BlinkyStitt closed 4 years ago
I noticed that the call_trace is shorter than I expect:
>>> arbitrage_tx.call_trace()
Call trace for '0x396df1e2e96ac4779df1dd6379c3684fc952f8a3e104d75aee048d1f2ea0b057':
ArgobytesOwnedVault.atomicArbitrage 0:12892 (0x0F8B800bb499Be1A538D5d18556163C1C42a83E0)
├─GasTokenBurner.startFreeGasTokens 470:491
├─AccessControl.hasRole 562:647
│ └─EnumerableSet.contains 589:639
│ └─EnumerableSet._contains 602:631
├─UniversalERC20.universalBalanceOf 787:830
│ └─UniversalERC20.isETH 793:812
├─UniversalERC20.universalTransfer 852:1004
│ ├─UniversalERC20.isETH 865:884
│ └─ArgobytesAtomicTrade 956:969 (0x59D67dC8DF1C4d1ef77787b1a95954C9465d4A14)
├─ArgobytesAtomicTrade.atomicTrade 1490:11507 (0x59D67dC8DF1C4d1ef77787b1a95954C9465d4A14)
│ ├─UniversalERC20.universalBalanceOf 1943:1986
│ │ └─UniversalERC20.isETH 1949:1968
│ ├─ArgobytesAtomicTrade.executeSolo 2163:11507
│ │ ├─UniversalERC20.universalBalanceOf 4095:4138
│ │ │ └─UniversalERC20.isETH 4101:4120
│ │ ├─UniversalERC20.universalTransfer 4179:4331
│ │ │ ├─UniversalERC20.isETH 4192:4211
│ │ │ └─ExampleAction 4283:4296 (0x5408F18ac36973Ed918edE34D8889d590f0aC537)
│ │ ├─ExampleAction.sweep 4585:5046 (0x5408F18ac36973Ed918edE34D8889d590f0aC537)
│ │ │ ├─UniversalERC20.universalBalanceOf 4814:4857
│ │ │ │ └─UniversalERC20.isETH 4820:4839
│ │ │ └─UniversalERC20.universalTransfer 4885:5037
│ │ │ ├─UniversalERC20.isETH 4898:4917
│ │ │ └─UniswapV1Action 4989:5002 (0xEAD0eA5a65D23E271097695c5A01BAF047496609)
│ │ ├─UniswapV1Action.tradeEtherToToken 5396:7430 (0xEAD0eA5a65D23E271097695c5A01BAF047496609)
│ │ │ └─<UnknownContract>.0xad65d76d 5973:7418 (0x97deC872013f6B5fB443861090ad931542878126)
│ │ │ └─<UnknownContract>.0xad65d76d 5984:7415 (0x2157A7894439191e520825fe9399aB8655E0f708)
│ │ │ ├─FiatTokenProxy.balanceOf 6186:6495 (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)
│ │ │ │ ├─Proxy._fallback 6230:6495
│ │ │ │ │ ├─AdminUpgradeabilityProxy._willFallback 6234:6273
│ │ │ │ │ │ ├─AdminUpgradeabilityProxy._admin 6238:6253
│ │ │ │ │ │ └─Proxy._willFallback 6269:6271
│ │ │ │ │ ├─UpgradeabilityProxy._implementation 6278:6293
│ │ │ │ │ ├─Proxy._delegate 6296:6495
│ │ │ │ │ │ └─FiatTokenV1.balanceOf 6308:6482 (0x0882477e7895bdC5cea7cB1552ed914aB157Fe56)
│ │ │ └─FiatTokenProxy.transfer 6822:7410 (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)
│ │ │ └─Proxy._fallback 6866:7410
│ │ │ ├─AdminUpgradeabilityProxy._willFallback 6870:6909
│ │ │ │ ├─AdminUpgradeabilityProxy._admin 6874:6889
│ │ │ │ └─Proxy._willFallback 6905:6907
│ │ │ ├─UpgradeabilityProxy._implementation 6914:6929
│ │ │ └─Proxy._delegate 6932:7410
│ │ │ └─FiatTokenV1.transfer 6944:7396 (0x0882477e7895bdC5cea7cB1552ed914aB157Fe56)
│ │ │ ├─SafeMath.sub 7278:7299
│ │ │ └─SafeMath.add 7350:7374
│ │ ├─Strings.toString 7458:7643
│ │ └─Strings2.toString 7650:10726
├─GasTokenBurner.endFreeGasTokens 11653:11731
│ └─SafeMath.sub 11671:11723
└─GasTokenBurner._freeGasTokens 11739:12644
└─ChiToken.freeUpTo 11870:12535 (0x0000000000004946c0e9F43F4Dee607b0eF1fA1c)
├─ERC20WithoutTotalSupply.balanceOf 11946:11967
├─Math.min 11970:11984
└─ChiToken.free 11987:12513
├─ERC20WithoutTotalSupply._burn 11994:12103
│ └─SafeMath.sub 12039:12056
└─ChiToken._destroyChildren 12108:12509
├─ChiToken.computeAddress2 12125:12190
├─<UnknownContract>.0x00000000 12211:12218 (0xdB392efce2Cf6AdE5505A2Cb2B438Df1F0c983b4)
├─ChiToken.computeAddress2 12253:12318
├─<UnknownContract>.0x00000000 12339:12346 (0x03162B21b8D7F054584A2882d4CC653DD22c1c84)
├─ChiToken.computeAddress2 12381:12446
└─<UnknownContract>.0x00 12467:12474 (0x6975B3E13bbc6A26b8A74db1C49A2e306d4E9D84)
>>>
It ends with ChiToken, like I expect, but there should be more calls to UniswapV1Action
in the call_trace. Right now it is only showing tradeEtherToToken
, but there should also be tradeTokenToToken
and tradeTokenToEth
. I'm sure these are being called, otherwise the test wouldn't see an increase in balance at the end.
So I copied this test and changed it to work for kyber. And the kyber test had the correct return_value set! So I think maybe uniswap using vyper might be related.
If I skip the gas token burning and shorten the amount of calls, it gives a return_value. So I think this is related to some complexities in freeing GST2/CHI
Should I open this on ganache-core instead?
@WyseNynja I was able to use Github's transfer function to move this to ganache-core as we do try to keep core functionality separate from the CLI repo. (I think we're thinking of a monorepo to simplify all of this in the future)
@WyseNynja I'm having issues with just running your setup. When I finally run the test command you provided, I get:
tests/test_uniswap_v1_arbitrage.py E
@pytest.fixture(scope="session")
def dai_erc20():
> yield Contract.from_explorer("0x6b175474e89094c44da98b954eedeac495271d0f")
E ValueError: Explorer API not set for this network
tests/conftest.py:80: ValueError
The only thing I changed was brownie-network-config.yaml@38
I changed the fork: mainnet
to fork: https://mainnet.infura.io/v3/<my-infura-key>
here's my bash history:
nvm use 12
./scripts/setup.sh
./venv/bin/brownie networks import brownie-network-config.yaml True
. ./scripts/activate
brownie compile
npm i -g ganache-cli
BURN_GAS_TOKEN=0 ./scripts/test-deploy.sh
./scripts/test.sh tests/test_uniswap_v1_arbitrage.py -I
was i supposed to run both BURN_GAS_TOKEN=1 ...
and BURN_GAS_TOKEN=0 ...
?
I think you want to leave it as fork: mainnet
and change the actual mainnet
block to use your infura key. (This line: https://github.com/SatoshiAndKin/argobytes-contracts-brownie/blob/master/brownie-network-config.yaml#L13)
Since you've already run brownie networks import
, you can edit ~/.brownie/network-config.yaml
directly.
BURN_GAS_TOKEN just enables or disable the gas token minting/freeing. It shouldn't change this error.
I upgraded brownie and all the other dependencies and also upgraded to ganache-cli 6.10.2-forking.0.
I've changed things around and am using LGT instead of GST2 now, but that hasn't changed the issue.
The test trades ether to token, a token to another token, and that token back to ether. The transaction's status claims success and there is no revert reason, but the call_trace
only includes the first trade (ether to token).
$ ./scripts/test.sh tests/test_uniswap_v1_arbitrage.py -I
+ [ -z /home/ski/code/argobytes-contracts-brownie/venv ]
+ brownie test --network mainnet-fork tests/test_uniswap_v1_arbitrage.py -I
Brownie v1.10.6 - Python development framework for Ethereum
============================= test session starts ==============================
platform linux -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
Test order randomisation NOT enabled. Enable with --random-order or --random-order-bucket=<bucket_type>
rootdir: /home/ski/code/argobytes-contracts-brownie
plugins: eth-brownie-1.10.6, web3-5.11.1, hypothesis-5.26.0, timeout-1.4.2, random-order-1.0.4, xdist-1.34.0, forked-1.3.0
collecting ...
Launching 'ganache-cli --accounts 10 --hardfork istanbul --fork ws://127.0.0.1:8546 --gasLimit 12000000 --mnemonic opinion adapt negative bone suit ill fossil alcohol razor script damp fold --port 8575'...
collected 1 item
tests/test_uniswap_v1_arbitrage.py F
address_zero = '0x0000000000000000000000000000000000000000', argobytes_atomic_trade = <ArgobytesAtomicActions Contract '0x7d182cE21DF6bde1EEf913D04c7f688B351c2F52'>
argobytes_owned_vault = <ArgobytesOwnedVault Contract '0x0F00F5Af520ebD3255141f24aDd53D34A4D9955C'>
uniswap_v1_factory = <Vyper_contract Contract '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'>
uniswap_v1_action = <UniswapV1Action Contract '0x045661743647a37b4606D754A66D1BE95c6Fc6d6'>, usdc_erc20 = <FiatTokenProxy Contract '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'>
weth9_erc20 = <WETH9 Contract '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'>
def test_uniswap_arbitrage(address_zero, argobytes_atomic_trade, argobytes_owned_vault, uniswap_v1_factory, uniswap_v1_action, usdc_erc20, weth9_erc20):
assert argobytes_owned_vault.balance() == 0
assert uniswap_v1_action.balance() == 0
value = 1e18
# send some ETH into the vault
accounts[0].transfer(argobytes_owned_vault, value)
# send some ETH into the action to simulate arbitrage profits
accounts[0].transfer(uniswap_v1_action, value)
# make sure balances match what we expect
assert argobytes_owned_vault.balance() == value
assert uniswap_v1_action.balance() == value
usdc_exchange = uniswap_v1_action.getExchange(uniswap_v1_factory, usdc_erc20)
weth9_exchange = uniswap_v1_action.getExchange(uniswap_v1_factory, weth9_erc20)
encoded_actions = argobytes_atomic_trade.encodeActions(
[
uniswap_v1_action,
uniswap_v1_action,
uniswap_v1_action,
],
[
# trade ETH to USDC
# uniswap_v1_action.tradeEtherToToken(address to, address exchange, address dest_token, uint dest_min_tokens, uint trade_gas)
uniswap_v1_action.tradeEtherToToken.encode_input(uniswap_v1_action, usdc_exchange, usdc_erc20, 1, 0),
# trade USDC to WETH9
# uniswap_v1_action.tradeTokenToToken(address to, address exchange, address src_token, address dest_token, uint dest_min_tokens, uint trade_gas)
uniswap_v1_action.tradeTokenToToken.encode_input(
uniswap_v1_action, usdc_exchange, usdc_erc20, weth9_erc20, 1, 0),
# trade WETH9 to ETH
# uniswap_v1_action.tradeTokenToEther(address to, address exchange, address src_token, uint dest_min_tokens, uint trade_gas)
uniswap_v1_action.tradeTokenToEther.encode_input(address_zero, weth9_exchange, weth9_erc20, 1, 0),
],
[True, False, False],
)
arbitrage_tx = argobytes_owned_vault.atomicArbitrage(
address_zero, argobytes_atomic_trade, address_zero, [address_zero], value, encoded_actions, {'from': accounts[1]})
assert argobytes_owned_vault.balance() > value
# TODO: https://github.com/trufflesuite/ganache-core/issues/611
# make sure the transaction succeeded
# there should be a revert above if status == 0, but something is wrong
assert arbitrage_tx.status == 1
> assert arbitrage_tx.return_value is not None
E AssertionError: assert None is not None
E + where None = <Transaction '0x813a1336c56100c78f36915bcf9f9ef38c5f5832ec5fa2513caad43893d02e0a'>.return_value
tests/test_uniswap_v1_arbitrage.py:63: AssertionError
Interactive mode enabled. Use quit() to continue running tests.
>>> arbitrage_tx.call_trace()
Call trace for '0x813a1336c56100c78f36915bcf9f9ef38c5f5832ec5fa2513caad43893d02e0a':
Initial call cost [141812 gas]
ArgobytesOwnedVault.atomicArbitrage 0:6950 [6146 / 94397 gas]
├── LiquidGasTokenUser.initialGas 468:490 [77 gas]
├── AccessControl.hasRole 561:646 [147 / 1136 gas]
│ └── EnumerableSet.contains 588:638 [66 / 989 gas]
│ └── EnumerableSet._contains 601:630 [923 gas]
├── UniversalERC20.universalBalanceOf 786:829 [782 / 845 gas]
│ └── UniversalERC20.isETH 792:811 [63 gas]
├── UniversalERC20.universalTransfer 854:1026 [126 / 7968 gas]
│ ├── UniversalERC20.isETH 867:886 [63 gas]
│ └── Address.sendValue 895:1012 [324 / 7779 gas]
│ └── ArgobytesAtomicActions [CALL] 971:984 [7455 gas]
├── ArgobytesAtomicActions.atomicTrades [CALL] 1509:6550 [189816 / 78185 gas]
│ ├── UniversalERC20.universalBalanceOf 1965:2008 [782 / 845 gas]
│ │ └── UniversalERC20.isETH 1971:1990 [63 gas]
│ └── ArgobytesAtomicActions._executeSolo 2185:6550 [6152 / -112476 gas]
│ └── Address.functionCallWithValue 4011:6550 [58 / -118628 gas]
│ └── Address._functionCallWithValue 4027:6550 [-185663 / -118686 gas]
│ ├── Address.isContract 4033:4070 [809 gas]
│ └── UniswapV1Action.tradeEtherToToken [CALL] 4332:6489 [10468 / 66168 gas]
│ └── <UnknownContract>.0xad65d76d [CALL] 4896:6341 [9052 / 55700 gas]
│ └── <UnknownContract>.0xad65d76d [DELEGATECALL] 4907:6338 [10548 / 46648 gas]
│ ├── FiatTokenProxy.balanceOf [STATICCALL] 5109:5418 [1550 / 4984 gas]
│ │ └── Proxy._fallback 5153:5418 [45 / 3434 gas]
│ │ ├── AdminUpgradeabilityProxy._willFallback 5157:5196 [79 / 932 gas]
│ │ │ ├── AdminUpgradeabilityProxy._admin 5161:5176 [844 gas]
│ │ │ └── Proxy._willFallback 5192:5194 [9 gas]
│ │ ├── UpgradeabilityProxy._implementation 5201:5216 [844 gas]
│ │ └── Proxy._delegate 5219:5418 [-576 / 1613 gas]
│ │ └── FiatTokenV1.balanceOf [DELEGATECALL] 5231:5405 [2189 gas]
│ └── FiatTokenProxy.transfer [CALL] 5745:6333 [1384 / 31116 gas]
│ └── Proxy._fallback 5789:6333 [45 / 29732 gas]
│ ├── AdminUpgradeabilityProxy._willFallback 5793:5832 [79 / 932 gas]
│ │ ├── AdminUpgradeabilityProxy._admin 5797:5812 [844 gas]
│ │ └── Proxy._willFallback 5828:5830 [9 gas]
│ ├── UpgradeabilityProxy._implementation 5837:5852 [844 gas]
│ └── Proxy._delegate 5855:6333 [-409 / 27911 gas]
│ └── FiatTokenV1.transfer [DELEGATECALL] 5867:6319 [28176 / 28320 gas]
│ ├── SafeMath.sub 6201:6222 [68 gas]
│ └── SafeMath.add 6273:6297 [76 gas]
└── LiquidGasTokenUser.freeGasTokens 6696:6708 [40 gas]
>>>
@WyseNynja I'm trying to run the tests on a separate process of ganache so I can debug it and monitor logs. I created a new live
network, but i get the below error:
tests/test_uniswap_v1_arbitrage.py E
self = <brownie.test.fixtures.PytestBrownieFixtures object at 0x7fdb46e3a070>
@pytest.fixture(scope="module")
def module_isolation(self):
"""
Resets the test environment before and after a test module runs. This ensures
a clean environment for the module, and that it's results will not affect
subsequent tests.
"""
> brownie.rpc.reset()
E SystemError: RPC is not active.
venv/lib/python3.8/site-packages/brownie/test/fixtures.py:36: SystemError
Any idea what I need to change? My config looks like:
brownie-network-config.yaml
live:
- name: Ethereum
networks:
- name: Standalone Ganache
id: myganache
host: http://127.0.0.1:8575
chainid: 1
timeout: 180
explorer: https://api.etherscan.io/api
scripts/test.sh
has brownie test --network myganache "$@"
Nevermind, it appears that if I'm already ganache on the same port brownie won't try to launch it
Alright, I have a pretty stable way to reproduce this issue and debug what's going on. Some preliminary findings:
If I run ganache in a separate process and run the test, I receive the error as stated in this issue. If i quit brownie, brownie will revert a bunch of snapshots to "revert" the ganache change back to the start. If I run the test again without restarting ganache, I get a different error:
> arbitrage_tx = argobytes_owned_vault.atomicArbitrage(
address_zero, argobytes_atomic_trade, address_zero, [address_zero], value, encoded_actions, {'from': accounts[1]})
E brownie.exceptions.VirtualMachineError: revert: UniswapV1Action.tradeEtherToToken -> IUniswapExchange.ethToTokenTransferInput: reverted without a reason
E Trace step -1, program counter 1663:
E File "contracts/ArgobytesOwnedVault.sol", line 175, in ArgobytesOwnedVault.atomicArbitrage:
E // TODO: confirm that this actually saves us gas!
E freeGasTokens(gas_token, initial_gas);
E
E revert(reason);
E } catch (
E bytes memory /*lowLevelData*/
E ) {
tests/test_uniswap_v1_arbitrage.py:54: VirtualMachineError
This is interesting because it's the same error that the call trace points to when I ran the test in ganache the first time. Digging into how debug_traceTransaction
works in ganache, it looks like we recreate a block from the parent block's state trie. This is sane in theory, but forking has a way of not managing state properly in a historical sense.
I tried my changes in #613 and it's still not working. I'm going to continue digging into this specific issue a tad further, but my hunch is that solving other forking issues will solve this issue
@WyseNynja are we able to run these tests on ganache without forking? Like deploying any contracts we need to and running tests against those on a completely fresh ganache state?
That sounds like some good progress!
The tests could be rewritten to not depend on forking mainnet, but they would have to deploy some ERC20s and Uniswap/Kyber and bootstrap them with liquidity. That's how the early versions of my code did it, but it was fragile and done by a bunch of bash scripts that glued together their deploy scripts.
This test doesn't use a real exchange, but it does use all of my contracts: https://github.com/SatoshiAndKin/argobytes-contracts-brownie/blob/03befdc4f87f2f4ceefc35885a98ed869d807bf4/tests/test_argobytes_owned_vault.py#L30
I assume my earlier #605 is the same issue as this?
You are correct @haltman-at, I'll mark that one as a duplicate since this one is older (it was originally on the ganache-cli repo so that's probably why you didn't find it when you made #605)
However, I will use your #605 example to try to figure out the common root cause here. Thanks for letting me know!
@mds1 commented in #571 with reproduction steps that point to very similar reproduction steps that this issue has. I believe #571 was originally calling out the same issue, but it got shifted to solving a different problem with a similar symptom
I've put @mds1's comment below so it's easy for me to see all of the relevant recent information quickly
My current plan of attack here is to study both of these reproduction steps to understand the root issue and solve that. I don't believe this has to do with reentrance, but storage lookups are not adjusting as they should and are causing something to go awry and hit a piece of code that says you reentered
I'm using ganache-core 2.11.3-beta.0, and it seems I'm still seeing this bug in certain cases.
Steps to reproduce:
yarn
.env
with INFURA_ID=yourInfuraId
yarn test
In step 4, only two tests in batch-zkSync-deposit-test.js
will run. The first will pass, and the second will fail with Error: Returned error: VM Exception while processing transaction: revert ReentrancyGuard: reentrant call
. We can verify there is no reentry as follows:
it.skip
to skip that test. Run tests again, and our previously failing test now passesThe reentrant call error is from the zkSync contract, found here.
The full console output from the error is below:
Error: Returned error: VM Exception while processing transaction: revert ReentrancyGuard: reentrant call -- Reason given: ReentrancyGuard: reentrant call.
at PromiEvent (node_modules/@truffle/contract/lib/promievent.js:9:30)
at TruffleContract.deposit (node_modules/@truffle/contract/lib/execute.js:169:26)
at Context.<anonymous> (test/batch-zkSync-deposit-test.js:124:46)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
It seems this has something to do with the use of evm_revert
. In the beforeEach
and afterEach
hooks we save and restore the ganache state using evm_snapshot
and evm_revert
. If you comment out the evm_rever
functionality (89 and 102) the above error does not happen.
I also just noticed I'm missing some await
s in the beforeEach
hook, but adding those in does not resolve this issue
As you alluded to @mds1, I believe that snapshots/reverts are not handled properly in forking. I believe this is at a minimum affecting all storage, but it could also be an issue with how we handle storage deletes in between snapshot/revert. I'm got a ganache-core reproducing the error, so we're nearing a fix!
Turns out my test was incorrectly written and actually does work, so I still don't have a unit test that reproduces this. I did some refactoring that I was hoping was going to fix issues, but alas it does not. Investigating further to figure out exact root cause
Giving a little update here. I still don't have a unit test that reproduces the issues from @mds1's repo, but I was able to get a quick test setup that allowed me to debug it. I'm happy to say that I have code that resolves the issue within @mds1's repo! Early next week, I'll test the changes against @WyseNynja's repo to see if I killed 2 birds with 1 stone. Regardless, we'll look at making a beta release sometime next week
Hope yall have a good Labor Day!
Looks like I did not solve @WyseNynja's issue which is specific to debug_traceTransaction. I will be moving @mds1's issue (again haha) to #624 since his were specifically due to snapshots (which probably partially affected this issue)
We'll go forward with a beta release for that as I figure out debug_traceTransaction
Thanks for all your hard work!
Quick update here: I've figured out the root cause of @WyseNynja's bug (at least the next hurdle haha). I can reproduce it in a test, and hoping to get over this one soon!
Seems like I'm able to get that one test to pass @WyseNynja! Hoping we can get a beta release out shortly after PR reviews
This was released in https://github.com/trufflesuite/ganache-core/releases/tag/v2.12.2-beta.0
@WyseNynja have you been able to test this yet? Are your forking issues resolved? Looks like it also made into a stable release a couple weeks ago: https://github.com/trufflesuite/ganache-core/releases/tag/v2.13.0 | https://github.com/trufflesuite/ganache-cli/releases/tag/v6.12.0
Yes! My tests so far are much better. I haven't had time to finish them all (been working on proxy contracts instead), but the ones that were failing are passing now. Thank you!
On Oct 26, 2020, at 10:40 AM, Mike Seese notifications@github.com wrote:
@WyseNynja have you been able to test this yet? Are your forking issues resolved? Looks like it also made into a stable release a couple weeks ago: https://github.com/trufflesuite/ganache-core/releases/tag/v2.13.0 | https://github.com/trufflesuite/ganache-cli/releases/tag/v6.12.0
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.
This is awesome; thanks!
I have a project that uses brownie and ganache-cli --fork against a geth node. (Geth is running according to these steps).
A few of my tests check the return_value of my transactions. Most succeed, but one has no return value despite not reverting.
Expected Behavior
A transaction with a status of 1 should have a return value.
Current Behavior
Steps to Reproduce
./scripts/test.sh tests/test_uniswap_v1_arbitrage.py -I
Context
Due to previous issues with reverting traces pointing to incorrect lines, I've disabled solc's runs, so that shouldn't be the problem.
I suspected that gastoken2/chi might be related to the issue, but removing that code didn't change anything.
I'm sure the transaction isn't actually reverting because my test balance increases after the transaction is completed. If the transaction had reverted, the balance would not increase.
I don't think Brownie is the problem here. Brownie just gets the trace from ganache.
Your Environment