Skip to content

Commit 4257612

Browse files
committed
Token approval bugfixes & other improvements
- Added token approval for vault token (required once for withdraw) - Added token allowance assertions - Added checksum() utility function
1 parent 3124ae5 commit 4257612

File tree

7 files changed

+105
-14
lines changed

7 files changed

+105
-14
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ How to use the package
122122
123123
124124
# Approve a token for deposit to the vault (only required once if never approved):
125-
position.do_approve_deposit_token(<token_address> or <BEP20Token object>)
125+
position.do_approve_token(<token_address> or <BEP20Token object>)
126+
# Approve the vault token for withdraw if using the "Convert All" strategy (only required once if never approved):
127+
position.do_approve_token(position.bep20_vault_token, _spender=position.gateway.address)
126128
127129
128130
# Invest the given amount of stable and asset token into the vault:

alpaca_finance/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.0.0"
1+
__version__ = "1.1.1"

alpaca_finance/automated_vault/contracts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def __init__(self, vault_address: str, w3_provider: Web3 = None, protocol: str =
3737
"""
3838
AVAILABLE_PROTOCOLS = ["pancakeswap", "biswap"]
3939

40+
self.address = vault_address
4041
self.contract = get_bsc_contract_instance(contract_address=vault_address,
4142
abi_filename="DeltaNeutralVault.json", w3_provider=w3_provider)
4243
self.protocol = protocol.lower()
@@ -141,6 +142,7 @@ def assetTokenAddress(self) -> str:
141142

142143
class DeltaNeutralVaultGateway:
143144
def __init__(self, gateway_address: str, w3_provider: Web3 = None):
145+
self.address = gateway_address
144146
self.contract = get_bsc_contract_instance(contract_address=gateway_address,
145147
abi_filename="DeltaNeutralVaultGateway.json", w3_provider=w3_provider)
146148

alpaca_finance/automated_vault/position.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Union, Optional
22
from math import floor
33

4-
from ..util import get_entry_prices, get_web3_provider, get_vault_addresses
4+
from ..util import get_entry_prices, get_web3_provider, get_vault_addresses, checksum
55
from ._config import DEFAULT_BSC_RPC_URL
66
from .receipt import TransactionReceipt, build_receipt
77
from .contracts import DeltaNeutralVault, DeltaNeutralOracle, AutomatedVaultController, DeltaNeutralVaultGateway
@@ -10,6 +10,7 @@
1010
import requests
1111
import web3.contract
1212
from web3 import Web3
13+
from web3.constants import MAX_INT
1314
from attrdict import AttrDict
1415
from bep20 import BEP20Token
1516

@@ -47,18 +48,33 @@ def __init__(self, position_key: str, owner_wallet_address: str, owner_wallet_ke
4748

4849
""" ------------------ Transactional Methods (Requires private wallet key) ------------------ """
4950

50-
def do_invest(self, stable_token_amt: int = 0, asset_token_amt: int = 0) -> TransactionReceipt:
51+
def do_invest(self, stable_token_amt: int = 0, asset_token_amt: int = 0, _approve=False) -> TransactionReceipt:
5152
"""
5253
Invest the specified amount of each token into the Automated Vault.
5354
Use self.asset_token and self.stable_token to identify the underlying assets.
5455
5556
:param stable_token_amt: The amount of stable token to deposit
5657
:param asset_token_amt: The amount of asset token to deposit
58+
:param _approve: If True, approves the deposit token for spending by the vault token
5759
:return:
5860
"""
5961
assert stable_token_amt > 0 or asset_token_amt > 0, \
6062
"Please provide an investment value for either the stable or asset tokens"
6163

64+
# Ensure that allowances match desired investment amount
65+
if stable_token_amt > 0:
66+
token_bal = self.stable_token.balanceOf(self.owner_address)
67+
assert token_bal >= stable_token_amt, \
68+
f"Insufficient funds to invest {stable_token_amt} {self.stable_token.symbol()} ({token_bal} Owned)"
69+
if _approve:
70+
self.do_approve_token(self.stable_token)
71+
if asset_token_amt > 0:
72+
token_bal = self.asset_token.balanceOf(self.owner_address)
73+
assert token_bal >= asset_token_amt, \
74+
f"Insufficient funds to invest {asset_token_amt} {self.asset_token.symbol()} ({token_bal} Owned)"
75+
if _approve:
76+
self.do_approve_token(self.asset_token)
77+
6278
return self._execute(self.vault.invest(stable_token_amt, asset_token_amt, shareReceiver=self.owner_address))
6379

6480
def do_withdraw(self, shares: int, pct_stable: float = None, strategy: str = "Minimize Trading") -> TransactionReceipt:
@@ -68,14 +84,23 @@ def do_withdraw(self, shares: int, pct_stable: float = None, strategy: str = "Mi
6884
:param shares: The amount of share to withdraw from the vault (in share tokens) (self.shares()[0] = close position)
6985
:param pct_stable: The percentage of stable token returned to the owner (.50 = 50% stable and 50% asset returned)
7086
:param strategy: The strategy to use to withdraw, as shown on the webapp (Minimize Trading, Convert All)
87+
(NOT IN USE) :param _approve: If True, force approves the vault token to be spent by either the gateway or the vault contract
7188
"""
7289
assert self.shares()[0] >= shares, f"Shares owned insufficient to withdraw {shares} " \
7390
f"({self.from_wei(shares, self.bep20_vault_token.decimals())}) shares"
7491

7592
if strategy.lower() == "minimize trading":
93+
# assert self.bep20_vault_token.allowance(self.owner_address, self.vault.address) >= shares, \
94+
# f"Insufficient approval amount - Spender ({self.vault.address}) requires an allowance of {shares} " \
95+
# f"{self.bep20_vault_token.symbol()} ({self.bep20_vault_token.address})"
96+
7697
return self._execute(self.vault.withdraw(shares))
7798
elif strategy.lower() == "convert all":
7899
assert pct_stable is not None, "Please provide a stable token percentage to determine token swap"
100+
assert self.bep20_vault_token.allowance(self.owner_address, self.gateway.address) >= shares, \
101+
f"Insufficient approval amount - Spender ({self.gateway.address}) requires an allowance of {shares} " \
102+
f"{self.bep20_vault_token.symbol()} ({self.bep20_vault_token.address})"
103+
79104
assert 0.0 < pct_stable <= 1.0, "Invalid value for pct_stable parameter, must follow 0.0 < pct_stable <= 1.0"
80105

81106
stable_return_bps = floor(pct_stable * 10000)
@@ -93,17 +118,26 @@ def do_close(self, pct_stable: float = None, strategy: str = "Minimize Trading")
93118
"""
94119
return self.do_withdraw(shares=self.shares()[0], pct_stable=pct_stable, strategy=strategy)
95120

96-
def do_approve_deposit_token(self, token: Union[BEP20Token, str], amount: int = None) -> TransactionReceipt:
121+
def do_approve_token(self, token: Union[BEP20Token, str], amount: int = None, _spender: str = None) -> TransactionReceipt:
97122
"""
98-
Approves the given token for deposit into an Automated Vault.
123+
Approves the given token for usage by the Automated Vault.
99124
100125
:param token: Optional - either the BEP20Token object, or the token address (str)
101126
:param amount: The amount of token to approve, default = maximum
127+
:param _spender: (Should not be changed by the caller) The address to give token spending access to
102128
"""
103129
if type(token) != BEP20Token:
104130
token = BEP20Token(token)
105131

106-
func_call = token.prepare_approve(self.address, amount)
132+
if amount is None:
133+
# Use maximum approval amount if no amount is specified
134+
amount = 2 ** 256 - 1
135+
136+
if _spender is None:
137+
_spender = self.address
138+
139+
func_call = token.prepare_approve(checksum(_spender), amount)
140+
107141
return self._execute(func_call)
108142

109143

alpaca_finance/util.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from web3 import Web3
99

1010

11+
checksum = lambda addr: Web3.toChecksumAddress(addr)
12+
13+
1114
def get_web3_provider(network_rpc_url: str) -> Web3:
1215
"""Returns a Web3 connection provider object"""
1316
return Web3(Web3.HTTPProvider(network_rpc_url))

examples/automated_vault/approve_deposit_token.ipynb renamed to examples/automated_vault/approve_token.ipynb

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"cells": [
33
{
44
"cell_type": "code",
5-
"execution_count": 1,
5+
"execution_count": 2,
66
"metadata": {},
77
"outputs": [],
88
"source": [
@@ -17,7 +17,7 @@
1717
},
1818
{
1919
"cell_type": "code",
20-
"execution_count": 2,
20+
"execution_count": 3,
2121
"metadata": {},
2222
"outputs": [],
2323
"source": [
@@ -29,7 +29,7 @@
2929
},
3030
{
3131
"cell_type": "code",
32-
"execution_count": 3,
32+
"execution_count": 4,
3333
"metadata": {},
3434
"outputs": [],
3535
"source": [
@@ -51,7 +51,7 @@
5151
{
5252
"data": {
5353
"text/plain": [
54-
"('Long 3x BUSD-BTCB PCS2', 'l3x-busdbtcb-pcs2', 'BUSD')"
54+
"('Long 3x BUSD-BTCB PCS2', 'l3x-busdbtcb-pcs2', 'BUSD', 'L3x-BUSDBTCB-PCS2')"
5555
]
5656
},
5757
"execution_count": 5,
@@ -63,7 +63,7 @@
6363
"# Create a automated vault position instance:\n",
6464
"position = AutomatedVaultPosition(position_key=POSITION_KEY, owner_wallet_address=PUBKEY, owner_wallet_key=PRIVKEY)\n",
6565
"\n",
66-
"position.name, position.key, position.asset_token.symbol()"
66+
"position.name, position.key, position.asset_token.symbol(), position.bep20_vault_token.symbol()"
6767
]
6868
},
6969
{
@@ -90,7 +90,7 @@
9090
}
9191
],
9292
"source": [
93-
"txn_recept = position.do_approve_deposit_token(position.asset_token)\n",
93+
"txn_recept = position.do_approve_token(position.asset_token)\n",
9494
"txn_recept"
9595
]
9696
},
@@ -113,6 +113,56 @@
113113
"source": [
114114
"f\"Gas Spend USD: ${txn_recept.gasSpendUSD}\""
115115
]
116+
},
117+
{
118+
"cell_type": "markdown",
119+
"metadata": {},
120+
"source": [
121+
"## Approve vault token (L3x-BUSDBTCB-PCS2) for withdraw from the vault\n",
122+
"\n",
123+
"> NOTE: This is only needed if the **\"Convert All\"** strategy is used."
124+
]
125+
},
126+
{
127+
"cell_type": "code",
128+
"execution_count": 6,
129+
"metadata": {},
130+
"outputs": [
131+
{
132+
"data": {
133+
"text/plain": [
134+
"TransactionReceipt(transactionHash=HexBytes('0x48f5dd6535f5a2c92ade75494ee1215e037bc8bef91fa98025390d1f865130ce'), blockHash=HexBytes('0xb909a29e08c35350dd6975fc814cef96408afcb79199178d615e25cf4793b982'), blockNumber=19902313, contractAddress=None, cumulativeGasUsed=4586517, gasSpendUSD=0.04049917075722871, fromAddress='0xC9E6e248928aC18eD6b103653cBcF66d23B743C6', toAddress='0xA1679223b7585725aFb425a6F59737a05e085C40', status=1, transactionIndex=48, type='0x0', effectiveGasPrice=None)"
135+
]
136+
},
137+
"execution_count": 6,
138+
"metadata": {},
139+
"output_type": "execute_result"
140+
}
141+
],
142+
"source": [
143+
"txn_receipt = position.do_approve_token(position.bep20_vault_token, _spender=position.gateway.address)\n",
144+
"txn_receipt"
145+
]
146+
},
147+
{
148+
"cell_type": "code",
149+
"execution_count": 7,
150+
"metadata": {},
151+
"outputs": [
152+
{
153+
"data": {
154+
"text/plain": [
155+
"'Gas Spend USD: $0.04049917075722871'"
156+
]
157+
},
158+
"execution_count": 7,
159+
"metadata": {},
160+
"output_type": "execute_result"
161+
}
162+
],
163+
"source": [
164+
"f\"Gas Spend USD: ${txn_receipt.gasSpendUSD}\""
165+
]
116166
}
117167
],
118168
"metadata": {

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
attrdict
22
web3
33
requests
4-
bep20>=1.0.1
4+
bep20>=1.0.2
55
eth_abi
66
hexbytes
77
python-dotenv

0 commit comments

Comments
 (0)