Skip to content
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

Merged
merged 10 commits into from
Mar 14, 2022
Merged

Conversation

maximebrugel
Copy link
Contributor

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

  • Deposit:
    => Use vault underlying asset (input).
    => Deposit this token in Beefy.
    => Receive the "moo" token, the share of the vault (output).
  • Withdraw:
    => 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 or describeWithoutFork. Because we don't want to run all tests on a BSC fork.

There are also new .env variables :

FORKING="false"
FORK_CHAINID=""
FORK_URL="" 

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.

@maximebrugel maximebrugel added the To review Let people know this PR is ready for a review label Mar 6, 2022
Copy link
Contributor

@adrien-supizet adrien-supizet left a 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).
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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
Comment on lines 10 to 12
FORKING="false"
FORK_CHAINID=""
FORK_URL=""
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


describe("ExchangeHelpers", () => {
describeWithoutFork("ExchangeHelpers", () => {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

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));
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@adrien-supizet adrien-supizet Mar 7, 2022

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

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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".

Copy link
Contributor

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?

Copy link
Contributor

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

Copy link
Contributor Author

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.

Comment on lines +76 to +125
const tx = await context.nestedFactory
.connect(context.user1)
.create(0, [{ inputToken: ETH, amount: bnbToDepositAndFees, orders, fromReserve: false }], {
value: bnbToDepositAndFees,
});
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@adrien-supizet adrien-supizet removed the To review Let people know this PR is ready for a review label Mar 8, 2022
@maximebrugel
Copy link
Contributor Author

@adrien-supizet Changes are done ✅

Copy link
Contributor

@adrien-supizet adrien-supizet left a 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,
Copy link
Contributor

@adrien-supizet adrien-supizet Mar 14, 2022

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:

  1. it matches the natspec description
  2. it is easier to understand
  3. you don't have to cast it to address L42
  4. 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)
Copy link
Contributor

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;
Copy link
Contributor

@adrien-supizet adrien-supizet Mar 14, 2022

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.

@adrien-supizet adrien-supizet added the Fix and merge Minor changes required label Mar 14, 2022
fix: remove cast from parameter (beefy operator)
@maximebrugel maximebrugel merged commit f9ec12e into master Mar 14, 2022
@maximebrugel maximebrugel deleted the beefy-operator branch March 14, 2022 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix and merge Minor changes required
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants