Repository: Cog-Creators/Red-DiscordBot

Issue:
[V3 Config] Overflow error
# Other bugs

<!-- 
Did you find a bug with something other than a command? Fill out the following:
-->

#### What were you trying to do?

`bank set <some integer greater than 2^63-1>`

#### What were you expecting to happen?

The credits to be set to the specified integer.

#### What actually happened?

```py
ERROR events on_command_error 205: Exception in command 'bank set'
Traceback (most recent call last):
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/discord/ext/commands/core.py", line 61, in wrapped
    ret = await coro(*args, **kwargs)
  File "/home/zephyrkul/.pyenv/versions/3.7.0/envs/red/lib/python3.7/site-packages/redbot/cogs/economy/economy.py", line 206, in _set
    await bank.set_balance(to, creds.sum)
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/redbot/core/bank.py", line 181, in set_balance
    await group.balance.set(amount)
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/redbot/core/config.py", line 152, in set
    await self.driver.set(*self.identifiers, value=value)
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/redbot/core/drivers/red_mongo.py", line 95, in set
    upsert=True,
  File "/home/zephyrkul/.pyenv/versions/3.7.0/lib/python3.7/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/pymongo/collection.py", line 995, in update_one
    session=session),
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/pymongo/collection.py", line 851, in _update_retryable
    _update, session)
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/pymongo/mongo_client.py", line 1243, in _retryable_write
    return self._retry_with_session(retryable, func, s, None)
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/pymongo/mongo_client.py", line 1196, in _retry_with_session
    return func(session, sock_info, retryable)
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/pymongo/collection.py", line 847, in _update
    retryable_write=retryable_write)
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/pymongo/collection.py", line 817, in _update
    retryable_write=retryable_write).copy()
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/pymongo/pool.py", line 584, in command
    self._raise_connection_failure(error)
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/pymongo/pool.py", line 745, in _raise_connection_failure
    raise error
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/pymongo/pool.py", line 579, in command
    unacknowledged=unacknowledged)
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/pymongo/network.py", line 114, in command
    codec_options, ctx=compression_ctx)
  File "/home/zephyrkul/.pyenv/versions/red/lib/python3.7/site-packages/pymongo/message.py", line 679, in _op_msg
    flags, command, identifier, docs, check_keys, opts)
OverflowError: MongoDB can only handle up to 8-byte ints
```

#### How can we reproduce this issue?

Have a mongodb backend and store a config value larger than 2^63-1 (9223372036854775807).



Discussion:

Resolving patch:
diff --git a/redbot/cogs/economy/economy.py b/redbot/cogs/economy/economy.py
--- a/redbot/cogs/economy/economy.py
+++ b/redbot/cogs/economy/economy.py
@@ -8,7 +8,7 @@
 import discord
 
 from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
-from redbot.core import Config, bank, commands
+from redbot.core import Config, bank, commands, errors
 from redbot.core.i18n import Translator, cog_i18n
 from redbot.core.utils.chat_formatting import box
 from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
@@ -171,7 +171,7 @@ async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int)
 
         try:
             await bank.transfer_credits(from_, to, amount)
