Most ERC-20 tokens, taking advantage of EVM’s 32 byte word size, use high bit integers for balances. An 18 decimal place token with a nominal value of “1” is represented within EVM as 1,000,000,000,000,000,000.
Parts I-III of the Convex Optimization series have used relatively small values to illustrate the concept.
However these lessons have hidden some complexity for the sake of instruction, which we must now deal with.
We return to the two-pool vectorized convex optimization example from Part II:
uniswap_v2_convex_2pool_vector.py
import cvxpy
import numpy
from fractions import Fraction
NUM_TOKENS = 2
fees = [
Fraction(997, 1000), # Pool A
Fraction(997, 1000), # Pool B
]
reserves_starting = [
numpy.array([100_000, 50_000]), # Pool A
numpy.array([110_000, 50_000]), # Pool B
]
deposits = [
cvxpy.Variable(NUM_TOKENS, nonneg=True), # Pool A
cvxpy.Variable(NUM_TOKENS, nonneg=True), # Pool B
]
withdrawals = [
cvxpy.Variable(NUM_TOKENS, nonneg=True), # Pool A
cvxpy.Variable(NUM_TOKENS, nonneg=True), # Pool B
]
reserves_after_swap = [
starting_reserves + fee * deposit - withdrawal
for starting_reserves, fee, deposit, withdrawal in zip(
reserves_starting,
fees,
deposits,
withdrawals,
)
]
final_reserves = [
starting_reserves + deposit - withdrawal
for starting_reserves, deposit, withdrawal in zip(
reserves_starting,
deposits,
withdrawals,
)
]
profit = (
# swap Y->X at pool B, swap X->Y at pool A, keep difference dY
withdrawals[0][1] - deposits[1][1]
)
constraints = [
# Received amount at PoolB is deposited at PoolA
withdrawals[1][0] == deposits[0][0],
# Only swap one token at a time
deposits[0][1] == 0,
deposits[1][0] == 0,
withdrawals[0][0] == 0,
withdrawals[1][1] == 0,
# Enforce x*y=k invariants
cvxpy.geo_mean(
reserves_after_swap[0]
) >= cvxpy.geo_mean(reserves_starting[0]),
cvxpy.geo_mean(
reserves_after_swap[1]
) >= cvxpy.geo_mean(reserves_starting[1]),
]
# Define and solve the problem
problem = cvxpy.Problem(
objective=cvxpy.Maximize(profit),
constraints=constraints,
)
problem.solve(verbose=False)
print(f"Solved. profit={profit.value}")
print(f"Pool A reserves (after swap): {reserves_after_swap[0].value}")
print(f"Pool B reserves (after swap): {reserves_after_swap[1].value}")
print(f"Forward token amount (X): {deposits[0][0].value}")
Now consider that the pools in question are holding 18 decimal place ERC-20 tokens. For example, instead of a 50,000 balance of token Y in each pool, the balance is instead 50,000,000,000,000,000,000,000, or 50,000 * 10**18.
Changing only this block:
DECIMALS = 18
reserves_starting = [
numpy.array([
100_000 * 10**DECIMALS,
50_000 * 10**DECIMALS,
]), # Pool A
numpy.array([
110_000 * 10**DECIMALS,
50_000 * 10**DECIMALS,
]), # Pool B
]
Then running, we get a disturbing error!
Traceback (most recent call last):
File "/home/btd/code/convex/uniswap_v2_convex_2pool_vector_scaled.py", line 74, in <module>
raise ValueError("Problem could not be solved optimally.")
ValueError: Problem could not be solved optimally.
Adding the verbose=True
option to problem.solve()
, we get some more useful information: