Hi guys! So as discussed at the end of my previous article, I want to add support for the deployment/usage of ganache-cli to be able to test my contracts effectively. This is what we will focus on in this new post.
"nodejs_envs": { "ganache_env": { "nodejs_version": "16.15.1", "packages": ["ganache-cli"] } },
"""NodeJs manager component""" import logging from nvp.nvp_component import NVPComponent from nvp.nvp_context import NVPContext logger = logging.getLogger(__name__) def create_component(ctx: NVPContext): """Create an instance of the component""" return NodeJsManager(ctx) class NodeJsManager(NVPComponent): """NodeJsManager component""" def __init__(self, ctx: NVPContext): """Script runner constructor""" NVPComponent.__init__(self, ctx) def get_env_desc(self, env_name): """Retrieve the desc for a given environment""" # If there is a current project we first search in that one: proj = self.ctx.get_current_project() desc = None if proj is not None: desc = proj.get_nodejs_env(env_name) if desc is None: # Then search in all projects: projs = self.ctx.get_projects() for proj in projs: desc = proj.get_nodejs_env(env_name) if desc is not None: break if desc is None: all_envs = self.config.get("nodejs_envs", {}) desc = all_envs.get(env_name, None) assert desc is not None, f"Cannot find nodejs environment with name {env_name}" return desc def get_env_dir(self, env_name, desc=None): """Retrieve the installation dir for a given nodejs env.""" if desc is None: desc = self.get_env_desc(env_name) default_env_dir = self.get_path(self.ctx.get_root_dir(), ".nodeenvs") return desc.get("install_dir", default_env_dir) def setup_nodejs_env(self, env_name, env_dir=None, renew_env=False, do_update=False): """Setup a given nodejs environment""" desc = self.get_env_desc(env_name) if env_dir is None: # try to use the install dir from the desc if any or use the default install dir: env_dir = self.get_env_dir(env_name, desc) # Ensure the parent folder exists: self.make_folder(env_dir) # create the env folder if it doesn't exist yet: dest_folder = self.get_path(env_dir, env_name) tools = self.get_component("tools") new_env = False if self.dir_exists(dest_folder) and renew_env: logger.info("Removing previous python environment at %s", dest_folder) self.remove_folder(dest_folder) if not self.dir_exists(dest_folder): # Should extract the nodejs package first: vers = desc['nodejs_version'] ext = ".7z" if self.is_windows else ".tar.xz" suffix = "win-x64" if self.is_windows else "linux-x64" base_name = f"node-v{vers}-{suffix}" filename = f"{base_name}{ext}" pkg_dir = self.get_path(self.ctx.get_root_dir(), "tools", self.platform) pkg_file = self.get_path(pkg_dir, filename) if not self.file_exists(pkg_file): logger.info("Downloading nodejs version %s for %s...", vers, self.platform) url = f"https://nodejs.org/dist/v{vers}/{filename}" tools.download_file(url, pkg_file) logger.info("Installing nodejs version %s...", vers) tools.extract_package(pkg_file, env_dir, target_dir=dest_folder, extracted_dir=base_name) new_env = True # py_path = self.get_path(dest_folder, pdesc['sub_path']) # if new_env or do_update: # # trigger the update of pip: # logger.info("Updating pip...") # self.execute([py_path, "-m", "pip", "install", "--upgrade", "pip"]) # # Next we should prepare the requirements file: # req_file = self.get_path(dest_folder, "requirements.txt") # content = "\n".join(desc["packages"]) # self.write_text_file(content, req_file) # logger.info("Installing python requirements...") # self.execute([py_path, "-m", "pip", "install", "-r", req_file]) def process_cmd_path(self, cmd): """Process a given command path""" if cmd == "setup": env_name = self.get_param("env_name") logger.info("Should setup environment %s here.", env_name) env_dir = self.get_param("env_dir") renew_env = self.get_param("renew_env", False) do_update = self.get_param("do_update", False) self.setup_nodejs_env(env_name, env_dir, renew_env, do_update) return True if cmd == "remove": env_name = self.get_param("env_name") logger.info("Should remove environment %s here.", env_name) return True return False if __name__ == "__main__": # Create the context: context = NVPContext() # Add our component: comp = context.register_component("nodejs", NodeJsManager(context)) context.define_subparsers("main", ["setup", "remove"]) psr = context.get_parser('main.setup') psr.add_argument("env_name", type=str, help="Name of the environment to setup") psr = context.get_parser('main.remove') psr.add_argument("env_name", type=str, help="Name of the environment to remove") comp.run()
$ nvp nodejs setup ganache_env 2022/06/12 07:01:12 [nvp.nvp_project] ERROR: Cannot load project NervHome: exception: No module named 'PIL' 2022/06/12 07:01:12 [__main__] INFO: Should setup environment ganache_env here. 2022/06/12 07:01:12 [__main__] INFO: Downloading nodejs version 16.15.1 for windows... 2022/06/12 07:01:12 [nvp.components.tools] INFO: Downloading file from https://nodejs.org/dist/v16.15.1/node-v16.15.1-win-x64.7z... [==================================================] 17121974/17121974 100.000% 2022/06/12 07:01:34 [__main__] INFO: Installing nodejs version 16.15.1... 2022/06/12 07:01:34 [nvp.components.tools] INFO: Extracting D:\Projects\NervProj\tools\windows\node-v16.15.1-win-x64.7z...
""" NVP plug entrypoint module for NervHome """ import logging from components.navision import Navision from components.backup_manager import BackupManager from components.gif_resizer import GifResizer from components.file_renamer import FileRenamer from components.movie_handler import MovieHandler from components.file_dedup import FileDedup from components.picture_handler import PictureHandler from components.password_generator import PasswordGenerator from components.text_translator import TextTranslator logger = logging.getLogger('NervHome') def register_nvp_plugin(context, proj): """This function should register this plugin in the current NVP context""" logger.debug("Registering NervHome NVP plugin.") # Note that we register this as a context component so that it is not only # available on the nervhome project: context.register_component('navision', Navision(context, proj)) context.register_component('backup', BackupManager(context, proj)) context.register_component('gif_resizer', GifResizer(context, proj)) context.register_component('file_renamer', FileRenamer(context, proj)) context.register_component('movie_handler', MovieHandler(context, proj)) context.register_component('file_dedup', FileDedup(context, proj)) context.register_component('picture_handler', PictureHandler(context, proj)) context.register_component('password_gen', PasswordGenerator(context, proj)) context.register_component('text_translator', TextTranslator(context, proj))
# Note: the nvp_plug system bellow is obsolete and should be removed eventually: if ctx.is_master_context() and self.file_exists(proj_path, "nvp_plug.py"): # logger.info("Loading NVP plugin from %s...", proj_name) try: sys.path.insert(0, proj_path) plug_module = import_module("nvp_plug") plug_module.register_nvp_plugin(ctx, self) sys.path.pop(0) # Remove the module name from the list of loaded modules: del sys.modules["nvp_plug"] except ModuleNotFoundError as err: logger.error("Cannot load project %s: exception: %s", self.get_name(False), str(err))
$ nvp nodejs setup ganache_env 2022/06/12 11:21:53 [__main__] INFO: Installing nodejs version 16.15.1... 2022/06/12 11:21:53 [nvp.components.tools] INFO: Extracting D:\Projects\NervProj\tools\windows\node-v16.15.1-win-x64.7z... npm WARN config global `--global`, `--local` are deprecated. Use `--location=global` instead. changed 20 packages, and audited 203 packages in 7s 11 packages are looking for funding run `npm fund` for details found 0 vulnerabilities 2022/06/12 11:22:10 [__main__] INFO: Installing packages: ['ganache'] changed 1 package, and audited 203 packages in 2s 11 packages are looking for funding run `npm fund` for details found 0 vulnerabilities
"ganache": { "nodejs_env": "ganache_env", "cmd": "${NODE} ${NODE_ENV_DIR}/node_modules/ganache/dist/node/cli.js" }
if "nodejs_env" in desc: nodejs = self.get_component("nodejs") env_name = desc['nodejs_env'] env_dir = nodejs.get_env_dir(env_name) node_root_dir = self.get_path(env_dir, env_name) hlocs["${NODE_ENV_DIR}"] = node_root_dir node_path = nodejs.get_node_path(env_name) hlocs["${NODE}"] = node_path hlocs["${NPM}"] = f"{node_path} {node_root_dir}/node_modules/npm/bin/npm-cli.js"
$ nvp ganache ganache v7.3.0 (@ganache/cli: 0.4.0, @ganache/core: 0.4.0) Starting RPC server Available Accounts ================== (0) 0x4e888652c47b053fb5F72FA96E4036aB15c7cC93 (1000 ETH) (1) 0x3E692CfBb5C1cf7B3997242C9eBE8C7Bd54880Be (1000 ETH) (2) 0x89BfF5009A50cB154AAF4589BfbD8Ac6618f7E13 (1000 ETH) (3) 0xb2913BbA1B4Bf57a68e7ddCD84022Cb67CBdee38 (1000 ETH) (4) 0x7Db390db41fAAaa5483D0191b67d2e4EbB246e51 (1000 ETH) (5) 0x070c06261E135dB3286419980f69CE9ED5b2Ee5E (1000 ETH) (6) 0xab22A767342B93843D07B326A81C2461a5E33F9a (1000 ETH) (7) 0xa5d98A88cBbFe0C947F00Cc0c8Ac14cFD2fAc5aA (1000 ETH) (8) 0xc9F4b0e96229F11b9e2d60184Eb1261E411C91b5 (1000 ETH) (9) 0x799B6F260Fc268843c99F582c43112b607B51865 (1000 ETH) Private Keys ================== (0) 0xfc9b50e51883c70185e9e3ab7f64c3d4f324fec136d480993ec03aeb041e9962 (1) 0x73e0a314c731c6b2ef0fac41bcc6957f715380b0d3a16bc48256c8e72ba341a7 (2) 0xe71659570a882dbb11952bda508e760a6c9a5720215c8acd8d514b3dd00a2b79 (3) 0xebe487198293d177879f6f67e47248e7abe0ca2cbcba5f7ddd9ce261ae7c73f3 (4) 0x5239f211a7aca12a63046ae9501f1dd2709226a1cc6bd5ec6258808383ac7065 (5) 0x1eea2b212bdc8541f72e886524dd771df29309548e11c9435d37abb719227f2f (6) 0xfbcfffa982129d70b110a4d48234e1f95e733ea98b079244c0089b330bbd7fb6 (7) 0xa3141529424d2a9503f2b2da04b399d42793f459114e307a2e84b6588e971025 (8) 0x5bd642a37f0e1de30afc67a619f0541e0fcfa7541fca93655d094283a04e451f (9) 0x5bcf805bcd57418327a22a35294750c99093ae837d627989fca21168b2ba3fe1 HD Wallet ================== Mnemonic: crew deposit army oxygen elevator common planet scene tribe sentence decrease nature Base HD Path: m/44'/60'/0'/0/{account_index} Default Gas Price ================== 2000000000 BlockGas Limit ================== 30000000 Call Gas Limit ================== 50000000 Chain Id ================== 1337 RPC Listening on 127.0.0.1:8545
ganache -f https://bsc-dataseed1.binance.org
nvp bchain check-pair -p "https://127.0.0.1:8545" WBNB/BUSD
$ nvp bchain check-pair -p "http://127.0.0.1:8545" WBNB/BUSD 2022/06/12 16:18:44 [__main__] INFO: Using provider url http://127.0.0.1:8545 for chain bsc 2022/06/12 16:18:47 [nvh.crypto.blockchain.arbitrage_manager] INFO: PairChecker result for BUSD (against WBNB) is: PASSED
$ nvp bchain check-pair -p "http://127.0.0.1:8545" WBNB/GENIX 2022/06/12 16:19:13 [__main__] INFO: Using provider url http://127.0.0.1:8545 for chain bsc 2022/06/12 16:19:15 [nvh.crypto.blockchain.arbitrage_manager] INFO: PairChecker result for GENIX (against WBNB) is: PASSED
$ nvp bchain check-pair -p "http://127.0.0.1:8545" WBNB/GINUX 2022/06/12 16:24:51 [__main__] INFO: Using provider url http://127.0.0.1:8545 for chain bsc 2022/06/12 16:24:57 [nvh.crypto.blockchain.arbitrage_manager] ERROR: Pair checker test failed for GINUX: execution reverted: VM Exception while processing transaction: revert transferToken failed. 2022/06/12 16:24:57 [nvh.crypto.blockchain.arbitrage_manager] INFO: PairChecker result for GINUX (against WBNB) is: FAILED 2022/06/12 16:24:57 [nvh.crypto.blockchain.arbitrage_manager] INFO: Pair address: 0x85B446d3EDC3A7fe4db8A88649c14fdcB4e911dE, exchange: PancakeSwap2, router: 0x10ED43C718714eb63d5aA57B78B54704E256024E, ifp: 9975
def get_mnemonic_accounts(self, mnemonics, account_index=0, count=1): """Generate a account from a mnemonic""" w3 = Web3() w3.eth.account.enable_unaudited_hdwallet_features() res = [] for i in range(count): account = w3.eth.account.from_mnemonic(mnemonics, account_path=f"m/44'/60'/0'/0/{account_index+i}") res.append({"address": account.address, "private_key": account.key.hex()}) return res
# use the first account to send some ethers: chain.set_account_address(accounts[0]['address']) chain.set_private_key(accounts[0]['private_key']) # Display the native balance: bal = chain.get_native_balance() print(f"Initial account0 balance: {bal} {chain.get_native_symbol()}") chain.transfer(999.0, "evm_arb") bal = chain.get_native_balance() print(f"Final account0 balance: {bal} {chain.get_native_symbol()}") chain.set_account("evm_arb") bal = chain.get_native_balance() print(f"evm_arb balance: {bal} {chain.get_native_symbol()}")
addr = chain.deploy_contract("PairCheckerV5") arbman.pair_checker_sc = chain.get_contract(addr, abi_file="ABI/PairCheckerV5.json")
# And we also have to send some WBNB to the pair address: # Send some BNB on the WBNB token: bal = t0.get_balance() print(f"Stage1 WBNB balance: {bal}") chain.transfer(0.2, t0.address()) bal = t0.get_balance() print(f"Stage2 WBNB balance: {bal}") # Now we transfer the WBNB token to the pair address: bal = t0.get_balance(account=addr) print(f"Stage3 pair initial WBNB balance: {bal}") t0.transfer(addr, t0.to_amount(0.2)) bal = t0.get_balance(account=addr) print(f"Stage4 pair final WBNB balance: {bal}")
$ arbman.check_pair(pair, t0) 2022/06/12 20:58:37 [nvh.crypto.blockchain.arbitrage_manager] ERROR: Pair checker test failed for GINUX: execution reverted: VM Exception while processing transaction: revert Pancake: INSUFFICIENT_LIQUIDITY 2022/06/12 20:58:37 [nvh.crypto.blockchain.arbitrage_manager] INFO: PairChecker result for GINUX (against WBNB) is: FAILED 2022/06/12 20:58:37 [nvh.crypto.blockchain.arbitrage_manager] INFO: Pair address: 0x85B446d3EDC3A7fe4db8A88649c14fdcB4e911dE, exchange: PancakeSwap2, router: 0x10ED43C718714eb63d5aA57B78B54704E256024E, ifp: 9975
function swap( uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data ) external lock { require( amount0Out > 0 || amount1Out > 0, "Pancake: INSUFFICIENT_OUTPUT_AMOUNT" ); (uint112 _reserve0, uint112 _reserve1, ) = getReserves(); // gas savings require( amount0Out < _reserve0 && amount1Out < _reserve1, "Pancake: INSUFFICIENT_LIQUIDITY" );
$ pobj = chain.get_pair(addr="0x85B446d3EDC3A7fe4db8A88649c14fdcB4e911dE") pobj.get_reserves() (107867324822485882037, 79318074672041400795394145201)
revert( string( abi.encodePacked( "r0=", Strings.toString(r0), ", r1=", Strings.toString(r1) ) ) );
VM Exception while processing transaction: revert r0=125324017036209626383, r1=81185631560566122939989913391
2022/06/13 07:43:26 [nvh.crypto.blockchain.arbitrage_manager] ERROR: Pair checker test failed for GINUX: execution reverted: VM Exception while processing transaction: revert [error] fn=swapTokens0NFL 2022/06/13 07:43:26 [nvh.crypto.blockchain.arbitrage_manager] INFO: PairChecker result for GINUX (against WBNB) is: FAILED 2022/06/13 07:43:26 [nvh.crypto.blockchain.arbitrage_manager] INFO: Pair address: 0x85B446d3EDC3A7fe4db8A88649c14fdcB4e911dE, exchange: PancakeSwap2, router: 0x10ED43C718714eb63d5aA57B78B54704E256024E, ifp: 9975
// If stype==0 it means we have FL support: if (stype == 0) { swapTokens0FL( pair, swapAmount0, swapAmount1, address(this), new bytes(0) ); } else { swapTokens0NFL( pair, swapAmount0, swapAmount1, address(this), new bytes(0) ); }/
stype = 1 if dex0.is_flash_loan_supported() else 0
if eq(stype, 0) { mstore(ptr, shl(224, 0x6d9a640a)) // for sig "swap(uint256,uint256,address)" } if eq(stype, 1) { mstore(ptr, shl(224, 0x022c0d9f)) // for sig "swap(uint256,uint256,address,bytes)" }
r0b = r0+amount
and r1b = r1 - amountOut
but it seems that some token may behave differently for the reserves in dstToken, for instance for GINUX we find: r1b - (r1-amountOut) == 11214770857
when we use an amountOut value of 63749319922005. ⇒ Interesting 🤔 But how could that happen in the code ? I need to find a clear explanation…if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
function swapAndLiquify(uint256 contractTokenBalance) private lockTheSwap { // split the contract balance into halves uint256 half = contractTokenBalance.div(2); uint256 otherHalf = contractTokenBalance.sub(half); // capture the contract's current ETH balance. // this is so that we can capture exactly the amount of ETH that the // swap creates, and not make the liquidity event include any ETH that // has been manually sent to the contract uint256 initialBalance = address(this).balance; // swap tokens for ETH swapTokensForEth(half); // <- this breaks the ETH -> HATE swap when swap+liquify is triggered // how much ETH did we just swap into? uint256 newBalance = address(this).balance.sub(initialBalance); // add liquidity to uniswap addLiquidity(otherHalf, newBalance); emit SwapAndLiquify(half, newBalance, otherHalf); }
expected=63955597073879 got=61397373190925 (expected-got)/expected 0.03999999999998186
2022/06/13 12:37:23 [nvh.crypto.blockchain.arbitrage_manager] ERROR: Pair checker test failed for GINUX: execution reverted: VM Exception while processing transaction: revert [ok] r0=125574043306384440312, r1=81028894613374954895881896594, r0dt=0, r1dt=-2563240925341, swap_exp=64365469366655, swap_rcv=61790850591989, transfer_exp=61790850591989, transfer_rcv=59330139303662, amount_back=91716
nvp bchain deploy -c bsc -a evm_arb PairCheckerV5
for addr in self.quote_tokens: val = dex.get_quote(1.0, addr, self.native_symbol) self.quote_token_values[addr] = val token = self.chain.get_token(addr) logger.info("Quote token %s value: %.6g %s", token.symbol(), val, self.native_symbol) # Get the current balance of qtoken in the pair checker: bal = token.get_balance(as_value=False, account=self.pair_checker_address) if bal == 0: if token.get_balance(as_value=False) > 400000: logger.info("Sending %s funds to pair checker contract...", token.symbol()) token.transfer(self.pair_checker_address, 200000) else: logger.warning("Not enough %s funds to send to pair checker contract.", token.symbol()) bal = token.get_balance(as_value=False, account=self.pair_checker_address) logger.info("PairChecker balance: %d %s", bal, token.symbol())
SQL_CREATE_ARB_SETUPS_TABLE = """ CREATE TABLE IF NOT EXISTS arb_setups ( id SERIAL PRIMARY KEY, timestamp integer NOT NULL, block_number integer NOT NULL, p0addr char(42) NOT NULL, p1addr char(42) NOT NULL, duration SMALLINT NOT NULL, profit_value float NOT NULL, state SMALLINT NOT NULL ); """
def check_current_setups(self): """Check the currently existing arb setups""" if len(self.current_setups) == 0: return paddrs = [] for arb in self.current_setups: # get all the reserves of interest: paddrs.append(arb["p0addr"]) paddrs.append(arb["p1addr"]) pair_reserves, _ = self.get_all_reserves(paddrs) for arb in self.current_setups: qtoken = arb["qtoken"] am0, pval = self.compute_arb_profit(arb["p0"], arb["p1"], pair_reserves, qtoken) if am0 is not None: best_profit = qtoken.to_value(pval) # Convert the qtoken profit value into native (wrapped) token value: best_profit *= self.quote_token_values[qtoken.address()] if am0 is None or best_profit < self.min_profit: diff = self.last_block_number - arb["block_number"] sym0 = arb["t0"].symbol() sym1 = arb["t1"].symbol() logger.info("Arb setup on %s/%s is gone after %d blocks.", sym0, sym1, diff) arb["duration"] = diff arb["done"] = True # Add the setup in the db: self.chain.get_db().insert_arb_setups([arb]) self.current_setups = [arb for arb in self.current_setups if "done" not in arb]
rows = db.get_all_arb_setups() fig = plt.figure(figsize=(8, 4)) profits = [row[6] for row in rows] dur = [row[5] for row in rows] num = len(profits) print(f"Collected {num} arb setups.") ax1 = fig.add_subplot(121) ax1.plot(profits) ax1.title.set_text("Profits") ax2 = fig.add_subplot(122) ax2.plot(dur) ax2.title.set_text("Duration") plt.show()
2022/06/14 21:19:13 [__main__] INFO: hash1=0x8118f7b7a2ca099a05835cc54bbfd319daa1f46f9ae9ad6e5fda4ee00976a4c7 2022/06/14 21:19:13 [__main__] INFO: hash2=0x8118f7b7a2ca099a05835cc54bbfd319daa1f46f9ae9ad6e5fda4ee00976a4c7 2022/06/14 21:19:17 [__main__] INFO: hash1=0xdadd21f238aefe9564efeb1d29871a0bf02fed8135389833f7ba75d8d1fcc2ce 2022/06/14 21:19:17 [__main__] INFO: hash2=0xdadd21f238aefe9564efeb1d29871a0bf02fed8135389833f7ba75d8d1fcc2ce
2022/06/15 20:14:13 [__main__] INFO: Block age: 0.373568 2022/06/15 20:14:17 [__main__] INFO: Block age: -1.171809 2022/06/15 20:14:22 [__main__] INFO: Block age: 0.276598 2022/06/15 20:14:24 [__main__] INFO: Block age: -0.045503 2022/06/15 20:14:28 [__main__] INFO: Block age: 0.978560 2022/06/15 20:14:31 [__main__] INFO: Block age: 0.509058 2022/06/15 20:14:33 [__main__] INFO: Block age: -0.245053 2022/06/15 20:14:37 [__main__] INFO: Block age: 0.741160 2022/06/15 20:14:41 [__main__] INFO: Block age: 1.072904 2022/06/15 20:14:43 [__main__] INFO: Block age: 0.907354 2022/06/15 20:14:49 [__main__] INFO: Block age: 0.754012 2022/06/15 20:14:52 [__main__] INFO: Block age: 0.710214 2022/06/15 20:14:55 [__main__] INFO: Block age: 0.560893