Skip to content

Commit

Permalink
remove deposit and withdraw fee, loss paid by users, update slippage …
Browse files Browse the repository at this point in the history
…checks, fix tests
  • Loading branch information
a17 committed Jul 3, 2024
1 parent cb3d38e commit d5b9d6d
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 243 deletions.
3 changes: 1 addition & 2 deletions contracts/strategy/StrategyLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,8 @@ library StrategyLib {

uint withdrew = balance > balanceBefore ? balance - balanceBefore : 0;
uint withdrewUSD = withdrew * assetPrice / 1e18;
uint priceChangeTolerance = ITetuVaultV2(ISplitter(_splitter).vault()).withdrawFee();
uint difference = expectedWithdrewUSD > withdrewUSD ? expectedWithdrewUSD - withdrewUSD : 0;
require(difference * FEE_DENOMINATOR / expectedWithdrewUSD <= priceChangeTolerance, TOO_HIGH);
require(difference * 1e18 / expectedWithdrewUSD < 1e16, TOO_HIGH);
}
}

Expand Down
28 changes: 21 additions & 7 deletions contracts/vault/ERC4626Upgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ abstract contract ERC4626Upgradeable is ERC20PermitUpgradeable, ReentrancyGuard,
/// depositing, and withdrawing
IERC20 internal _asset;

uint internal _withdrawLoss;

/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint[50 - 2] private __gap;

function __ERC4626_init(
IERC20 asset_,
string memory _name,
Expand Down Expand Up @@ -114,6 +123,12 @@ abstract contract ERC4626Upgradeable is ERC20PermitUpgradeable, ReentrancyGuard,

_burn(owner, shares);

uint withdrawLoss = _withdrawLoss;
if (withdrawLoss != 0) {
assets -= withdrawLoss;
_withdrawLoss = 0;
}

emit Withdraw(msg.sender, receiver, owner, assets, shares);

_asset.safeTransfer(receiver, assets);
Expand Down Expand Up @@ -143,6 +158,12 @@ abstract contract ERC4626Upgradeable is ERC20PermitUpgradeable, ReentrancyGuard,

_burn(owner, shares);

uint withdrawLoss = _withdrawLoss;
if (withdrawLoss != 0) {
assets -= withdrawLoss;
_withdrawLoss = 0;
}

emit Withdraw(msg.sender, receiver, owner, assets, shares);

_asset.safeTransfer(receiver, assets);
Expand Down Expand Up @@ -216,11 +237,4 @@ abstract contract ERC4626Upgradeable is ERC20PermitUpgradeable, ReentrancyGuard,

/// @param receiver The receiver of the shares received after deposit
function afterDeposit(uint assets, uint shares, address receiver) internal virtual {}

/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint[49] private __gap;
}
83 changes: 23 additions & 60 deletions contracts/vault/TetuVaultV2.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.17;

import "../openzeppelin/Math.sol";
Expand All @@ -11,6 +10,7 @@ import "./ERC4626Upgradeable.sol";

