Skip to content

Commit c217ebd

Browse files
authored
Umami vaults use non-standard deposit function (#456)
- Create a test and figure out why it is not working
1 parent 21693e8 commit c217ebd

File tree

8 files changed

+345
-6
lines changed

8 files changed

+345
-6
lines changed

contracts/guard/src/GuardV0Base.sol

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,8 @@ abstract contract GuardV0Base is IGuard, Multicall, SwapCowSwap {
665665
* - Callsites for deposits and redemptions
666666
* - Vault share and denomination tokens
667667
* - Any ERC-4626 extensions are not supported by this function, like special share tokens
668-
* - ERC-4626 withdrawal address must be always
668+
* - ERC-4626 withdrawal address must be always the Safe
669+
* - Because of non-standardisation the whitelisted function list is long
669670
*/
670671
function whitelistERC4626(address vault, string calldata notes) external {
671672
IERC4626 vault_ = IERC4626(vault);
@@ -677,6 +678,12 @@ abstract contract GuardV0Base is IGuard, Multicall, SwapCowSwap {
677678
allowCallSite(vault, getSelector("withdraw(uint256,address,address)"), notes);
678679
allowCallSite(vault, getSelector("redeem(uint256,address,address)"), notes);
679680

681+
// Umami non-standard ERC-4626
682+
// See UmamiDepositManager()
683+
// https://arbiscan.io/address/0x959f3807f0aa7921e18c78b00b2819ba91e52fef#code
684+
allowCallSite(vault, getSelector("deposit(uint256,uint256,address)"), notes);
685+
allowCallSite(vault, getSelector("redeem(uint256,uint256,address,address)"), notes);
686+
680687
// ERC-7540
681688
// See ERC7540DepositManager
682689
allowCallSite(vault, getSelector("deposit(uint256,address,address)"), notes);

eth_defi/abi/guard/GuardV0.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

eth_defi/abi/guard/SimpleVaultV0.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

eth_defi/abi/safe-integration/TradingStrategyModuleV0.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

eth_defi/provider/anvil.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class RPCRequestError(Exception):
7777
"block_time": "--block-time",
7878
"steps_tracing": "--steps-tracing",
7979
"code_size_limit": "--code-size-limit",
80+
"verbose": "-vvvvv",
8081
}
8182

8283

@@ -213,6 +214,7 @@ def launch_anvil(
213214
log_wait=False,
214215
code_size_limit: int = None,
215216
rpc_smoke_test=True,
217+
verbose=False,
216218
) -> AnvilLaunch:
217219
"""Creates Anvil unit test backend or mainnet fork.
218220
@@ -390,6 +392,10 @@ def test_anvil_fork_transfer_busd(web3: Web3, large_busd_holder: HexAddress, use
390392
:parma log_wait:
391393
Display info level logging while waiting for Anvil to start.
392394
395+
:param verbose:
396+
Make Anvil the proces to dump a lot of stuff to stdout/stderr.
397+
398+
See -vvvv https://getfoundry.sh/anvil/reference/anvil
393399
"""
394400

395401
attempts_left = attempts
@@ -437,6 +443,7 @@ def test_anvil_fork_transfer_busd(web3: Web3, large_busd_holder: HexAddress, use
437443
hardfork=hardfork,
438444
gas_limit=gas_limit,
439445
steps_tracing=steps_tracing,
446+
verbose=verbose,
440447
)
441448

442449
if code_size_limit:

eth_defi/umami/vault.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
"""Umami gmUSDC vault support."""
22

33
import datetime
4+
from decimal import Decimal
45
from functools import cached_property
56
import logging
67

78
from web3.contract import Contract
9+
810
from eth_typing import BlockIdentifier
11+
from eth_typing import HexAddress
912

1013
from eth_defi.erc_4626.core import get_deployed_erc_4626_contract
14+
from eth_defi.erc_4626.deposit_redeem import ERC4626DepositManager, ERC4626DepositRequest
15+
from eth_defi.erc_4626.flow import deposit_4626
1116
from eth_defi.erc_4626.vault import ERC4626Vault
1217
from eth_defi.vault.base import VaultTechnicalRisk
1318

@@ -21,6 +26,8 @@ class UmamiVault(ERC4626Vault):
2126
2227
Umami vaults do not have open source Github repository, developer documentation or easy developer access for integrations,
2328
making it not recommended to deal with them.
29+
30+
- Vault smart contract code: https://arbiscan.io/address/0x959f3807f0aa7921e18c78b00b2819ba91e52fef#code
2431
"""
2532

2633
def get_risk(self) -> VaultTechnicalRisk | None:
@@ -63,3 +70,143 @@ def get_performance_fee(self, block_identifier: BlockIdentifier) -> float | None
6370

6471
def get_estimated_lock_up(self) -> datetime.timedelta:
6572
return datetime.timedelta(days=3)
73+
74+
def get_deposit_manager(self) -> "eth_defi.umami.vault.UmamiDepositManager":
75+
return UmamiDepositManager(self)
76+
77+
78+
class UmamiDepositManager(ERC4626DepositManager):
79+
"""Umami deposit manager with custom logic."""
80+
81+
def create_deposit_request(
82+
self,
83+
owner: HexAddress,
84+
to: HexAddress = None,
85+
amount: Decimal = None,
86+
raw_amount: int = None,
87+
check_max_deposit=True,
88+
check_enough_token=True,
89+
max_slippage=0.01,
90+
gas=30_000_000,
91+
) -> ERC4626DepositRequest:
92+
"""Umami has a slippage tolerance on deposits.
93+
94+
- Umami has a 0.15% deposit fee taken from the shares minted.
95+
- Umami deposit is gas hungry
96+
- Umami deposit must have ETH attached to the transaction as something spends it there
97+
98+
.. code-block:: solidity
99+
100+
// DEPOSIT & WITHDRAW
101+
// ------------------------------------------------------------------------------------------
102+
103+
/**
104+
* @notice Deposit a specified amount of assets and mint corresponding shares to the receiver
105+
* @param assets The amount of assets to deposit
106+
* @param minOutAfterFees Minimum amount out after fees
107+
* @param receiver The address to receive the minted shares
108+
* @return shares The estimate amount of shares minted for the deposited assets
109+
*/
110+
function deposit(uint256 assets, uint256 minOutAfterFees, address receiver)
111+
public
112+
payable
113+
override
114+
whenDepositNotPaused
115+
nonReentrant
116+
returns (uint256 shares)
117+
{
118+
// Check for rounding error since we round down in previewDeposit.
119+
require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES");
120+
require(
121+
totalAssets() + assets <= previewVaultCap() + asset.balanceOf(address(this)), "AssetVault: over vault cap"
122+
);
123+
// Transfer assets to aggregate vault, transfer before minting or ERC777s could reenter.
124+
asset.safeTransferFrom(msg.sender, address(this), assets);
125+
aggregateVault.handleDeposit{ value: msg.value }(assets, minOutAfterFees, receiver, msg.sender, address(0));
126+
127+
emit Deposit(msg.sender, receiver, assets, shares);
128+
}
129+
130+
ETH spend:
131+
132+
.. code-block:: solidity
133+
134+
/**
135+
* @notice Handles a deposit of a specified amount of an ERC20 asset into the AggregateVault from an account, with a deposit fee deducted.
136+
* @param assets The amount of the asset to be deposited.
137+
* @param account The address of the account from which the deposit will be made.
138+
*/
139+
function handleDeposit(uint256 assets, uint256 minOutAfterFees, address account, address sender, address callback)
140+
external
141+
payable
142+
onlyAssetVault
143+
{
144+
if (assets == 0) revert AmountEqualsZero();
145+
if (account == address(0)) revert ZeroAddress();
146+
AVStorage storage stg = _getStorage();
147+
uint256 gas = _gasRequirement(callback != address(0));
148+
if (msg.value < gas * tx.gasprice) revert MinGasRequirement();
149+
150+
// store request data
151+
uint256 key = _saveRequest(sender, account, msg.sender, callback, true, assets, minOutAfterFees);
152+
153+
// send execution gas cost
154+
TransferUtils.transferNativeAsset(stg.rebalanceKeeper, msg.value);
155+
156+
_executeHook(HookType.DEPOSIT_HOOK, msg.data[4:]);
157+
158+
// emit request event
159+
Emitter(stg.emitter).emitDepositRequest(key, account, msg.sender);
160+
}
161+
"""
162+
163+
if not raw_amount:
164+
raw_amount = self.vault.denomination_token.convert_to_raw(amount)
165+
166+
vault = self.vault
167+
from_ = owner
168+
receiver = owner
169+
170+
logger.info(
171+
"Depositing to vault %s, amount %s, raw amount %s, from %s",
172+
vault.address,
173+
amount,
174+
raw_amount,
175+
from_,
176+
)
177+
178+
preview_amount = vault.vault_contract.functions.previewDeposit(raw_amount).call()
179+
estimated_shares = vault.share_token.convert_to_decimals(preview_amount)
180+
181+
min_shares = int(preview_amount * (1 - max_slippage))
182+
183+
logger.info("Estimated %s shares before slippage: %s, slippage set to %s, min amount out %s", vault.share_token.symbol, estimated_shares, max_slippage, min_shares)
184+
185+
contract = vault.vault_contract
186+
187+
if not raw_amount:
188+
assert isinstance(amount, Decimal)
189+
assert amount > 0
190+
raw_amount = vault.denomination_token.convert_to_raw(amount)
191+
192+
if check_enough_token:
193+
actual_balance_raw = vault.denomination_token.fetch_raw_balance_of(from_)
194+
assert actual_balance_raw >= raw_amount, f"Not enough token in {from_} to deposit {amount} to {vault.address}, has {actual_balance_raw}, tries to deposit {raw_amount}"
195+
196+
if check_max_deposit:
197+
max_deposit = contract.functions.maxDeposit(receiver).call()
198+
if max_deposit != 0:
199+
assert raw_amount <= max_deposit, f"Max deposit {max_deposit} is less than {raw_amount}"
200+
201+
call = contract.functions.deposit(raw_amount, min_shares, receiver)
202+
203+
return ERC4626DepositRequest(
204+
vault=self.vault,
205+
owner=owner,
206+
to=owner,
207+
funcs=[call],
208+
amount=amount,
209+
raw_amount=raw_amount,
210+
gas=gas,
211+
value=Decimal(0.1),
212+
)

eth_defi/vault/deposit_redeem.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from abc import ABC, abstractmethod
55
from dataclasses import dataclass
66
from decimal import Decimal
7+
import logging
8+
from pprint import pformat
79

810
from web3 import Web3
911
from web3.contract.contract import ContractFunction
@@ -15,6 +17,9 @@
1517
from eth_defi.trace import assert_transaction_success_with_explanation
1618

1719

20+
logger = logging.getLogger(__name__)
21+
22+
1823
class VaultTransactionFailed(Exception):
1924
"""One of vault deposit/redeem transactions reverted"""
2025

@@ -216,6 +221,12 @@ class DepositRequest:
216221
#: It's a list because for Gains we need 2 tx
217222
funcs: list[ContractFunction]
218223

224+
#: Set transaction gas limit
225+
gas: int | None = None
226+
227+
#: Attached ETH value to the tx
228+
value: Decimal | None = None
229+
219230
def __post_init__(self):
220231
from eth_defi.vault.base import VaultBase
221232

@@ -261,7 +272,7 @@ def parse_deposit_transaction(
261272

262273
return DepositTicket(vault_address=self.vault.address, owner=self.owner, to=self.to, raw_amount=self.raw_amount, tx_hash=tx_hash, gas_used=gas_used, block_timestamp=block_timestamp, block_number=block_number)
263274

264-
def broadcast(self, from_: HexAddress = None, gas: int = 1_000_000) -> RedemptionTicket:
275+
def broadcast(self, from_: HexAddress = None, gas: int | None = None, check_value=True) -> RedemptionTicket:
265276
"""Broadcast all the transactions in this request.
266277
267278
:param from_:
@@ -280,11 +291,37 @@ def broadcast(self, from_: HexAddress = None, gas: int = 1_000_000) -> Redemptio
280291
if from_ is None:
281292
from_ = self.owner
282293

294+
if gas is None:
295+
if self.gas:
296+
gas = self.gas
297+
else:
298+
# Default to 1M
299+
gas = 1_000_000
300+
301+
tx_data = {"from": from_, "gas": gas}
302+
if self.value:
303+
tx_data["value"] = Web3.to_wei(self.value, "ether")
304+
305+
# If we ask for value, make sure our account is topped up
306+
if check_value:
307+
balance = self.web3.eth.get_balance(from_)
308+
assert balance >= tx_data["value"], f"Not enough ETH balance in {from_} to cover value {self.value} ETH, has {Web3.from_wei(balance, 'ether')} ETH"
309+
310+
logger.info(
311+
"Broadcasting deposit request to vault %s from %s with gas %s and tx params:\n%s",
312+
self.vault.address,
313+
from_,
314+
gas,
315+
pformat(tx_data),
316+
)
317+
283318
tx_hashes = []
284319
for func in self.funcs:
285-
tx_hash = func.transact({"from": from_, "gas": gas})
320+
tx_hash = func.transact(tx_data)
321+
286322
assert_transaction_success_with_explanation(self.web3, tx_hash)
287323
tx_hashes.append(tx_hash)
324+
288325
return self.parse_deposit_transaction(tx_hashes)
289326

290327

0 commit comments

Comments
 (0)