DeFi: Automating some claims

Hello world! So, the cryptoworld is basically on fire right now (and in fact this has been the case for quite a few very lonnnnnnnggg months already since we entered bear market territory…) but anyway, I still have some funds placed on DeFi protocols and generating a few bucks now and then, and the idea in this dev session is to automate the retrieval process for those rewards, converting them in stable coins in the process. This should not be too hard, right ? So I have hope to be able to finish this new article just tonight! [But I might be dreaming a little lol We'll see…]

  • Let's start with my Belt rewards from https://belt.fi/ on the BSC.
  • So performing a first claim manually we see that we call the withdraw function on the contract 0xD4BbC80b9B102b77B21A06cb77E954049605E6c1
  • The function prototype is: withdraw(uint256 _pid, uint256 _wantAmt)
  • And here we call it with pid==8 and wantAmt==0
  • In the process, we collect some Belt token (address = 0xE0e514c71282b6f4e823703a39374Cf58dc3eA4f) which we will need to exchange for BUSD.
  • ⇒ Let's start building the script to handle this now.
  • At some point, I will need to get the current price of belt in USD (or BUSD), so we check if we have a path for BELT/BUSD:
    belt_addr = "0xE0e514c71282b6f4e823703a39374Cf58dc3eA4f"
    dex.get_quote(1.0,belt_addr, "BUSD")
  • And here is my first version for this automatic Belt Finance handling:
    """BelfFinance protocol."""
    
    import logging
    import time
    
    from nvp.nvp_component import NVPComponent
    from nvp.nvp_context import NVPContext
    
    from nvh.crypto.blockchain.evm_blockchain import EVMBlockchain
    
    logger = logging.getLogger(__name__)
    
    
    class BelfFinance(NVPComponent):
        """BelfFinance component class"""
    
        def __init__(self, ctx, chain=None):
            """Component constructor"""
            NVPComponent.__init__(self, ctx)
    
            if chain is None:
                chain = self.ctx.get_component("bsc_chain")
    
            self.chain: EVMBlockchain = chain
    
            self.config = self.ctx.get_config()["belt_finance"]
    
            logger.debug("Loading Belt finance chef contract...")
            self.master_sc = self.chain.get_contract(
                self.config["bsc_master_address"], abi_file="ABI/bsc_belt_finance.json"
            )
    
            self.belt_token = self.chain.get_token(self.config["belt_token_address"])
            self.busd_token = self.chain.get_token("BUSD")
    
        def get_pending_rewards(self, as_value=False):
            """report the pending rewards"""
    
            # We call the function "pendingBELT" to get the pending belt reward
            addr = self.chain.get_account_address()
    
            # The only pool we are interested in for the moment is the beltETH pool with pid==8
            pid = 8
            pending = self.master_sc.call_function("pendingBELT", pid, addr)
            if as_value:
                return self.belt_token.to_value(pending)
            return pending
    
        def get_belt_price(self):
            """Get the price of BELT in USD"""
            dex = self.chain.get_default_exchange()
            return dex.get_quote(1.0, self.belt_token.address(), "BUSD")
    
        def collect_pending_rewards(self, min_usd_value=5.0, to_stablecoin=False):
            """Collect the pending rewards"""
    
            amount = self.get_pending_rewards(as_value=False)
            val = self.belt_token.to_value(amount)
            price = self.get_belt_price()
    
            usd_val = val * price
            if val < min_usd_value:
                logger.info("Not claiming %f BELT: value too low: %.4f BUSD", val, usd_val)
                return
    
            # Otherwise we can perform the claim:
            pid = 8
            logger.info("Claiming BELT rewards on pool %d...", pid)
            opr = self.master_sc.build_function_call("withdraw", pid, 0)
    
            b0 = self.belt_token.get_balance(as_value=False)
            if self.chain.perform_operation(opr, self.config["harvest_max_gas"]) is not None:
                b1 = self.belt_token.get_balance(as_value=False)
                while b1 == b0:
                    logger.info("Waiting for BELT balance update...")
                    time.sleep(1.0)
                    b1 = self.belt_token.get_balance(as_value=False)
                got = b1 - b0
                val = self.belt_token.to_value(got)
                logger.info("=> Claimed %f BELT from belt finance for pool %d", val, pid)
    
                if not to_stablecoin:
                    # Send a collect message right now:
                    rchat = self.get_component("rchat")
                    msg = ":white_check_mark: 💰 *[Belt.finance]*: "
                    msg += f"Claimed {val} BELT from belt finance from pool {pid}"
                    rchat.send_message(msg)
    
            b1 = self.belt_token.get_balance(as_value=False)
            if b1 == b0:
                logger.info("Did not collect any BELT token: not swapping for BUSD.")
                return
    
            # got = b1 - b0
            got = b1
            # We swapp **ALL** the available BELTs below not just what we received
            val = self.belt_token.to_value(got)
            logger.info("Swapping %f BELT for BUSD...", val)
            usd_b0 = self.busd_token.get_balance(as_value=False)
            dex = self.chain.get_default_exchange()
            addr0 = self.belt_token.address()
            addr1 = self.busd_token.address()
            dex.swap_tokens(got, [addr0, addr1])
            usd_b1 = self.busd_token.get_balance(as_value=False)
            usd_val = self.busd_token.to_value(usd_b1 - usd_b0)
    
            logger.info("=> Got %f BUSD from %f BELT", usd_val, val)
    
            rchat = self.get_component("rchat")
            msg = ":white_check_mark: 💰 *[Belt.finance]*: "
            msg += f"Swapped {val:.6g} BELT to {usd_val:.6g} BUSD"
            rchat.send_message(msg)
    
        def process_cmd_path(self, cmd):
            """Check if this component can process the given command"""
    
            if cmd == "price":
                price = self.get_belt_price()
                logger.info("Current BELT price: %.6g BUSD", price)
                return True
    
            if cmd == "pending":
                val = self.get_pending_rewards(as_value=True)
                price = self.get_belt_price()
                logger.info("Pending rewards: %.6f BELT (value: %.6f BUSD)", val, val * price)
                return True
    
            if cmd == "collect":
                min_val = self.get_param("min_value")
                to_stable = self.get_param("to_stable")
                self.collect_pending_rewards(min_val, to_stable)
                return True
    
            return False
    
    
    if __name__ == "__main__":
        # Create the context:
        context = NVPContext()
    
        # Add our component:
        comp = context.register_component("belt_finance", BelfFinance(context))
    
        context.define_subparsers("main", ["price", "pending"])
    
        psr = context.build_parser("collect")
        psr.add_float("--min", dest="min_value", default=5.0)("Minimal claim value in BUSD")
        psr.add_flag("-s", "--stable", dest="to_stable")("Swap BELT for stablecoin")
    
        comp.run()
    
  • With that I can run the following command for instance to collect the pending BELTs and automatically swap them for BUSD in the process:
    $ nvp beltfinance collect --min 0.1 -s
  • Now I just need to run that once per day and I should be good for a while 👍!: OK
  • I'm still getting errors very often from my automatic block retrieval system, mainly on the BSC chain, and it's annoying to get those error messages so often, so I need to have a look at it.
  • Note: While I'm at it, I also reduced the quantiy of outputs from the collect_evm_blocks script.
  • In the BSC block collect log the error we still get is as follow:
    File "/mnt/data1/dev/projects/NervProj/nvp/nvp_component.py", line 93, in run
    res = self.process_command(cmd)
    File "/mnt/data1/dev/projects/NervProj/nvp/nvp_component.py", line 83, in process_command
    return self.process_cmd_path(self.ctx.get_command_path())
    File "/mnt/data1/dev/projects/NervHome/nvh/crypto/blockchain/blockchain_manager.py", line 55, in process_cmd_path
    chain.handle("collect_evm_blocks")
    File "/mnt/data1/dev/projects/NervProj/nvp/nvp_component.py", line 105, in handle
    return self.call_handler(f"{self.handlers_path}.{hname}", self, *args, **kwargs)
    File "/mnt/data1/dev/projects/NervProj/nvp/nvp_component.py", line 100, in call_handler
    return self.ctx.call_handler(hname, *args, **kwargs)
    File "/mnt/data1/dev/projects/NervProj/nvp/nvp_context.py", line 676, in call_handler
    return handler(*args, **kwargs)
    File "/mnt/data1/dev/projects/NervHome/nvh/crypto/blockchain/handlers/collect_evm_blocks.py", line 36, in handle
    process_block(cdb, tdb, bck, f"{idx*100.0/count:.2f}%: ")
    File "/mnt/data1/dev/projects/NervHome/nvh/crypto/blockchain/handlers/collect_evm_blocks.py", line 67, in process_block
    process_transactions(cdb, tdb, txs)
    File "/mnt/data1/dev/projects/NervHome/nvh/crypto/blockchain/handlers/collect_evm_blocks.py", line 126, in process_transactions
    tdb.insert_swap_tokens_ops(parsed["swap_tokens"])
    File "/mnt/data1/dev/projects/NervHome/nvh/crypto/blockchain/transactions_db.py", line 247, in insert_swap_tokens_ops
    self.execute(self.insert_swap_tokens_sql, rows, many=True, commit=True)
    File "/mnt/data1/dev/projects/NervHome/nvh/crypto/blockchain/transactions_db.py", line 113, in execute
    return self.sql_db.execute(*args, **kaargs)
    File "/mnt/data1/dev/projects/NervHome/nvh/core/postgresql_db.py", line 60, in execute
    c.executemany(code, data)
    psycopg2.errors.NumericValueOutOfRange: "-11579208923731620000000000000000000000000" is out of range for type real
  • Which is pretty interesting… How could I be getting those large negative values ? Let's see…
  • ⇒ So I'm now checking for those errors trying to retrieve a corresponding tx hash:
            if am_in < 0.0 or am_out < 0.0:
                logger.warning("Invalid amounts in tx (hash=%s)", txh)
                utl.send_rocketchat_message(f"Invalid amounts in tx {txh}")
                return None
  • Hmmmm 🤔 Actually this doesn't seem to be it: out amounts are always positive values, but we get the errors elsewhere after we applied our tweak to use negative float values… So this means the input value is larger that 1e74 here ? 😲! So updating the checks to also take this possibility into account:
            if am_in < 0.0 or am_out < 0.0 or am_in > 1e74 or am_out > 1e74:
                logger.warning("Invalid amounts %f or %f in tx (hash=%s)", am_in, am_out, txh)
                utl.send_rocketchat_message(f"Invalid amounts in tx {txh}")
                return None
These problematic cases are most probably just signature collisions with specific contracts implementations doing radically different things than what we are expecting here ⇒ So ignoring those invalid inputs is certainly the best option here.
  • OK: here is a typical example of those invalid transactions: 0x75d3e08ecd4c48789b32bdc39f6e45e01f51ec686d4af2361b38d6897eef936a, amount values are clearly incorrectly provided here. So let's just silently ignore these errors now 👍.
  • Now that I have started to process the raw tx data and extract some high level info from these, I think one additional small step I could take on this point would now be to report the most significant “raw transaction” type on the last few millions transactions once per day, to “remind me” that I should handle them lol.
  • I started with a new handler to simply report a transaction hash given a tx id which I will need to provide an “example” transaction corresponding to a specific signature:
    """Retrieve a Transaction hash given a transaction Id"""
    
    import logging
    
    from nvh.crypto.blockchain.evm_blockchain import EVMBlockchain
    
    logger = logging.getLogger("get_tx_hash")
    
    
    def handle(chain: EVMBlockchain, tid):
        """Handler function entry point"""
    
        tdb = chain.get_tx_db()
    
        tname = tdb.get_txdata_table()
        sql = f"SELECT block_number,tx_index from {tname} WHERE id={tid};"
        cur = tdb.execute(sql)
        row = cur.fetchone()
        if row is None:
            logger.warning("No transaction found with id=%d", tid)
            return None
        bnum, tx_index = row[0], row[1]
    
        # Get the corresponding block:
        bck = chain.get_block(bnum)
    
        txh = bck["transactions"][tx_index].hex()
        return txh
    
  • Next we need to collect the most significant signatures and report only one:
    """Find the most significant signature in the latest transactions"""
    
    import logging
    
    import nvp.core.utils as utl
    
    from nvh.crypto.blockchain.evm_blockchain import EVMBlockchain
    
    logger = logging.getLogger("find_relevant_signatures")
    
    
    def handle(chain: EVMBlockchain, tx_window_size=2000000, sig_count=1, tx_count=5):
        """Handler function entry point"""
    
        tdb = chain.get_tx_db()
    
        tname = tdb.get_txdata_table()
        sql = f"SELECT id,sig,registers FROM {tname} WHERE registers is not null LIMIT {tx_window_size};"
        rows = tdb.execute(sql).fetchall()
        nrows = len(rows)
        logger.info("Got %d rows", nrows)
    
        bcount = 0
    
        size_stats = {}
        sig_tx_ids = {}
    
        for row in rows:
            sig = row[1]
            if sig is None:
                sig = 0
    
            if sig not in size_stats:
                size_stats[sig] = {
                    "sig": hex(utl.to_uint32(sig)),
                    "sig_num": sig,
                    "min_size": 100000,
                    "max_size": 0,
                    "mean_size": 0,
                    "count": 0,
                }
    
            if sig not in sig_tx_ids:
                sig_tx_ids[sig] = []
    
            if len(sig_tx_ids[sig]) < tx_count:
                sig_tx_ids[sig].append(row[0])
    
            desc = size_stats[sig]
            size = 0 if row[2] is None else len(row[2])
            desc["min_size"] = min(desc["min_size"], size)
            desc["max_size"] = max(desc["max_size"], size)
            desc["mean_size"] += size
            desc["count"] += 1
    
            bcount += size
    
        stats = list(size_stats.values())
        for desc in stats:
            desc["share"] = desc["mean_size"]
            desc["mean_size"] /= desc["count"]
    
        stats.sort(key=lambda item: item["share"], reverse=True)
    
        logger.info("Total number of bytes: %d", bcount)
    
        logger.info("Most significant sigs: %s", stats[:sig_count])
        for i in range(sig_count):
            tids = sig_tx_ids[stats[i]["sig_num"]]
            tids = [chain.handle("get_tx_hash", tid) for tid in tids]
            logger.info("Sig %d ref transactions: %s", i, tids)
    
  • And this works great, just calling:
    chain.handle("find_relevant_signatures")
  • I get the curernt result:
    2022/07/03 10:04:51 [find_relevant_signatures] INFO: Got 2000000 rows
    2022/07/03 10:04:54 [find_relevant_signatures] INFO: Total number of bytes: 478017569
    2022/07/03 10:04:54 [find_relevant_signatures] INFO: Most significant sigs: [{'sig': '0xc9807539', 'sig_num': -914328263, 'min_size': 896, 'max_size': 1408, 'mean_size': 1235.1013127734946, 'count': 23995, 'share': 29636256}]
    2022/07/03 10:04:55 [find_relevant_signatures] INFO: Sig 0 ref transactions: ['0x9c45b5dcc9041e921d65f6d07b585c2f15965694341576556be5d4d654ef0a67', '0x92706123bef1c4f7c4728c3390e1962e281044d5b80ba81957bccf98b05a5c96', '0xbe2a21f152210d1613f1ca652aa9314a4ca329a9c70da75856f3ec3580cf9f0a', '0x61cdb2d318f8a2da9265870f493a1a6f74c1134d4c5bd1b44bef4bdf1ce7cb25', '0xd43e26dff6428f5f9b2197c760aa24e4ff7a8b168cd27a3e1dbefc8231e5773d']
  • ⇒ That first signature is interesting: it's for the method transmit(bytes _report, bytes32[] _rs, bytes32[] _ss, bytes32 _rawVs) and it seems to always come from a AccessControlledOffchainAggregator contract: this seems to be related to oracle stuff, so I would not know what to do with that data and I can probably just discard it for now.
  • While working on this I realized that my current way of storing the registers for all the txs was not correct: I would set the regs to None/NULL if there was no value for them, but I should rather use an empty string by default, and only use “NULL” when I have successfully parsed the tx (otherwise I would need an additional boolean flag column to explicitly mark parsed transaction raws).
  • And after some additional tweaking I can now report the most significant tx time I have on a blockchain with this command:
            if cmd == "find-relevant-sig":
                chain_name = self.get_param("chain")
                chain: EVMBlockchain = self.get_component(f"{chain_name}_chain")
                sigs = chain.handle("find_relevant_signatures", verbose=False)
                sig = sigs[0]["sig"]
                txh = sigs[0]["tx_ids"][0]
    
                utl.send_rocketchat_message(f"{chain_name.upper()}: current most significant sig: {sig}, example tx: {txh}")
                return True
  • ⇒ Adding that in my dayly script:
    lfile="${log_dir}/relevant_sigs.log"
    nvp bchain find-relevant-sig -c bsc 2>&1 | tee -a $lfile
    nvp bchain find-relevant-sig -c eth 2>&1 | tee -a $lfile
    nvp bchain find-relevant-sig -c avax 2>&1 | tee -a $lfile
    nvp bchain find-relevant-sig -c celo 2>&1 | tee -a $lfile
    
  • Actually, the next type of signature on the list is really interesting too, here is an example tx hash: 0x8e35dba657657f64c2326837b22757fe2543458ef4237fb682a02f8a3086a494
  • So the sig is “0x7c025200” and according to https://www.4byte.directory/ this is for the method: “swap(address,(address,address,address,address,uint256,uint256,uint256,bytes),bytes)”
  • But in the tx mentioned above the input data is not parsed: strange 🤔 (but at the same time, the function declaration contains some kind of struct as parameter apparently): let's check the contract (which is the 1inch router by the way)
  • Okay hmmm… a pretty complex contract for once (with some assembly code 😎), and I think our call comes from the function:
        function swap(
            IAggregationExecutor caller,
            SwapDescription calldata desc,
            bytes calldata data
        )
  • ⇒ Pretty interesting new stuff: so I should spend some time on it. And in fact this is another kind of swap operation so I should eventually parse it as such.
  • You know what ? Checking some txs around the 1inch router above, I eventually found a contract that has no verified source code, but where we still seem to be calling some regular functions such as safeTransfer(address _token, address _to, uint256 _value).
  • And I'm thinking: what if I wanted to try and hack into this contract ?
  • ⇒ I mean, I would need the contract address sure, but then also a custom “ABI” to use for it: would it be possible to create such in ABI manually or should I rather consider trying to send a raw transaction ? let's think about it a little.
  • So I could write a simple ABI file such as this:
    [
      {
        "inputs": [
          { "internalType": "address", "name": "_token", "type": "address" },
          { "internalType": "address", "name": "_to", "type": "address" },
          { "internalType": "uint256", "name": "_value", "type": "uint256" }
        ],
        "name": "safeTransfer",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
      }
    ]
    
  • And then build a contract with that file content. but actually when creating a contract I first load the content of that ABI file as a regular dict in python! So maybe I could just build that ABI manually just before using it ?
  • ⇒ And so here we go! A first version of new ABIBuilder class:
    class ABIBuilder(NVPObject):
        """Class holding a Contract ABI construction"""
    
        def __init__(self, fnames):
            """Constructor"""
            NVPObject.__init__(self)
            self.abi = []
            self.add_functions(fnames)
    
        def get_abi(self):
            """Retrieve the abi data"""
            return self.abi
    
        def add_function(self, fname):
            """Add a function to the abi"""
            # fname will be a full function declaration, so we parse it as such:
            idx = fname.find("(")
            self.check(idx > 0, "Invalid function name: %s", fname)
            args = fname[idx + 1 : -1]
            fname = fname[:idx]
            logger.info("Adding function '%s' with args '%s'", fname, args)
            inputs = []
            args = [] if args == "" else args.split(",")
    
            for idx, aname in enumerate(args):
                inputs.append({"internalType": aname, "name": f"arg{idx}", "type": aname})
    
            desc = {"inputs": inputs, "name": fname, "outputs": [], "stateMutability": "nonpayable", "type": "function"}
            self.abi.append(desc)
    
        def add_functions(self, fnames):
            """Add a list of functions"""
            for fname in fnames:
                self.add_function(fname)
  • And now I can indirectly use that to generate an ABI dict as I want just before creating a contract:
    abi = chain.build_abi(["safeTransfer(address,address,uint256)", "swap(uint256,uint256,address,address)"])
    print("Generated ABI:", abi)
    sc = chain.get_contract(addr, abi=abi)
  • Or even better: I can just pass those function names to the get_contract function:
    sc = chain.get_contract(addr, funcs=["safeTransfer(address,address,uint256)", "swap(uint256,uint256,address,address)"])
  • ⇒ And this is working very well! That's super cool as it could reduce significantly my dependency on ABIs provided externally, well done Manu 🤣👍!
  • Now, let's setup the same system but for the Benqi protocol on Avalanche:
  • Arrff… I was thinking of using the rewardAccrued map from the benqi controller contract directly, but unfortunately, that map is not updated dynamically, so instead I need to compute the rewards manually using this kind of code:
            RewardMarketState storage supplyState = rewardSupplyState[rewardType][
                qiToken
            ];
            Double memory supplyIndex = Double({mantissa: supplyState.index});
            Double memory supplierIndex = Double({
                mantissa: rewardSupplierIndex[rewardType][qiToken][supplier]
            });
            rewardSupplierIndex[rewardType][qiToken][supplier] = supplyIndex
                .mantissa;
    
            if (supplierIndex.mantissa == 0 && supplyIndex.mantissa > 0) {
                supplierIndex.mantissa = initialIndexConstant;
            }
    
            Double memory deltaIndex = sub_(supplyIndex, supplierIndex);
            uint256 supplierTokens = QiToken(qiToken).balanceOf(supplier);
            uint256 supplierDelta = mul_(supplierTokens, deltaIndex);
            uint256 supplierAccrued = add_(
                rewardAccrued[rewardType][supplier],
                supplierDelta
            );
            rewardAccrued[rewardType][supplier] = supplierAccrued;
  • Okay, so here is the corresponding python version which seems to provide correct values:
        def get_pending_rewards(self, as_value=False):
            """report the pending rewards"""
    
            # We call the function "pendingBELT" to get the pending belt reward
            addr = self.chain.get_account_address()
    
            # Need to manually compute the pending rewards here:
            qi_btc_addr = self.config["qi_btc_address"]
            qitoken = self.chain.get_token(qi_btc_addr)
            supplier_tokens = qitoken.get_balance(as_value=False)
    
            res = self.ctrl_sc.call_function("rewardSupplyState", 0, qi_btc_addr)
            supply_index = res[0]
            # supply_ts = res[1]
            # logger.info("supply state: %s", res)
    
            supplier_index = self.ctrl_sc.call_function("rewardSupplierIndex", 0, qi_btc_addr, addr)
            # logger.info("supplier index: %d", supplier_index)
    
            delta_index = supply_index - supplier_index
            # logger.info("qi_btc balance: %d", supplier_tokens)
            pending_qi = (supplier_tokens * delta_index) // 1e36
    
            # Compute AVAX reward:
            res = self.ctrl_sc.call_function("rewardSupplyState", 1, qi_btc_addr)
            supply_index = res[0]
            supplier_index = self.ctrl_sc.call_function("rewardSupplierIndex", 1, qi_btc_addr, addr)
            delta_index = supply_index - supplier_index
    
            pending_avax = (supplier_tokens * delta_index) // 1e36
    
            if as_value:
                pending_qi = self.qi_token.to_value(pending_qi)
                pending_avax = self.avax_token.to_value(pending_avax)
    
            return pending_qi, pending_avax
  • Appart from that, “regular stuff” 😀 So I don't think there is anything else to discuss on this one. Here is the final full script:
    """BenqiFinance protocol."""
    
    import logging
    import time
    
    from nvp.nvp_component import NVPComponent
    from nvp.nvp_context import NVPContext
    
    from nvh.crypto.blockchain.evm_blockchain import EVMBlockchain
    
    logger = logging.getLogger(__name__)
    
    
    class BenqiFinance(NVPComponent):
        """BenqiFinance component class"""
    
        def __init__(self, ctx, chain=None):
            """Component constructor"""
            NVPComponent.__init__(self, ctx)
    
            if chain is None:
                chain = self.ctx.get_component("avax_chain")
    
            self.chain: EVMBlockchain = chain
    
            self.config = self.ctx.get_config()["benqi_finance"]
    
            logger.debug("Loading Benqi finance controller contract...")
            self.ctrl_sc = self.chain.get_contract(
                self.config["controller_address"], abi_file="ABI/avax_benqi_controller.json"
            )
    
            self.qi_token = self.chain.get_token(self.config["qi_token_address"])
            self.avax_token = self.chain.get_wrapped_native_token()
            self.usdc_token = self.chain.get_token("USDC")
    
        def get_pending_rewards(self, as_value=False):
            """report the pending rewards"""
    
            # We call the function "pendingBELT" to get the pending belt reward
            addr = self.chain.get_account_address()
    
            # Need to manually compute the pending rewards here:
            qi_btc_addr = self.config["qi_btc_address"]
            qitoken = self.chain.get_token(qi_btc_addr)
            supplier_tokens = qitoken.get_balance(as_value=False)
    
            res = self.ctrl_sc.call_function("rewardSupplyState", 0, qi_btc_addr)
            supply_index = res[0]
            # supply_ts = res[1]
            # logger.info("supply state: %s", res)
    
            supplier_index = self.ctrl_sc.call_function("rewardSupplierIndex", 0, qi_btc_addr, addr)
            # logger.info("supplier index: %d", supplier_index)
    
            delta_index = supply_index - supplier_index
            # logger.info("qi_btc balance: %d", supplier_tokens)
            pending_qi = (supplier_tokens * delta_index) // 1e36
    
            # Compute AVAX reward:
            res = self.ctrl_sc.call_function("rewardSupplyState", 1, qi_btc_addr)
            supply_index = res[0]
            supplier_index = self.ctrl_sc.call_function("rewardSupplierIndex", 1, qi_btc_addr, addr)
            delta_index = supply_index - supplier_index
    
            pending_avax = (supplier_tokens * delta_index) // 1e36
    
            if as_value:
                pending_qi = self.qi_token.to_value(pending_qi)
                pending_avax = self.avax_token.to_value(pending_avax)
    
            return pending_qi, pending_avax
    
        def get_qi_price(self):
            """Get the price of QI in USD"""
            dex = self.chain.get_default_exchange()
            return dex.get_quote(1.0, self.qi_token.address(), "USDC")
    
        def get_avax_price(self):
            """Get the price of AVAX in USD"""
            dex = self.chain.get_default_exchange()
            return dex.get_quote(1.0, "AVAX", "USDC")
    
        def collect_reward_type(self, rtype, token, amount, price, qi_tokens, min_usd_value, to_stablecoin):
            """Collect reward type"""
            val = token.to_value(amount)
    
            usd_val = val * price
            sym = token.symbol()
    
            # Will receive native tokens directly here:
            is_native = sym == "WAVAX"
            if is_native:
                sym = "AVAX"
    
            if usd_val < min_usd_value:
                logger.info("Not claiming %f %s: value too low: %.4f USD", val, sym, usd_val)
                return
    
            # Otherwise we can perform the claim:
            logger.info("Claiming %s rewards...", sym)
            addr = self.chain.get_account_address()
    
            opr = self.ctrl_sc.build_function_call("claimReward", rtype, addr, qi_tokens)
    
            b0 = self.chain.get_balance(sym, as_value=False)
            if self.chain.perform_operation(opr, self.config["harvest_max_gas"]) is not None:
                b1 = self.chain.get_balance(sym, as_value=False)
                while b1 == b0:
                    logger.info("Waiting for %s balance update...", sym)
                    time.sleep(1.0)
                    b1 = self.chain.get_balance(sym, as_value=False)
                got = b1 - b0
                val = token.to_value(got)
                logger.info("=> Claimed %f %s from benqi finance", val, sym)
    
                if not to_stablecoin:
                    # Send a collect message right now:
                    rchat = self.get_component("rchat")
                    msg = ":white_check_mark: 💰 *[Benqi.finance]*: "
                    msg += f"Claimed {val} {sym} from benqi finance."
                    rchat.send_message(msg)
    
            b1 = self.chain.get_balance(sym, as_value=False)
            if b1 == b0:
                logger.info("Did not collect any %s token: not swapping for USDC.", sym)
                return
    
            got = b1 - b0
            val = token.to_value(got)
            logger.info("Swapping %f %s for USDC...", val, sym)
            usd_b0 = self.usdc_token.get_balance(as_value=False)
            dex = self.chain.get_default_exchange()
            addr1 = self.usdc_token.address()
            dex.swap_tokens(got, [sym, addr1])
            usd_b1 = self.usdc_token.get_balance(as_value=False)
            usd_val = self.usdc_token.to_value(usd_b1 - usd_b0)
    
            logger.info("=> Got %f BUSD from %f %s", usd_val, val, sym)
    
            rchat = self.get_component("rchat")
            msg = ":white_check_mark: 💰 *[Benqi.finance]*: "
            msg += f"Swapped {val:.6g} {sym} to {usd_val:.6g} USDC"
            rchat.send_message(msg)
    
        def collect_pending_rewards(self, min_usd_value=5.0, to_stablecoin=False):
            """Collect the pending rewards"""
    
            amount_qi, amount_avax = self.get_pending_rewards(as_value=False)
            price_qi = self.get_qi_price()
            price_avax = self.get_avax_price()
    
            qi_tokens = [self.config["qi_btc_address"]]
    
            self.collect_reward_type(0, self.qi_token, amount_qi, price_qi, qi_tokens, min_usd_value, to_stablecoin)
            self.collect_reward_type(1, self.avax_token, amount_avax, price_avax, qi_tokens, min_usd_value, False)
    
        def process_cmd_path(self, cmd):
            """Check if this component can process the given command"""
    
            if cmd == "price":
                p1 = self.get_qi_price()
                p2 = self.get_avax_price()
                logger.info("Current QI price: %.6g USDC", p1)
                logger.info("Current AVAX price: %.6g USDC", p2)
                return True
    
            if cmd == "pending":
                r1, r2 = self.get_pending_rewards(as_value=True)
                p1 = self.get_qi_price()
                p2 = self.get_avax_price()
                logger.info(
                    "Pending rewards: %.6f QI (value: %.6f USDC), %.6f AVAX (value: %.6f USDC)", r1, r1 * p1, r2, r2 * p2
                )
                return True
    
            if cmd == "collect":
                min_val = self.get_param("min_value")
                to_stable = self.get_param("to_stable")
                self.collect_pending_rewards(min_val, to_stable)
                return True
    
            return False
    
    
    if __name__ == "__main__":
        # Create the context:
        context = NVPContext()
    
        # Add our component:
        comp = context.register_component("belt_finance", BenqiFinance(context))
    
        context.define_subparsers("main", ["price", "pending"])
    
        psr = context.build_parser("collect")
        psr.add_float("--min", dest="min_value", default=5.0)("Minimal claim value in USDC")
        psr.add_flag("-s", "--stable", dest="to_stable")("Swap rewards for stablecoin")
    
        comp.run()
    
  • Continuing with our journey, let's try to also collect my rewards from my staked Cakes in pancakeswap2:
  • First refering to an existing transaction: OK
  • So to claim the rewards we interact with the contract at 0x0F96E19Bdc787e767BA1e8F1aDD0f62cbdad87C8
  • And we call the function deposit(uint256 _amount) with an amount value of 0
  • Let's check the contract itself a bit. OK
  • And now here is mu own class to handle this pancakeswap2 pool:
    """PancakeSwap2 protocol."""
    
    import logging
    import time
    
    from nvp.nvp_component import NVPComponent
    from nvp.nvp_context import NVPContext
    
    from nvh.crypto.blockchain.evm_blockchain import EVMBlockchain
    
    logger = logging.getLogger(__name__)
    
    
    class PancakeSwap2(NVPComponent):
        """PancakeSwap2 component class"""
    
        def __init__(self, ctx, chain=None):
            """Component constructor"""
            NVPComponent.__init__(self, ctx)
    
            if chain is None:
                chain = self.ctx.get_component("bsc_chain")
    
            self.chain: EVMBlockchain = chain
    
            self.config = self.ctx.get_config()["pancakeswap2"]
    
            logger.debug("Loading PancakeSwap2 chef contract...")
            self.chef_sc = self.chain.get_contract(self.config["chef_address"], abi_file="ABI/bsc_pancakeswap2.json")
    
            addr = self.chef_sc.call_function("rewardToken")
            self.reward_token = self.chain.get_token(addr)
            self.symbol = self.reward_token.symbol()
            logger.info("Reward token is: %s", self.symbol)
            self.busd_token = self.chain.get_token("BUSD")
    
        def get_pending_rewards(self, as_value=False):
            """report the pending rewards"""
    
            # We call the function "pendingReward" to get the pending rewards
            addr = self.chain.get_account_address()
    
            # The only pool we are interested in for the moment is the beltETH pool with pid==8
            pending = self.chef_sc.call_function("pendingReward", addr)
            if as_value:
                return self.reward_token.to_value(pending)
            return pending
    
        def get_reward_token_price(self):
            """Get the price of reward token in USD"""
            dex = self.chain.get_default_exchange()
            return dex.get_quote(1.0, self.reward_token.address(), "BUSD")
    
        def collect_pending_rewards(self, min_usd_value=5.0, to_stablecoin=False):
            """Collect the pending rewards"""
    
            amount = self.get_pending_rewards(as_value=False)
            val = self.reward_token.to_value(amount)
            price = self.get_reward_token_price()
    
            sym = self.symbol
            usd_val = val * price
            if usd_val < min_usd_value:
                logger.info("Not claiming %f %s: value too low: %.4f BUSD", val, sym, usd_val)
                return
    
            # Otherwise we can perform the claim:
            logger.info("Claiming %s rewards...", sym)
            opr = self.chef_sc.build_function_call("deposit", 0)
    
            b0 = self.reward_token.get_balance(as_value=False)
            if self.chain.perform_operation(opr, self.config["harvest_max_gas"]) is not None:
                b1 = self.reward_token.get_balance(as_value=False)
                while b1 == b0:
                    logger.info("Waiting for %s balance update...", sym)
                    time.sleep(1.0)
                    b1 = self.reward_token.get_balance(as_value=False)
                got = b1 - b0
                val = self.reward_token.to_value(got)
                logger.info("=> Claimed %f %s from pancakeswap2", val, sym)
    
                if not to_stablecoin:
                    # Send a collect message right now:
                    rchat = self.get_component("rchat")
                    msg = ":white_check_mark: 💰 *[PancakeSwap2]*: "
                    msg += f"Claimed {val} {sym} from pancakeswap2 pool"
                    rchat.send_message(msg)
    
            b1 = self.reward_token.get_balance(as_value=False)
            if b1 == b0:
                logger.info("Did not collect any %s token: not swapping for BUSD.", sym)
                return
    
            # got = b1 - b0
            got = b1
            # We swapp **ALL** the available BELTs below not just what we received
            val = self.reward_token.to_value(got)
            logger.info("Swapping %f %s for BUSD...", val, sym)
            usd_b0 = self.busd_token.get_balance(as_value=False)
            dex = self.chain.get_default_exchange()
            addr0 = self.reward_token.address()
            addr1 = self.busd_token.address()
            dex.swap_tokens(got, [addr0, addr1])
            usd_b1 = self.busd_token.get_balance(as_value=False)
            usd_val = self.busd_token.to_value(usd_b1 - usd_b0)
    
            logger.info("=> Got %f BUSD from %f %s", usd_val, val, sym)
    
            rchat = self.get_component("rchat")
            msg = ":white_check_mark: 💰 *[PancakeSwap2]*: "
            msg += f"Swapped {val:.6g} {sym} to {usd_val:.6g} BUSD"
            rchat.send_message(msg)
    
        def process_cmd_path(self, cmd):
            """Check if this component can process the given command"""
    
            if cmd == "price":
                price = self.get_reward_token_price()
                logger.info("Current %s price: %.6g BUSD", self.symbol, price)
                return True
    
            if cmd == "pending":
                val = self.get_pending_rewards(as_value=True)
                price = self.get_reward_token_price()
                logger.info("Pending rewards: %.6f %s (value: %.6f BUSD)", val, self.symbol, val * price)
                return True
    
            if cmd == "collect":
                min_val = self.get_param("min_value")
                to_stable = self.get_param("to_stable")
                self.collect_pending_rewards(min_val, to_stable)
                return True
    
            return False
    
    
    if __name__ == "__main__":
        # Create the context:
        context = NVPContext()
    
        # Add our component:
        comp = context.register_component("pancakeswap2_pools", PancakeSwap2(context))
    
        context.define_subparsers("main", ["price", "pending"])
    
        psr = context.build_parser("collect")
        psr.add_float("--min", dest="min_value", default=5.0)("Minimal claim value in BUSD")
        psr.add_flag("-s", "--stable", dest="to_stable")("Swap BELT for stablecoin")
    
        comp.run()
    
  • Finally we just add a cron task for that and we should be good: OK
  • And here we are! We have automatic claims for 3 different protocols above, not bad, so let's stop here for this session! See you in the next one ✌️
  • blog/2022/0706_automating_defi_claims.txt
  • Last modified: 2022/07/06 17:01
  • by 127.0.0.1