/// @title Vault for storing underlying tokens and managing them with strategy splitter.
/// @author belbix
/// @author a17
contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {
using SafeERC20 for IERC20;
using Math for uint;
Expand All @@ -20,13 +20,14 @@ contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {
// *************************************************************

/// @dev Version of this contract. Adjust manually on each code modification.
string public constant VAULT_VERSION = "2.2.0";
string public constant VAULT_VERSION = "3.0.0";

/// @dev Denominator for buffer calculation. 100% of the buffer amount.
uint constant public BUFFER_DENOMINATOR = 100_000;
/// @dev Denominator for fee calculation.
uint constant public FEE_DENOMINATOR = 100_000;
/// @dev Max 1% fee.
uint constant public MAX_FEE = FEE_DENOMINATOR / 100;

uint constant internal SLIPPAGE_DENOMINATOR = 100_000; // 100%

uint constant internal MAX_WITHDRAW_SLIPPAGE = 500; // 0.5%

// *************************************************************
// VARIABLES
Expand All @@ -43,13 +44,13 @@ contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {
/// @dev Percent of assets that will always stay in this vault.
uint public buffer;

/// @dev Maximum amount for withdraw. Max UINT256 by default.
/// @dev Maximum amount for withdraw. Max uint by default.
uint public maxWithdrawAssets;
/// @dev Maximum amount for redeem. Max UINT256 by default.
/// @dev Maximum amount for redeem. Max uint by default.
uint public maxRedeemShares;
/// @dev Maximum amount for deposit. Max UINT256 by default.
/// @dev Maximum amount for deposit. Max uint by default.
uint public maxDepositAssets;
/// @dev Maximum amount for mint. Max UINT256 by default.
/// @dev Maximum amount for mint. Max uint by default.
uint public maxMintShares;
/// @dev Fee for deposit/mint actions. Zero by default.
uint public override depositFee;
Expand Down Expand Up @@ -83,9 +84,7 @@ contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {
event Invest(address splitter, uint amount);
event MaxWithdrawChanged(uint maxAssets, uint maxShares);
event MaxDepositChanged(uint maxAssets, uint maxShares);
event FeeChanged(uint depositFee, uint withdrawFee);
event DoHardWorkOnInvestChanged(bool oldValue, bool newValue);
event FeeTransfer(uint amount);
event LossCovered(uint amount, uint requestedAmount, uint balance);
event WithdrawRequested(address sender, uint startBlock);
event WithdrawRequestBlocks(uint blocks);
Expand Down Expand Up @@ -183,16 +182,6 @@ contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {
emit MaxWithdrawChanged(maxAssets, maxShares);
}

/// @dev Set deposit/withdraw fees
function setFees(uint _depositFee, uint _withdrawFee) external {
require(isGovernance(msg.sender), "DENIED");
require(_depositFee <= MAX_FEE && _withdrawFee <= MAX_FEE, "TOO_HIGH");

depositFee = _depositFee;
withdrawFee = _withdrawFee;
emit FeeChanged(_depositFee, _withdrawFee);
}

/// @dev If activated will call doHardWork on splitter on each invest action.
function setDoHardWorkOnInvest(bool value) external {
require(isGovernance(msg.sender), "DENIED");
Expand Down Expand Up @@ -229,7 +218,7 @@ contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {

/// @dev Price of 1 full share
function sharePrice() external view returns (uint) {
uint units = 10 ** uint256(decimals());
uint units = 10 ** uint(decimals());
uint totalSupply_ = totalSupply();
return totalSupply_ == 0
? units
Expand All @@ -246,18 +235,15 @@ contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {
// *************************************************************

function previewDeposit(uint assets) public view virtual override returns (uint) {
uint shares = convertToShares(assets);
return shares - (shares * depositFee / FEE_DENOMINATOR);
return convertToShares(assets);
}

function previewMint(uint shares) public view virtual override returns (uint) {
uint supply = totalSupply();
if (supply != 0) {
uint assets = shares.mulDiv(totalAssets(), supply, Math.Rounding.Up);
return assets * FEE_DENOMINATOR / (FEE_DENOMINATOR - depositFee);
} else {
return shares * FEE_DENOMINATOR / (FEE_DENOMINATOR - depositFee);
return shares.mulDiv(totalAssets(), supply, Math.Rounding.Up);
}
return shares;
}

/// @dev Calculate available to invest amount and send this amount to splitter
Expand All @@ -269,14 +255,7 @@ contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {

address _splitter = address(splitter);
IERC20 asset_ = _asset;
uint _depositFee = depositFee;
// send fee to insurance contract
if (_depositFee != 0) {
uint toFees = assets * _depositFee / FEE_DENOMINATOR;
asset_.safeTransfer(address(insurance), toFees);
emit FeeTransfer(toFees);
}
uint256 toInvest = _availableToInvest(_splitter, asset_);
uint toInvest = _availableToInvest(_splitter, asset_);
// invest only when buffer is filled
if (toInvest > 0) {

Expand Down Expand Up @@ -331,12 +310,10 @@ contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {
return assets;
}
uint shares = assets.mulDiv(supply, _totalAssets, Math.Rounding.Up);
shares = shares * FEE_DENOMINATOR / (FEE_DENOMINATOR - withdrawFee);
return supply == 0 ? assets : shares;
}

function previewRedeem(uint shares) public view virtual override returns (uint) {
shares = shares - (shares * withdrawFee / FEE_DENOMINATOR);
return convertToAssets(shares);
}

Expand All @@ -350,7 +327,6 @@ contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {

function maxWithdraw(address owner) public view override returns (uint) {
uint assets = convertToAssets(balanceOf(owner));
assets -= assets.mulDiv(withdrawFee, FEE_DENOMINATOR, Math.Rounding.Up);
return Math.min(maxWithdrawAssets, assets);
}

Expand All @@ -369,14 +345,7 @@ contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {
withdrawRequests[owner_] = block.number;
}

uint _withdrawFee = withdrawFee;
uint fromSplitter;
if (_withdrawFee != 0) {
// add fee amount
fromSplitter = assets * FEE_DENOMINATOR / (FEE_DENOMINATOR - _withdrawFee);
} else {
fromSplitter = assets;
}
uint fromSplitter = assets;

IERC20 asset_ = _asset;
uint balance = asset_.balanceOf(address(this));
Expand All @@ -392,19 +361,13 @@ contract TetuVaultV2 is ERC4626Upgradeable, ControllableV3, ITetuVaultV2 {
);
}
balance = asset_.balanceOf(address(this));
require(assets <= balance, "SLIPPAGE");

// send fee amount to insurance for keep correct calculations
// in case of compensation it will lead to double transfer
// but we assume that it will be rare case
if (_withdrawFee != 0) {
// we should compensate possible slippage from user fee too
uint toFees = Math.min(fromSplitter - assets, balance - assets);
if (toFees != 0) {
asset_.safeTransfer(address(insurance), toFees);
emit FeeTransfer(toFees);
}
uint slippage;
if (assets > balance) {
uint withdrawLoss = assets - balance;
_withdrawLoss = withdrawLoss;
slippage = SLIPPAGE_DENOMINATOR * withdrawLoss / assets;
}
require(slippage <= MAX_WITHDRAW_SLIPPAGE, "SLIPPAGE");
}

/// @dev Do necessary calculation for withdrawing from splitter and move assets to vault.
Expand Down
4 changes: 2 additions & 2 deletions test/tools/SplitterRebalanceResolverTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe("SplitterRebalanceResolverTest", function () {
it("maxGasAdjusted", async () => {
for (let i = 0; i < 30; i++) {
const gas = formatUnits(await resolver.maxGasAdjusted(), 9);
console.log(i, gas);
// console.log(i, gas);
await TimeUtils.advanceBlocksOnTs(60 * 60 * 24);
}
});
Expand Down Expand Up @@ -166,7 +166,7 @@ describe("SplitterRebalanceResolverTest", function () {
it("execute call", async () => {
const data = await resolver.checker();
expect(data.canExec).eq(true)
console.log('data.execPayload', data.execPayload)
// console.log('data.execPayload', data.execPayload)
const vaultCall = SplitterRebalanceResolver__factory.createInterface().decodeFunctionData('call', data.execPayload).vault
expect(vaultCall).eq(vault.address)

Expand Down
20 changes: 10 additions & 10 deletions test/vault/CheckWithdrawImpactTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe("Tests for StrategyBaseV2._checkWithdrawImpact", function () {

// prepare the strategy
await strategy.init(controller.address, splitter.address);
await vault.setFees(0, withdrawFee);
// await vault.setFees(0, withdrawFee);

// set strategy balance to expected value
await asset.transfer(strategy.address, balanceAfterWithdraw);
Expand All @@ -120,7 +120,7 @@ describe("Tests for StrategyBaseV2._checkWithdrawImpact", function () {
parseUnits("99", 6),
parseUnits("245", 6), // (99 - 50) * 5
parseUnits("5", 18),
100 // fee denominator is 100_000
0 // fee denominator is 100_000
);
expect(retBalance.eq(parseUnits("99", 6))).eq(true);
});
Expand All @@ -131,7 +131,7 @@ describe("Tests for StrategyBaseV2._checkWithdrawImpact", function () {
parseUnits("99", 8),
parseUnits("245", 8), // (99 - 50) * 5
parseUnits("5", 18),
100 // fee denominator is 100_000
0 // fee denominator is 100_000
);
expect(retBalance.eq(parseUnits("99", 8))).eq(true);
});
Expand All @@ -145,7 +145,7 @@ describe("Tests for StrategyBaseV2._checkWithdrawImpact", function () {
parseUnits("99", 6),
parseUnits("247", 6), // (99 - 50) * 5 + delta
parseUnits("5", 18),
1000 // fee denominator is 100_000
0 // fee denominator is 100_000
);
expect(retBalance.eq(parseUnits("99", 6))).eq(true);
});
Expand All @@ -156,7 +156,7 @@ describe("Tests for StrategyBaseV2._checkWithdrawImpact", function () {
parseUnits("99", 8),
parseUnits("247", 8), // (99 - 50) * 5 + delta
parseUnits("5", 18),
1000 // fee denominator is 100_000
0 // fee denominator is 100_000
);
expect(retBalance.eq(parseUnits("99", 8))).eq(true);
});
Expand All @@ -168,10 +168,10 @@ describe("Tests for StrategyBaseV2._checkWithdrawImpact", function () {
makeCheckWithdrawImpactTest(
usdc,
parseUnits("50", 6),
parseUnits("99", 6),
parseUnits("90", 6),
parseUnits("247", 6), // (99 - 50) * 5 + delta
parseUnits("5", 18),
100 // fee denominator is 100_000
0 // fee denominator is 100_000
)
).revertedWith("SB: Too high");
});
Expand All @@ -180,10 +180,10 @@ describe("Tests for StrategyBaseV2._checkWithdrawImpact", function () {
makeCheckWithdrawImpactTest(
usdc,
parseUnits("50", 8),
parseUnits("99", 8),
parseUnits("90", 8),
parseUnits("247", 8), // (99 - 50) * 5 + delta
parseUnits("5", 18),
100 // fee denominator is 100_000
0 // fee denominator is 100_000
)
).revertedWith("SB: Too high");
});
Expand All @@ -197,7 +197,7 @@ describe("Tests for StrategyBaseV2._checkWithdrawImpact", function () {
parseUnits("99", 8),
parseUnits("0", 8), // (!) investedAssetsUSD is zero
parseUnits("0", 18), // (!) price is zero
100 // fee denominator is 100_000
0 // fee denominator is 100_000
);
expect(retBalance.eq(parseUnits("99", 8))).eq(true);
});
Expand Down
Loading

0 comments on commit d5b9d6d

Please sign in to comment.