-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[New Operator] - Beefy Single Asset Vault #107
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well done 👏 Let's discuss the relationship vault <-> operator further.
It's on the right track 🎉
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
|
||
/// @title Beefy Single Vault Operator | ||
/// @notice Deposit/Withdraw in a Beefy vault (native or non-native). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might require a bit of work, but I suggest we look into supporting multiple vaults per operator.
1<->1 relationships between operators and vaults seem pretty cumbersome to maintain and operate.
Let's discuss pros and cons
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main argument is to avoid the operator storage, with token
and vault
immutable
(save gas), and also having the same process for adding/removing operations.
But it's easier to handle one operator with multiple vaults. So I think that both are fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While operating the protocol, it is probably easier to manage one operator + one storage contract, rather than 10s of operators. This is true for the admin deploying contract but it also makes it easier to plug new vaults to the frontend, and route the users between the vaults
I understand it will be slightly more costly for users though. But relatively to what Beefy requires as gas, I'm not sure it would make a significant difference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok let's do this!
.env.example
Outdated
FORKING="false" | ||
FORK_CHAINID="" | ||
FORK_URL="" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those env variables don't seem secret to me. Consider adding them to the example file to make life easier for other devs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
|
||
describe("ExchangeHelpers", () => { | ||
describeWithoutFork("ExchangeHelpers", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💅 I think you could have simply created the new describeWithFork
and used the regular describe
otherwise, so that you didn't have to change all existing describe clauses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No because describeWithoutFork
is used to exclude forking tests. Using describe
means that both tests are executed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, I don't want to execute NestedFactory
tests and the DummyRouter
with a fork. It's useless, I want to skip these tests while forking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, default is both fork and no fork and you specify for no fork, I was suggesting the other way around. But it does not matter much 👍
/// - [1] : The token deposited address | ||
function deposit(uint256 amount, uint256 minVaultAmount) | ||
external | ||
payable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❔ Should this function be payable
? The asset handled seem to always be of ERC20 type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
create
and processInputOrders
are payable
. Because deposit
is called via delegatecall
it needs to be payable
:)
uint256 vaultBalanceBefore = vault.balanceOf(address(this)); | ||
uint256 tokenBalanceBefore = token.balanceOf(address(this)); | ||
|
||
ExchangeHelpers.setMaxAllowance(token, address(vault)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I understand, all tokens in this contract will be in "transit" and never stored permanently.
I'm starting to think that we should remove setMaxAllowance
uses. But here it would be fine anyway if they're not stored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Beefy needs to transferFrom
the underlying token from NestedFactory
, so we need to approve the vault
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes obviously, my comment is about maxAllowance vs increaseAllowance
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh okay!
I think that maxAllowance is better because it's handling the tokens race condition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it?
To me, it's great for gas opt, but I don't think race conditions are a consideration here.
Systematically, allowance is increased by an amount before the amount is transferred, and there are no concurrent transactions as transactions are isolated and executed in order
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there can be race conditions here I'm curious to know more about it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, we are not trying to handle race conditions, but tokens that are handling race conditions. So, only using "increaseAllowance" will fail with some tokens.
/// @notice Deposit the asset in the Beefy vault and receive | ||
/// the vault token (moo). | ||
/// @param amount The token amount to deposit | ||
/// @param minVaultAmount The minimum vault token amount expected |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❔ I need more context as to why this variable is useful. I believe it acts as an equivalent to slippage control, and ensures good behavior. I'll look more in-depth in the docs to see how the vault tokens are emitted and why this should be passed as a parameter.
Please mention any other reason this variable is useful in the meantime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a matter of principle, I think that is better to specify the minimum amount that we want to receive.
It's not critical with a vault, but it's good to guarantee that a user will have this amount of "share".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does the number come from? Passed from the frontend?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I guess this is read from a method of the vault contract
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we need to "estimate" the amount before (backend). But it can be zero.
const tx = await context.nestedFactory | ||
.connect(context.user1) | ||
.create(0, [{ inputToken: ETH, amount: bnbToDepositAndFees, orders, fromReserve: false }], { | ||
value: bnbToDepositAndFees, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔧 I would like to also see a test case for deposits post-creation using the new process orders method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@adrien-supizet Changes are done ✅ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me 👏
Minor comments to look at.
returns (uint256[] memory amounts, address[] memory tokens) | ||
{ | ||
function deposit( | ||
IERC20 vault, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💅 vault
should be of type address here:
- it matches the natspec description
- it is easier to understand
- you don't have to cast it to
address
L42 - For the sake of consistency, it is passed as addresses in the constructor already
/// @param amount The vault token amount to withdraw | ||
/// @return amounts Array of amounts : | ||
/// - [0] : The token received amount | ||
/// - [1] : The vault token deposited amount | ||
/// @return tokens Array of token addresses | ||
/// - [0] : The token received address | ||
/// - [1] : The vault token deposited address | ||
function withdraw(uint256 amount) external returns (uint256[] memory amounts, address[] memory tokens) { | ||
function withdraw(IERC20 vault, uint256 amount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💅 Same comment about vault type
function addVault(address vault, address token) external onlyOwner { | ||
require(vault != address(0), "BVS: INVALID_VAULT_ADDRESS"); | ||
require(token != address(0), "BVS: INVALID_TOKEN_ADDRESS"); | ||
vaults[vault] = token; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❔ This method allows to override the current token address for a given vault.
We could add a require to check that the vault does not exist yet, enforcing the owner to delete a vault and re-add it if the token address changes.
In most cases, it can prevent overriding by mistake.
07f3ced
to
f68d6d8
Compare
fix: remove cast from parameter (beefy operator)
New operator to deposit/withdraw in Beefy => https://app.beefy.finance/#/bsc
On operator deployed is for one vault, and can be deployed on every chains where Beefy is available.
How the operator works
=> Use vault underlying asset (input).
=> Deposit this token in Beefy.
=> Receive the "moo" token, the share of the vault (output).
=> Use moo token (input).
=> Deposit the moo token by calling
withdraw
(will burn the moo token).=> Receive the underlying asset (output).
Test changes
Now, we can specify the describe that we want for our tests :
describeOnBscFork
ordescribeWithoutFork
. Because we don't want to run all tests on a BSC fork.There are also new
.env
variables :Note: I've been using Chainstack to run a full BSC node (the request limit is very good). The standard RPC endpoint was not working well for forking.