-        except ValueError as e:
+        except (ValueError, errors.BalanceTooHigh) as e:
             return await ctx.send(str(e))
 
         await ctx.send(
@@ -195,36 +195,35 @@ async def _set(self, ctx: commands.Context, to: discord.Member, creds: SetParser
         author = ctx.author
         currency = await bank.get_currency_name(ctx.guild)
 
-        if creds.operation == "deposit":
-            await bank.deposit_credits(to, creds.sum)
-            await ctx.send(
-                _("{author} added {num} {currency} to {user}'s account.").format(
+        try:
+            if creds.operation == "deposit":
+                await bank.deposit_credits(to, creds.sum)
+                msg = _("{author} added {num} {currency} to {user}'s account.").format(
                     author=author.display_name,
                     num=creds.sum,
                     currency=currency,
                     user=to.display_name,
                 )
-            )
-        elif creds.operation == "withdraw":
-            await bank.withdraw_credits(to, creds.sum)
-            await ctx.send(
-                _("{author} removed {num} {currency} from {user}'s account.").format(
+            elif creds.operation == "withdraw":
+                await bank.withdraw_credits(to, creds.sum)
+                msg = _("{author} removed {num} {currency} from {user}'s account.").format(
                     author=author.display_name,
                     num=creds.sum,
                     currency=currency,
                     user=to.display_name,
                 )
-            )
-        else:
-            await bank.set_balance(to, creds.sum)
-            await ctx.send(
-                _("{author} set {user}'s account balance to {num} {currency}.").format(
+            else:
+                await bank.set_balance(to, creds.sum)
+                msg = _("{author} set {user}'s account balance to {num} {currency}.").format(
                     author=author.display_name,
                     num=creds.sum,
                     currency=currency,
                     user=to.display_name,
                 )
-            )
+        except (ValueError, errors.BalanceTooHigh) as e:
+            await ctx.send(str(e))
+        else:
+            await ctx.send(msg)
 
     @_bank.command()
     @check_global_setting_guildowner()
@@ -260,7 +259,18 @@ async def payday(self, ctx: commands.Context):
         if await bank.is_global():  # Role payouts will not be used
             next_payday = await self.config.user(author).next_payday()
             if cur_time >= next_payday:
-                await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS())
+                try:
+                    await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS())
+                except errors.BalanceTooHigh as exc:
+                    await bank.set_balance(author, exc.max_balance)
+                    await ctx.send(
+                        _(
+                            "You've reached the maximum amount of {currency}! (**{balance:,}**) "
+                            "Please spend some more \N{GRIMACING FACE}\n\n"
+                            "You currently have {new_balance} {currency}."
+                        ).format(currency=credits_name, new_balance=exc.max_balance)
+                    )
+                    return
                 next_payday = cur_time + await self.config.PAYDAY_TIME()
                 await self.config.user(author).next_payday.set(next_payday)
 
@@ -297,14 +307,25 @@ async def payday(self, ctx: commands.Context):
                     ).PAYDAY_CREDITS()  # Nice variable name
                     if role_credits > credit_amount:
                         credit_amount = role_credits
-                await bank.deposit_credits(author, credit_amount)
+                try:
+                    await bank.deposit_credits(author, credit_amount)
+                except errors.BalanceTooHigh as exc:
+                    await bank.set_balance(author, exc.max_balance)
+                    await ctx.send(
+                        _(
+                            "You've reached the maximum amount of {currency}! "
+                            "Please spend some more \N{GRIMACING FACE}\n\n"
+                            "You currently have {new_balance} {currency}."
+                        ).format(currency=credits_name, new_balance=exc.max_balance)
+                    )
+                    return
                 next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
                 await self.config.member(author).next_payday.set(next_payday)
                 pos = await bank.get_leaderboard_position(author)
                 await ctx.send(
                     _(
                         "{author.mention} Here, take some {currency}. "
-                        "Enjoy! (+{amount} {new_balance}!)\n\n"
+                        "Enjoy! (+{amount} {currency}!)\n\n"
                         "You currently have {new_balance} {currency}.\n\n"
                         "You are currently #{pos} on the global leaderboard!"
                     ).format(
@@ -444,7 +465,21 @@ async def slot_machine(author, channel, bid):
             then = await bank.get_balance(author)
             pay = payout["payout"](bid)
             now = then - bid + pay
-            await bank.set_balance(author, now)
+            try:
+                await bank.set_balance(author, now)
+            except errors.BalanceTooHigh as exc:
+                await bank.set_balance(author, exc.max_balance)
+                await channel.send(
+                    _(
+                        "You've reached the maximum amount of {currency}! "
+                        "Please spend some more \N{GRIMACING FACE}\n{old_balance} -> {new_balance}!"
+                    ).format(
+                        currency=await bank.get_currency_name(getattr(channel, "guild", None)),
+                        old_balance=then,
+                        new_balance=exc.max_balance,
+                    )
+                )
+                return
             phrase = T_(payout["phrase"])
         else:
             then = await bank.get_balance(author)
@@ -561,10 +596,10 @@ async def paydaytime(self, ctx: commands.Context, seconds: int):
     async def paydayamount(self, ctx: commands.Context, creds: int):
         """Set the amount earned each payday."""
         guild = ctx.guild
-        credits_name = await bank.get_currency_name(guild)
-        if creds <= 0:
+        if creds <= 0 or creds > bank.MAX_BALANCE:
             await ctx.send(_("Har har so funny."))
             return
+        credits_name = await bank.get_currency_name(guild)
         if await bank.is_global():
             await self.config.PAYDAY_CREDITS.set(creds)
         else:
@@ -579,6 +614,9 @@ async def paydayamount(self, ctx: commands.Context, creds: int):
     async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
         """Set the amount earned each payday for a role."""
         guild = ctx.guild
+        if creds <= 0 or creds > bank.MAX_BALANCE:
+            await ctx.send(_("Har har so funny."))
+            return
         credits_name = await bank.get_currency_name(guild)
         if await bank.is_global():
             await ctx.send(_("The bank must be per-server for per-role paydays to work."))
diff --git a/redbot/core/bank.py b/redbot/core/bank.py
--- a/redbot/core/bank.py
+++ b/redbot/core/bank.py
@@ -4,9 +4,10 @@
 
 import discord
 
-from redbot.core import Config
+from . import Config, errors
 
 __all__ = [
+    "MAX_BALANCE",
     "Account",
     "get_balance",
     "set_balance",
@@ -26,6 +27,8 @@
     "set_default_balance",
 ]
 
+MAX_BALANCE = 2 ** 63 - 1
+
 _DEFAULT_GLOBAL = {
     "is_global": False,
     "bank_name": "Twentysix bank",
@@ -170,10 +173,22 @@ async def set_balance(member: discord.Member, amount: int) -> int:
     ------
     ValueError
         If attempting to set the balance to a negative number.
+    BalanceTooHigh
+        If attempting to set the balance to a value greater than
+        ``bank.MAX_BALANCE``
 
     """
     if amount < 0:
         raise ValueError("Not allowed to have negative balance.")
+    if amount > MAX_BALANCE:
+        currency = (
+            await get_currency_name()
+            if await is_global()
+            else await get_currency_name(member.guild)
+        )
+        raise errors.BalanceTooHigh(
+            user=member.display_name, max_balance=MAX_BALANCE, currency_name=currency
+        )
     if await is_global():
         group = _conf.user(member)
     else:
diff --git a/redbot/core/errors.py b/redbot/core/errors.py
--- a/redbot/core/errors.py
+++ b/redbot/core/errors.py
@@ -1,4 +1,11 @@
 import importlib.machinery
+from typing import Optional
+
+import discord
+
+from .i18n import Translator
+
+_ = Translator(__name__, __file__)
 
 
 class RedError(Exception):
@@ -14,3 +21,24 @@ def __init__(self, spec: importlib.machinery.ModuleSpec, *args, **kwargs):
 
     def __str__(self) -> str:
         return f"There is already a package named {self.spec.name.split('.')[-1]} loaded"
+
+
+class BankError(RedError):
+    """Base error class for bank-related errors."""
+
+
+class BalanceTooHigh(BankError, OverflowError):
+    """Raised when trying to set a user's balance to higher than the maximum."""
+
+    def __init__(
+        self, user: discord.abc.User, max_balance: int, currency_name: str, *args, **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.user = user
+        self.max_balance = max_balance
+        self.currency_name = currency_name
+
+    def __str__(self) -> str:
+        return _("{user}'s balance cannot rise above {max:,} {currency}.").format(
+            user=self.user, max=self.max_balance, currency=self.currency_name
+        )


Test patch: