Skip to content

Commit

Permalink
Added a new asset proxy to support yearn vault 4.2 (#207)
Browse files Browse the repository at this point in the history
* contract update for new yearn vault'

* linting'

* refactor to reduce code duplication

* lint

* Update contracts/YVaultV4AssetProxy.sol

Co-authored-by: Daejun Park <daejunpark@gmail.com>

* basic tests

* Update contracts/YVaultAssetProxy.sol

Co-authored-by: Jonny Rhea <5555162+jrhea@users.noreply.github.com>

* natspec returns

Co-authored-by: Daejun Park <daejunpark@gmail.com>
Co-authored-by: Jonny Rhea <5555162+jrhea@users.noreply.github.com>
  • Loading branch information
3 people authored Jul 6, 2021
1 parent cb909af commit 42f7d4d
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 15 deletions.
73 changes: 64 additions & 9 deletions contracts/YVaultAssetProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
pragma solidity ^0.8.0;

import "./interfaces/IERC20.sol";
import "./interfaces/IYearnVaultV2.sol";
import "./interfaces/IYearnVault.sol";
import "./WrappedPosition.sol";

/// @author Element Finance
Expand Down Expand Up @@ -45,6 +45,38 @@ contract YVaultAssetProxy is WrappedPosition {
uint8(_token.decimals()) == localVaultDecimals,
"Inconsistent decimals"
);
// We check that this is a compatible yearn version
_versionCheck(IYearnVault(vault_));
}

/// @notice An override-able version checking function, reverts if the vault has the wrong version
/// @param _vault The yearn vault address
/// @dev This function can be overridden by an inheriting upgrade contract
function _versionCheck(IYearnVault _vault) internal virtual view {
string memory apiVersion = _vault.apiVersion();
require(
_stringEq(apiVersion, "0.3.0") ||
_stringEq(apiVersion, "0.3.1") ||
_stringEq(apiVersion, "0.3.2") ||
_stringEq(apiVersion, "0.3.3") ||
_stringEq(apiVersion, "0.3.4") ||
_stringEq(apiVersion, "0.3.5"),
"Unsupported Version"
);
}

/// @notice checks if two strings are equal
/// @param s1 string one
/// @param s2 string two
/// @return bool whether they are equal
function _stringEq(string memory s1, string memory s2)
internal
pure
returns (bool)
{
bytes32 h1 = keccak256(abi.encodePacked(s1));
bytes32 h2 = keccak256(abi.encodePacked(s2));
return (h1 == h2);
}

/// @notice This function allows a user to deposit to the reserve
Expand All @@ -60,9 +92,7 @@ contract YVaultAssetProxy is WrappedPosition {
(uint256 localUnderlying, uint256 localShares) = _getReserves();
// Calculate the total reserve value
uint256 totalValue = localUnderlying;
uint256 yearnTotalSupply = vault.totalSupply();
uint256 yearnTotalAssets = vault.totalAssets();
totalValue += ((yearnTotalAssets * localShares) / yearnTotalSupply);
totalValue += _yearnDepositConverter(localShares, true);
// If this is the first deposit we need different logic
uint256 localReserveSupply = reserveSupply;
uint256 mintAmount;
Expand Down Expand Up @@ -129,11 +159,7 @@ contract YVaultAssetProxy is WrappedPosition {
amount -= 1;
}
// Calculate the amount of shares the amount deposited is worth
// Note - to get a realistic reading and avoid rounding errors we
// use the method of the yearn vault instead of '_pricePerShare'
uint256 yearnTotalSupply = vault.totalSupply();
uint256 yearnTotalAssets = vault.totalAssets();
uint256 neededShares = (amount * yearnTotalSupply) / yearnTotalAssets;
uint256 neededShares = _yearnDepositConverter(amount, false);

// If we have enough in local reserves we don't call out for deposits
if (localShares > neededShares) {
Expand Down Expand Up @@ -242,4 +268,33 @@ contract YVaultAssetProxy is WrappedPosition {
reserveUnderlying = uint128(_newReserveUnderlying);
reserveShares = uint128(_newReserveShares);
}

/// @notice Converts an input of shares to it's output of underlying or an input
/// of underlying to an output of shares, using yearn 's deposit pricing
/// @param amount the amount of input, shares if 'sharesIn == true' underlying if not
/// @param sharesIn true to convert from yearn shares to underlying, false to convert from
/// underlying to yearn shares
/// @dev WARNING - In yearn 0.3.1 - 0.3.5 this is an exact match for deposit logic
/// but not withdraw logic in versions 0.3.2-0.3.5. In versions 0.4.0+
/// it is not a match for yearn deposit ratios.
/// @return The converted output of either underlying or yearn shares
function _yearnDepositConverter(uint256 amount, bool sharesIn)
internal
virtual
view
returns (uint256)
{
// Load the yearn total supply and assets
uint256 yearnTotalSupply = vault.totalSupply();
uint256 yearnTotalAssets = vault.totalAssets();
// If we are converted shares to underlying
if (sharesIn) {
// then we get the fraction of yearn shares this is and multiply by assets
return (yearnTotalAssets * amount) / yearnTotalSupply;
} else {
// otherwise we figure out the faction of yearn assets this is and see how
// many assets we get out.
return (yearnTotalSupply * amount) / yearnTotalAssets;
}
}
}
58 changes: 58 additions & 0 deletions contracts/YVaultV4AssetProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
// WARNING: This has been validated for yearn vaults version 4.2, do not use for lower or higher
// versions without review
pragma solidity ^0.8.0;

import "./interfaces/IERC20.sol";
import "./interfaces/IYearnVault.sol";
import "./YVaultAssetProxy.sol";

/// @author Element Finance
/// @title Yearn Vault Asset Proxy
contract YVaultV4AssetProxy is YVaultAssetProxy {
/// @notice Constructs this contract by calling into the super constructor
/// @param vault_ The yearn v2 vault, must be version 0.4.2
/// @param _token The underlying token.
/// This token should revert in the event of a transfer failure.
/// @param _name The name of the token created
/// @param _symbol The symbol of the token created
constructor(
address vault_,
IERC20 _token,
string memory _name,
string memory _symbol
) YVaultAssetProxy(vault_, _token, _name, _symbol) {}

/// @notice Overrides the version checking to check for 0.4.2 instead
/// @param _vault The yearn vault address
/// @dev This function can be overridden by an inheriting upgrade contract
function _versionCheck(IYearnVault _vault) internal override view {
string memory apiVersion = _vault.apiVersion();
require(_stringEq(apiVersion, "0.4.2"), "Unsupported Version");
}

/// @notice Converts an input of shares to it's output of underlying or an input
/// of underlying to an output of shares, using yearn 's deposit pricing
/// @param amount the amount of input, shares if 'sharesIn == true' underlying if not
/// @param sharesIn true to convert from yearn shares to underlying, false to convert from
/// underlying to yearn shares
/// @dev WARNING - Fails for 0.3.2-0.3.5, please only use with 0.4.2
/// @return The converted output of either underlying or yearn shares
function _yearnDepositConverter(uint256 amount, bool sharesIn)
internal
override
view
returns (uint256)
{
// Load the yearn price per share
uint256 pricePerShare = vault.pricePerShare();
// If we are converted shares to underlying
if (sharesIn) {
// If the input is shares we multiply by the price per share
return (pricePerShare * amount) / 10**vaultDecimals;
} else {
// If the input is in underlying we divide by price per share
return (amount * 10**vaultDecimals) / (pricePerShare + 1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ interface IYearnVault is IERC20 {
function totalSupply() external view returns (uint256);

function totalAssets() external view returns (uint256);

function apiVersion() external view returns (string memory);
}
6 changes: 5 additions & 1 deletion contracts/test/MockERC20YearnVault.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pragma solidity ^0.8.0;
import "../interfaces/IYearnVaultV2.sol";
import "../interfaces/IYearnVault.sol";
import "../libraries/Authorizable.sol";
import "../libraries/ERC20Permit.sol";

Expand Down Expand Up @@ -38,6 +38,10 @@ contract MockERC20YearnVault is IYearnVault, Authorizable, ERC20Permit {
lockedProfitDegradation = (DEGRADATION_COEFFICIENT * 46) / 1e6;
}

function apiVersion() external override pure returns (string memory) {
return ("0.3.2");
}

/**
@notice Add tokens to the vault. Increases totalAssets.
@param _deposit The amount of tokens to deposit
Expand Down
13 changes: 11 additions & 2 deletions contracts/test/TestYVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.0;

import "../interfaces/IERC20.sol";
import "../interfaces/IYearnVaultV2.sol";
import "../interfaces/IYearnVault.sol";

import "../libraries/ERC20PermitWithSupply.sol";

Expand All @@ -23,12 +23,21 @@ contract TestYVault is ERC20PermitWithSupply {
external
returns (uint256)
{
uint256 _shares = (_amount * (10**decimals)) / pricePerShare(); // calculate shares
uint256 _shares;
if (totalSupply == 0) {
_shares = _amount;
} else {
_shares = (_amount * (10**decimals)) / pricePerShare(); // calculate shares
}
IERC20(token).transferFrom(msg.sender, address(this), _amount); // pull deposit from sender
_mint(destination, _shares); // mint shares for sender
return _shares;
}

function apiVersion() external virtual pure returns (string memory) {
return ("0.3.2");
}

function withdraw(
uint256 _shares,
address destination,
Expand Down
19 changes: 19 additions & 0 deletions contracts/test/TestYVaultV4.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import "./TestYVault.sol";

// NOTE - the test y vault uses the formula of 0.4.2 and versions prior to
// 0.3.2, so when we test the YAssetProxyV4 we actually want the same logic
// except that we want to return a diff version.
// The subtly of 0.3.2 - 0.3.5 yearn vaults is tested in the mainnet tests.

contract TestYVaultV4 is TestYVault {
constructor(address _token, uint8 _decimals)
TestYVault(_token, _decimals)
{}

function apiVersion() external override pure returns (string memory) {
return ("0.4.2");
}
}
2 changes: 1 addition & 1 deletion contracts/zaps/ZapTrancheHop.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import "../interfaces/IYearnVaultV2.sol";
import "../interfaces/IYearnVault.sol";
import "../libraries/Authorizable.sol";
import "../interfaces/IERC20.sol";
import "../interfaces/ITranche.sol";
Expand Down
2 changes: 1 addition & 1 deletion contracts/zaps/ZapYearnShares.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import "../interfaces/IYearnVaultV2.sol";
import "../interfaces/IYearnVault.sol";
import "../libraries/Authorizable.sol";
import "../interfaces/IERC20.sol";
import "../interfaces/ITranche.sol";
Expand Down
115 changes: 114 additions & 1 deletion test/wrappedPositionTest.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { expect } from "chai";
import { Signer } from "ethers";
import { BigNumber, Signer } from "ethers";
import { ethers, waffle } from "hardhat";

import { FixtureInterface, loadFixture } from "./helpers/deployer";
import { createSnapshot, restoreSnapshot } from "./helpers/snapshots";

import { TestYVaultV4 } from "typechain/TestYVaultV4";
import { YVaultV4AssetProxy } from "typechain/YVaultV4AssetProxy";

const { provider } = waffle;

describe("Wrapped Position", () => {
Expand Down Expand Up @@ -45,6 +48,29 @@ describe("Wrapped Position", () => {
// revert back to initial state after all tests pass
await restoreSnapshot(provider);
});
describe("Version locked deployments", () => {
it("Wont deploy YAssetProxy with a v4 yearn vault", async () => {
// Deploy a mock yearn v4
const mockYearnV4Factory = await ethers.getContractFactory(
"TestYVaultV4"
);
const yVaultV4 = await mockYearnV4Factory.deploy(
fixture.usdc.address,
await fixture.usdc.decimals()
);
// Attempt to deploy a non v4 wp on it
const yearnWPFactory = await ethers.getContractFactory(
"YVaultAssetProxy"
);
const tx = yearnWPFactory.deploy(
yVaultV4.address,
fixture.usdc.address,
"mock yearn",
"my"
);
await expect(tx).to.be.revertedWith("Unsupported Version");
});
});
describe("balanceOfUnderlying", () => {
beforeEach(async () => {
await createSnapshot(provider);
Expand Down Expand Up @@ -216,4 +242,91 @@ describe("Wrapped Position", () => {
expect(await fixture.position.reserveShares()).to.be.eq(7272728);
});
});
// We test explicity only the functionality changes in V4 not the whole
// contract
describe("Wrapped Yearn Vault v4 upgrade", async () => {
let vaultV4: TestYVaultV4;
let wpV4: YVaultV4AssetProxy;

before(async () => {
// Deploy a mock yearn v4
const mockYearnV4Factory = await ethers.getContractFactory(
"TestYVaultV4"
);
// TODO - Update so that the ethers factories play nice with types
vaultV4 = (await mockYearnV4Factory.deploy(
fixture.usdc.address,
await fixture.usdc.decimals()
)) as TestYVaultV4;
// Deploy a wrapped position
const wpFactory = await ethers.getContractFactory("YVaultV4AssetProxy");
wpV4 = (await wpFactory.deploy(
vaultV4.address,
fixture.usdc.address,
"mock yearn v4",
"test"
)) as YVaultV4AssetProxy;
// set approvals for accounts
await fixture.usdc.approve(wpV4.address, ethers.constants.MaxUint256);
await fixture.usdc
.connect(users[1].user)
.approve(wpV4.address, ethers.constants.MaxUint256);
// deposit into the yearn vault so there's a supply
await fixture.usdc.approve(vaultV4.address, ethers.constants.MaxUint256);
await vaultV4.deposit(1e6, users[0].address);
});

beforeEach(async () => {
await createSnapshot(provider);
});
afterEach(async () => {
// revert back to initial state after all tests pass
await restoreSnapshot(provider);
});

it("Won't deploy on a v3 wrapped position", async () => {
const wpFactory = await ethers.getContractFactory("YVaultAssetProxy");
const tx = wpFactory.deploy(
vaultV4.address,
fixture.usdc.address,
"mock yearn v4",
"test"
);
await expect(tx).to.be.revertedWith("Unsupported Version");
});

it("Reserve deposits", async () => {
// First deposit inits
await wpV4.reserveDeposit(1e6);
// Check that we got shares
let balance = await wpV4.reserveBalances(users[0].address);
expect(balance).to.be.eq(BigNumber.from(1e6));
// Do a deposit to convert to shares
await wpV4.connect(users[1].user).deposit(users[1].address, 1e6);
console.log(await fixture.usdc.balanceOf(vaultV4.address));
// We want to change deposit rate
await fixture.usdc.transfer(vaultV4.address, 5e5);
// Now we deposit and check the price of deposit
await wpV4.reserveDeposit(1e6);
// Check the new balance
balance = await wpV4.reserveBalances(users[0].address);
// Magic number from rounding error on deposits
expect(balance).to.be.eq(BigNumber.from(1857144));
});

it("withdraws with the correct ratio", async () => {
await wpV4.deposit(users[0].address, 1e6);
// Check that we got shares
const balance = await wpV4.balanceOf(users[0].address);
expect(balance).to.be.eq(BigNumber.from(1e6));
// We want to change withdraw rate
await fixture.usdc.transfer(vaultV4.address, 5e5);
// withdraw
const balanceBefore = await fixture.usdc.balanceOf(users[0].address);
await wpV4.withdraw(users[0].address, balance, 0);
const balanceAfter = await fixture.usdc.balanceOf(users[0].address);
// check the withdraw amount
expect(balanceAfter.sub(balanceBefore)).to.be.eq(125e4);
});
});
});

0 comments on commit 42f7d4d

Please sign in to comment.