ERC4626 (#3171)
This commit is contained in:
@ -6,6 +6,8 @@
|
|||||||
* `TimelockController`: Migrate `_call` to `_execute` and allow inheritance and overriding similar to `Governor`. ([#3317](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3317))
|
* `TimelockController`: Migrate `_call` to `_execute` and allow inheritance and overriding similar to `Governor`. ([#3317](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3317))
|
||||||
* `CrossChainEnabledPolygonChild`: replace the `require` statement with the custom error `NotCrossChainCall`. ([#3380](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3380))
|
* `CrossChainEnabledPolygonChild`: replace the `require` statement with the custom error `NotCrossChainCall`. ([#3380](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3380))
|
||||||
* `ERC20FlashMint`: Add customizable flash fee receiver. ([#3327](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3327))
|
* `ERC20FlashMint`: Add customizable flash fee receiver. ([#3327](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3327))
|
||||||
|
* `ERC20TokenizedVault`: add an extension of `ERC20` that implements the ERC4626 Tokenized Vault Standard. ([#3171](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3171))
|
||||||
|
* `Math`: add a `mulDiv` function that can round the result either up or down. ([#3171](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3171))
|
||||||
* `Strings`: add a new overloaded function `toHexString` that converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation. ([#3403](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3403))
|
* `Strings`: add a new overloaded function `toHexString` that converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation. ([#3403](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3403))
|
||||||
* `EnumerableMap`: add new `UintToUintMap` map type. ([#3338](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3338))
|
* `EnumerableMap`: add new `UintToUintMap` map type. ([#3338](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3338))
|
||||||
* `EnumerableMap`: add new `Bytes32ToUintMap` map type. ([#3416](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3416))
|
* `EnumerableMap`: add new `Bytes32ToUintMap` map type. ([#3416](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3416))
|
||||||
|
|||||||
239
contracts/interfaces/IERC4626.sol
Normal file
239
contracts/interfaces/IERC4626.sol
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "../token/ERC20/IERC20.sol";
|
||||||
|
import "../token/ERC20/extensions/IERC20Metadata.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Interface of the ERC4626 "Tokenized Vault Standard", as defined in
|
||||||
|
* https://eips.ethereum.org/EIPS/eip-4626[ERC-4626].
|
||||||
|
*
|
||||||
|
* _Available since v4.7._
|
||||||
|
*/
|
||||||
|
interface IERC4626 is IERC20, IERC20Metadata {
|
||||||
|
event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares);
|
||||||
|
|
||||||
|
event Withdraw(
|
||||||
|
address indexed caller,
|
||||||
|
address indexed receiver,
|
||||||
|
address indexed owner,
|
||||||
|
uint256 assets,
|
||||||
|
uint256 shares
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing.
|
||||||
|
*
|
||||||
|
* - MUST be an ERC-20 token contract.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*/
|
||||||
|
function asset() external view returns (address assetTokenAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the total amount of the underlying asset that is “managed” by Vault.
|
||||||
|
*
|
||||||
|
* - SHOULD include any compounding that occurs from yield.
|
||||||
|
* - MUST be inclusive of any fees that are charged against assets in the Vault.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*/
|
||||||
|
function totalAssets() external view returns (uint256 totalManagedAssets);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal
|
||||||
|
* scenario where all the conditions are met.
|
||||||
|
*
|
||||||
|
* - MUST NOT be inclusive of any fees that are charged against assets in the Vault.
|
||||||
|
* - MUST NOT show any variations depending on the caller.
|
||||||
|
* - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*
|
||||||
|
* NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the
|
||||||
|
* “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and
|
||||||
|
* from.
|
||||||
|
*/
|
||||||
|
function convertToShares(uint256 assets) external view returns (uint256 shares);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal
|
||||||
|
* scenario where all the conditions are met.
|
||||||
|
*
|
||||||
|
* - MUST NOT be inclusive of any fees that are charged against assets in the Vault.
|
||||||
|
* - MUST NOT show any variations depending on the caller.
|
||||||
|
* - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*
|
||||||
|
* NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the
|
||||||
|
* “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and
|
||||||
|
* from.
|
||||||
|
*/
|
||||||
|
function convertToAssets(uint256 shares) external view returns (uint256 assets);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver,
|
||||||
|
* through a deposit call.
|
||||||
|
*
|
||||||
|
* - MUST return a limited value if receiver is subject to some deposit limit.
|
||||||
|
* - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*/
|
||||||
|
function maxDeposit(address receiver) external view returns (uint256 maxAssets);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given
|
||||||
|
* current on-chain conditions.
|
||||||
|
*
|
||||||
|
* - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit
|
||||||
|
* call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called
|
||||||
|
* in the same transaction.
|
||||||
|
* - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the
|
||||||
|
* deposit would be accepted, regardless if the user has enough tokens approved, etc.
|
||||||
|
* - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*
|
||||||
|
* NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in
|
||||||
|
* share price or some other type of condition, meaning the depositor will lose assets by depositing.
|
||||||
|
*/
|
||||||
|
function previewDeposit(uint256 assets) external view returns (uint256 shares);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens.
|
||||||
|
*
|
||||||
|
* - MUST emit the Deposit event.
|
||||||
|
* - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the
|
||||||
|
* deposit execution, and are accounted for during deposit.
|
||||||
|
* - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not
|
||||||
|
* approving enough underlying tokens to the Vault contract, etc).
|
||||||
|
*
|
||||||
|
* NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token.
|
||||||
|
*/
|
||||||
|
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call.
|
||||||
|
* - MUST return a limited value if receiver is subject to some mint limit.
|
||||||
|
* - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*/
|
||||||
|
function maxMint(address receiver) external view returns (uint256 maxShares);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given
|
||||||
|
* current on-chain conditions.
|
||||||
|
*
|
||||||
|
* - MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call
|
||||||
|
* in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the
|
||||||
|
* same transaction.
|
||||||
|
* - MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint
|
||||||
|
* would be accepted, regardless if the user has enough tokens approved, etc.
|
||||||
|
* - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*
|
||||||
|
* NOTE: any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in
|
||||||
|
* share price or some other type of condition, meaning the depositor will lose assets by minting.
|
||||||
|
*/
|
||||||
|
function previewMint(uint256 shares) external view returns (uint256 assets);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens.
|
||||||
|
*
|
||||||
|
* - MUST emit the Deposit event.
|
||||||
|
* - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint
|
||||||
|
* execution, and are accounted for during mint.
|
||||||
|
* - MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not
|
||||||
|
* approving enough underlying tokens to the Vault contract, etc).
|
||||||
|
*
|
||||||
|
* NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token.
|
||||||
|
*/
|
||||||
|
function mint(uint256 shares, address receiver) external returns (uint256 assets);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the
|
||||||
|
* Vault, through a withdraw call.
|
||||||
|
*
|
||||||
|
* - MUST return a limited value if owner is subject to some withdrawal limit or timelock.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*/
|
||||||
|
function maxWithdraw(address owner) external view returns (uint256 maxAssets);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block,
|
||||||
|
* given current on-chain conditions.
|
||||||
|
*
|
||||||
|
* - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw
|
||||||
|
* call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if
|
||||||
|
* called
|
||||||
|
* in the same transaction.
|
||||||
|
* - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though
|
||||||
|
* the withdrawal would be accepted, regardless if the user has enough shares, etc.
|
||||||
|
* - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*
|
||||||
|
* NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in
|
||||||
|
* share price or some other type of condition, meaning the depositor will lose assets by depositing.
|
||||||
|
*/
|
||||||
|
function previewWithdraw(uint256 assets) external view returns (uint256 shares);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver.
|
||||||
|
*
|
||||||
|
* - MUST emit the Withdraw event.
|
||||||
|
* - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the
|
||||||
|
* withdraw execution, and are accounted for during withdraw.
|
||||||
|
* - MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner
|
||||||
|
* not having enough shares, etc).
|
||||||
|
*
|
||||||
|
* Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed.
|
||||||
|
* Those methods should be performed separately.
|
||||||
|
*/
|
||||||
|
function withdraw(
|
||||||
|
uint256 assets,
|
||||||
|
address receiver,
|
||||||
|
address owner
|
||||||
|
) external returns (uint256 shares);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the maximum amount of Vault shares that can be redeemed from the owner balance in the Vault,
|
||||||
|
* through a redeem call.
|
||||||
|
*
|
||||||
|
* - MUST return a limited value if owner is subject to some withdrawal limit or timelock.
|
||||||
|
* - MUST return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*/
|
||||||
|
function maxRedeem(address owner) external view returns (uint256 maxShares);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block,
|
||||||
|
* given current on-chain conditions.
|
||||||
|
*
|
||||||
|
* - MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call
|
||||||
|
* in the same transaction. I.e. redeem should return the same or more assets as previewRedeem if called in the
|
||||||
|
* same transaction.
|
||||||
|
* - MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the
|
||||||
|
* redemption would be accepted, regardless if the user has enough shares, etc.
|
||||||
|
* - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.
|
||||||
|
* - MUST NOT revert.
|
||||||
|
*
|
||||||
|
* NOTE: any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in
|
||||||
|
* share price or some other type of condition, meaning the depositor will lose assets by redeeming.
|
||||||
|
*/
|
||||||
|
function previewRedeem(uint256 shares) external view returns (uint256 assets);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Burns exactly shares from owner and sends assets of underlying tokens to receiver.
|
||||||
|
*
|
||||||
|
* - MUST emit the Withdraw event.
|
||||||
|
* - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the
|
||||||
|
* redeem execution, and are accounted for during redeem.
|
||||||
|
* - MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner
|
||||||
|
* not having enough shares, etc).
|
||||||
|
*
|
||||||
|
* NOTE: some implementations will require pre-requesting to the Vault before a withdrawal may be performed.
|
||||||
|
* Those methods should be performed separately.
|
||||||
|
*/
|
||||||
|
function redeem(
|
||||||
|
uint256 shares,
|
||||||
|
address receiver,
|
||||||
|
address owner
|
||||||
|
) external returns (uint256 assets);
|
||||||
|
}
|
||||||
22
contracts/mocks/ERC20TokenizedVaultMock.sol
Normal file
22
contracts/mocks/ERC20TokenizedVaultMock.sol
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "../token/ERC20/extensions/ERC20TokenizedVault.sol";
|
||||||
|
|
||||||
|
// mock class using ERC20
|
||||||
|
contract ERC20TokenizedVaultMock is ERC20TokenizedVault {
|
||||||
|
constructor(
|
||||||
|
IERC20Metadata asset,
|
||||||
|
string memory name,
|
||||||
|
string memory symbol
|
||||||
|
) ERC20(name, symbol) ERC20TokenizedVault(asset) {}
|
||||||
|
|
||||||
|
function mockMint(address account, uint256 amount) public {
|
||||||
|
_mint(account, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockBurn(address account, uint256 amount) public {
|
||||||
|
_burn(account, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,4 +20,13 @@ contract MathMock {
|
|||||||
function ceilDiv(uint256 a, uint256 b) public pure returns (uint256) {
|
function ceilDiv(uint256 a, uint256 b) public pure returns (uint256) {
|
||||||
return Math.ceilDiv(a, b);
|
return Math.ceilDiv(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mulDiv(
|
||||||
|
uint256 a,
|
||||||
|
uint256 b,
|
||||||
|
uint256 denominator,
|
||||||
|
Math.Rounding direction
|
||||||
|
) public pure returns (uint256) {
|
||||||
|
return Math.mulDiv(a, b, denominator, direction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ Additionally there are multiple custom extensions, including:
|
|||||||
* {ERC20Votes}: support for voting and vote delegation.
|
* {ERC20Votes}: support for voting and vote delegation.
|
||||||
* {ERC20VotesComp}: support for voting and vote delegation (compatible with Compound's token, with uint96 restrictions).
|
* {ERC20VotesComp}: support for voting and vote delegation (compatible with Compound's token, with uint96 restrictions).
|
||||||
* {ERC20Wrapper}: wrapper to create an ERC20 backed by another ERC20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}.
|
* {ERC20Wrapper}: wrapper to create an ERC20 backed by another ERC20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}.
|
||||||
|
* {ERC20TokenizedVault}: tokenized vault that manages shares (represented as ERC20) that are backed by assets (another ERC20).
|
||||||
|
|
||||||
Finally, there are some utilities to interact with ERC20 contracts in various ways.
|
Finally, there are some utilities to interact with ERC20 contracts in various ways.
|
||||||
|
|
||||||
@ -62,6 +63,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
|
|||||||
|
|
||||||
{{ERC20FlashMint}}
|
{{ERC20FlashMint}}
|
||||||
|
|
||||||
|
{{ERC20TokenizedVault}}
|
||||||
|
|
||||||
== Draft EIPs
|
== Draft EIPs
|
||||||
|
|
||||||
The following EIPs are still in Draft status. Due to their nature as drafts, the details of these contracts may change and we cannot guarantee their xref:ROOT:releases-stability.adoc[stability]. Minor releases of OpenZeppelin Contracts may contain breaking changes for the contracts in this directory, which will be duly announced in the https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CHANGELOG.md[changelog]. The EIPs included here are used by projects in production and this may make them less likely to change significantly.
|
The following EIPs are still in Draft status. Due to their nature as drafts, the details of these contracts may change and we cannot guarantee their xref:ROOT:releases-stability.adoc[stability]. Minor releases of OpenZeppelin Contracts may contain breaking changes for the contracts in this directory, which will be duly announced in the https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CHANGELOG.md[changelog]. The EIPs included here are used by projects in production and this may make them less likely to change significantly.
|
||||||
|
|||||||
217
contracts/token/ERC20/extensions/ERC20TokenizedVault.sol
Normal file
217
contracts/token/ERC20/extensions/ERC20TokenizedVault.sol
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "../ERC20.sol";
|
||||||
|
import "../utils/SafeERC20.sol";
|
||||||
|
import "../../../interfaces/IERC4626.sol";
|
||||||
|
import "../../../utils/math/Math.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Implementation of the ERC4626 "Tokenized Vault Standard" as defined in
|
||||||
|
* https://eips.ethereum.org/EIPS/eip-4626[EIP-4626].
|
||||||
|
*
|
||||||
|
* This extension allows the minting and burning of "shares" (represented using the ERC20 inheritance) in exchange for
|
||||||
|
* underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends
|
||||||
|
* the ERC20 standard. Any additional extensions included along it would affect the "shares" token represented by this
|
||||||
|
* contract and not the "assets" token which is an independent contract.
|
||||||
|
*
|
||||||
|
* _Available since v4.7._
|
||||||
|
*/
|
||||||
|
abstract contract ERC20TokenizedVault is ERC20, IERC4626 {
|
||||||
|
using Math for uint256;
|
||||||
|
|
||||||
|
IERC20Metadata private immutable _asset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777).
|
||||||
|
*/
|
||||||
|
constructor(IERC20Metadata asset_) {
|
||||||
|
_asset = asset_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-asset} */
|
||||||
|
function asset() public view virtual override returns (address) {
|
||||||
|
return address(_asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-totalAssets} */
|
||||||
|
function totalAssets() public view virtual override returns (uint256) {
|
||||||
|
return _asset.balanceOf(address(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-convertToShares} */
|
||||||
|
function convertToShares(uint256 assets) public view virtual override returns (uint256 shares) {
|
||||||
|
return _convertToShares(assets, Math.Rounding.Down);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-convertToAssets} */
|
||||||
|
function convertToAssets(uint256 shares) public view virtual override returns (uint256 assets) {
|
||||||
|
return _convertToAssets(shares, Math.Rounding.Down);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-maxDeposit} */
|
||||||
|
function maxDeposit(address) public view virtual override returns (uint256) {
|
||||||
|
return _isVaultCollateralized() ? type(uint256).max : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-maxMint} */
|
||||||
|
function maxMint(address) public view virtual override returns (uint256) {
|
||||||
|
return type(uint256).max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-maxWithdraw} */
|
||||||
|
function maxWithdraw(address owner) public view virtual override returns (uint256) {
|
||||||
|
return _convertToAssets(balanceOf(owner), Math.Rounding.Down);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-maxRedeem} */
|
||||||
|
function maxRedeem(address owner) public view virtual override returns (uint256) {
|
||||||
|
return balanceOf(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-previewDeposit} */
|
||||||
|
function previewDeposit(uint256 assets) public view virtual override returns (uint256) {
|
||||||
|
return _convertToShares(assets, Math.Rounding.Down);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-previewMint} */
|
||||||
|
function previewMint(uint256 shares) public view virtual override returns (uint256) {
|
||||||
|
return _convertToAssets(shares, Math.Rounding.Up);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-previewWithdraw} */
|
||||||
|
function previewWithdraw(uint256 assets) public view virtual override returns (uint256) {
|
||||||
|
return _convertToShares(assets, Math.Rounding.Up);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-previewRedeem} */
|
||||||
|
function previewRedeem(uint256 shares) public view virtual override returns (uint256) {
|
||||||
|
return _convertToAssets(shares, Math.Rounding.Down);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-deposit} */
|
||||||
|
function deposit(uint256 assets, address receiver) public virtual override returns (uint256) {
|
||||||
|
require(assets <= maxDeposit(receiver), "ERC20TokenizedVault: deposit more than max");
|
||||||
|
|
||||||
|
uint256 shares = previewDeposit(assets);
|
||||||
|
_deposit(_msgSender(), receiver, assets, shares);
|
||||||
|
|
||||||
|
return shares;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-mint} */
|
||||||
|
function mint(uint256 shares, address receiver) public virtual override returns (uint256) {
|
||||||
|
require(shares <= maxMint(receiver), "ERC20TokenizedVault: mint more than max");
|
||||||
|
|
||||||
|
uint256 assets = previewMint(shares);
|
||||||
|
_deposit(_msgSender(), receiver, assets, shares);
|
||||||
|
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-withdraw} */
|
||||||
|
function withdraw(
|
||||||
|
uint256 assets,
|
||||||
|
address receiver,
|
||||||
|
address owner
|
||||||
|
) public virtual override returns (uint256) {
|
||||||
|
require(assets <= maxWithdraw(owner), "ERC20TokenizedVault: withdraw more than max");
|
||||||
|
|
||||||
|
uint256 shares = previewWithdraw(assets);
|
||||||
|
_withdraw(_msgSender(), receiver, owner, assets, shares);
|
||||||
|
|
||||||
|
return shares;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev See {IERC4262-redeem} */
|
||||||
|
function redeem(
|
||||||
|
uint256 shares,
|
||||||
|
address receiver,
|
||||||
|
address owner
|
||||||
|
) public virtual override returns (uint256) {
|
||||||
|
require(shares <= maxRedeem(owner), "ERC20TokenizedVault: redeem more than max");
|
||||||
|
|
||||||
|
uint256 assets = previewRedeem(shares);
|
||||||
|
_withdraw(_msgSender(), receiver, owner, assets, shares);
|
||||||
|
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Internal convertion function (from assets to shares) with support for rounding direction
|
||||||
|
*
|
||||||
|
* Will revert if assets > 0, totalSupply > 0 and totalAssets = 0. That corresponds to a case where any asset
|
||||||
|
* would represent an infinite amout of shares.
|
||||||
|
*/
|
||||||
|
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256 shares) {
|
||||||
|
uint256 supply = totalSupply();
|
||||||
|
return
|
||||||
|
(assets == 0 || supply == 0)
|
||||||
|
? assets.mulDiv(10**decimals(), 10**_asset.decimals(), rounding)
|
||||||
|
: assets.mulDiv(supply, totalAssets(), rounding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Internal convertion function (from shares to assets) with support for rounding direction
|
||||||
|
*/
|
||||||
|
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256 assets) {
|
||||||
|
uint256 supply = totalSupply();
|
||||||
|
return
|
||||||
|
(supply == 0)
|
||||||
|
? shares.mulDiv(10**_asset.decimals(), 10**decimals(), rounding)
|
||||||
|
: shares.mulDiv(totalAssets(), supply, rounding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Deposit/mint common workflow
|
||||||
|
*/
|
||||||
|
function _deposit(
|
||||||
|
address caller,
|
||||||
|
address receiver,
|
||||||
|
uint256 assets,
|
||||||
|
uint256 shares
|
||||||
|
) private {
|
||||||
|
// If _asset is ERC777, `transferFrom` can trigger a reenterancy BEFORE the transfer happens through the
|
||||||
|
// `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
|
||||||
|
// calls the vault, which is assumed not malicious.
|
||||||
|
//
|
||||||
|
// Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
|
||||||
|
// assets are transfered and before the shares are minted, which is a valid state.
|
||||||
|
// slither-disable-next-line reentrancy-no-eth
|
||||||
|
SafeERC20.safeTransferFrom(_asset, caller, address(this), assets);
|
||||||
|
_mint(receiver, shares);
|
||||||
|
|
||||||
|
emit Deposit(caller, receiver, assets, shares);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Withdraw/redeem common workflow
|
||||||
|
*/
|
||||||
|
function _withdraw(
|
||||||
|
address caller,
|
||||||
|
address receiver,
|
||||||
|
address owner,
|
||||||
|
uint256 assets,
|
||||||
|
uint256 shares
|
||||||
|
) private {
|
||||||
|
if (caller != owner) {
|
||||||
|
_spendAllowance(owner, caller, shares);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If _asset is ERC777, `transfer` can trigger trigger a reentrancy AFTER the transfer happens through the
|
||||||
|
// `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
|
||||||
|
// calls the vault, which is assumed not malicious.
|
||||||
|
//
|
||||||
|
// Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
|
||||||
|
// shares are burned and after the assets are transfered, which is a valid state.
|
||||||
|
_burn(owner, shares);
|
||||||
|
SafeERC20.safeTransfer(_asset, receiver, assets);
|
||||||
|
|
||||||
|
emit Withdraw(caller, receiver, owner, assets, shares);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isVaultCollateralized() private view returns (bool) {
|
||||||
|
return totalAssets() > 0 || totalSupply() == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,12 @@ pragma solidity ^0.8.0;
|
|||||||
* @dev Standard math utilities missing in the Solidity language.
|
* @dev Standard math utilities missing in the Solidity language.
|
||||||
*/
|
*/
|
||||||
library Math {
|
library Math {
|
||||||
|
enum Rounding {
|
||||||
|
Down, // Toward negative infinity
|
||||||
|
Up, // Toward infinity
|
||||||
|
Zero // Toward zero
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Returns the largest of two numbers.
|
* @dev Returns the largest of two numbers.
|
||||||
*/
|
*/
|
||||||
@ -38,6 +44,109 @@ library Math {
|
|||||||
*/
|
*/
|
||||||
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
|
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
|
||||||
// (a + b - 1) / b can overflow on addition, so we distribute.
|
// (a + b - 1) / b can overflow on addition, so we distribute.
|
||||||
return a / b + (a % b == 0 ? 0 : 1);
|
return a == 0 ? 0 : (a - 1) / b + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
|
||||||
|
* @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv)
|
||||||
|
* with further edits by Uniswap Labs also under MIT license.
|
||||||
|
*/
|
||||||
|
function mulDiv(
|
||||||
|
uint256 x,
|
||||||
|
uint256 y,
|
||||||
|
uint256 denominator
|
||||||
|
) internal pure returns (uint256 result) {
|
||||||
|
unchecked {
|
||||||
|
// 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use
|
||||||
|
// use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
|
||||||
|
// variables such that product = prod1 * 2^256 + prod0.
|
||||||
|
uint256 prod0; // Least significant 256 bits of the product
|
||||||
|
uint256 prod1; // Most significant 256 bits of the product
|
||||||
|
assembly {
|
||||||
|
let mm := mulmod(x, y, not(0))
|
||||||
|
prod0 := mul(x, y)
|
||||||
|
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle non-overflow cases, 256 by 256 division.
|
||||||
|
if (prod1 == 0) {
|
||||||
|
return prod0 / denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the result is less than 2^256. Also prevents denominator == 0.
|
||||||
|
require(denominator > prod1);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
// 512 by 256 division.
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Make division exact by subtracting the remainder from [prod1 prod0].
|
||||||
|
uint256 remainder;
|
||||||
|
assembly {
|
||||||
|
// Compute remainder using mulmod.
|
||||||
|
remainder := mulmod(x, y, denominator)
|
||||||
|
|
||||||
|
// Subtract 256 bit number from 512 bit number.
|
||||||
|
prod1 := sub(prod1, gt(remainder, prod0))
|
||||||
|
prod0 := sub(prod0, remainder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1.
|
||||||
|
// See https://cs.stackexchange.com/q/138556/92363.
|
||||||
|
|
||||||
|
// Does not overflow because the denominator cannot be zero at this stage in the function.
|
||||||
|
uint256 twos = denominator & (~denominator + 1);
|
||||||
|
assembly {
|
||||||
|
// Divide denominator by twos.
|
||||||
|
denominator := div(denominator, twos)
|
||||||
|
|
||||||
|
// Divide [prod1 prod0] by twos.
|
||||||
|
prod0 := div(prod0, twos)
|
||||||
|
|
||||||
|
// Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.
|
||||||
|
twos := add(div(sub(0, twos), twos), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift in bits from prod1 into prod0.
|
||||||
|
prod0 |= prod1 * twos;
|
||||||
|
|
||||||
|
// Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such
|
||||||
|
// that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for
|
||||||
|
// four bits. That is, denominator * inv = 1 mod 2^4.
|
||||||
|
uint256 inverse = (3 * denominator) ^ 2;
|
||||||
|
|
||||||
|
// Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works
|
||||||
|
// in modular arithmetic, doubling the correct bits in each step.
|
||||||
|
inverse *= 2 - denominator * inverse; // inverse mod 2^8
|
||||||
|
inverse *= 2 - denominator * inverse; // inverse mod 2^16
|
||||||
|
inverse *= 2 - denominator * inverse; // inverse mod 2^32
|
||||||
|
inverse *= 2 - denominator * inverse; // inverse mod 2^64
|
||||||
|
inverse *= 2 - denominator * inverse; // inverse mod 2^128
|
||||||
|
inverse *= 2 - denominator * inverse; // inverse mod 2^256
|
||||||
|
|
||||||
|
// Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
|
||||||
|
// This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is
|
||||||
|
// less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1
|
||||||
|
// is no longer required.
|
||||||
|
result = prod0 * inverse;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Calculates x * y / denominator with full precision, following the selected rounding direction.
|
||||||
|
*/
|
||||||
|
function mulDiv(
|
||||||
|
uint256 x,
|
||||||
|
uint256 y,
|
||||||
|
uint256 denominator,
|
||||||
|
Rounding rounding
|
||||||
|
) internal pure returns (uint256) {
|
||||||
|
uint256 result = mulDiv(x, y, denominator);
|
||||||
|
if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) {
|
||||||
|
result += 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,4 +21,9 @@ module.exports = {
|
|||||||
'For',
|
'For',
|
||||||
'Abstain',
|
'Abstain',
|
||||||
),
|
),
|
||||||
|
Rounding: Enum(
|
||||||
|
'Down',
|
||||||
|
'Up',
|
||||||
|
'Zero',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
612
test/token/ERC20/extensions/ERC20TokenizedVault.test.js
Normal file
612
test/token/ERC20/extensions/ERC20TokenizedVault.test.js
Normal file
@ -0,0 +1,612 @@
|
|||||||
|
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||||
|
const { expect } = require('chai');
|
||||||
|
|
||||||
|
const ERC20DecimalsMock = artifacts.require('ERC20DecimalsMock');
|
||||||
|
const ERC20TokenizedVaultMock = artifacts.require('ERC20TokenizedVaultMock');
|
||||||
|
|
||||||
|
const parseToken = (token) => (new BN(token)).mul(new BN('1000000000000'));
|
||||||
|
const parseShare = (share) => (new BN(share)).mul(new BN('1000000000000000000'));
|
||||||
|
|
||||||
|
contract('ERC20TokenizedVault', function (accounts) {
|
||||||
|
const [ holder, recipient, spender, other, user1, user2 ] = accounts;
|
||||||
|
|
||||||
|
const name = 'My Token';
|
||||||
|
const symbol = 'MTKN';
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
this.token = await ERC20DecimalsMock.new(name, symbol, 12);
|
||||||
|
this.vault = await ERC20TokenizedVaultMock.new(this.token.address, name + ' Vault', symbol + 'V');
|
||||||
|
|
||||||
|
await this.token.mint(holder, web3.utils.toWei('100'));
|
||||||
|
await this.token.approve(this.vault.address, constants.MAX_UINT256, { from: holder });
|
||||||
|
await this.vault.approve(spender, constants.MAX_UINT256, { from: holder });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('metadata', async function () {
|
||||||
|
expect(await this.vault.name()).to.be.equal(name + ' Vault');
|
||||||
|
expect(await this.vault.symbol()).to.be.equal(symbol + 'V');
|
||||||
|
expect(await this.vault.asset()).to.be.equal(this.token.address);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('empty vault: no assets & no shares', function () {
|
||||||
|
it('status', async function () {
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deposit', async function () {
|
||||||
|
expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
|
||||||
|
expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(1));
|
||||||
|
|
||||||
|
const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: parseToken(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: recipient,
|
||||||
|
value: parseShare(1),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mint', async function () {
|
||||||
|
expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
|
||||||
|
expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1));
|
||||||
|
|
||||||
|
const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: parseToken(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: recipient,
|
||||||
|
value: parseShare(1),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('withdraw', async function () {
|
||||||
|
expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0');
|
||||||
|
|
||||||
|
const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: recipient,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redeem', async function () {
|
||||||
|
expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0');
|
||||||
|
|
||||||
|
const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: recipient,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('partially empty vault: assets & no shares', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
await this.token.mint(this.vault.address, parseToken(1)); // 1 token
|
||||||
|
});
|
||||||
|
|
||||||
|
it('status', async function () {
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deposit', async function () {
|
||||||
|
expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
|
||||||
|
expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(1));
|
||||||
|
|
||||||
|
const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: parseToken(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: recipient,
|
||||||
|
value: parseShare(1),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mint', async function () {
|
||||||
|
expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
|
||||||
|
expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1));
|
||||||
|
|
||||||
|
const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: parseToken(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: recipient,
|
||||||
|
value: parseShare(1),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('withdraw', async function () {
|
||||||
|
expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0');
|
||||||
|
|
||||||
|
const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: recipient,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redeem', async function () {
|
||||||
|
expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0');
|
||||||
|
|
||||||
|
const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: recipient,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('partially empty vault: shares & no assets', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
await this.vault.mockMint(holder, parseShare(1)); // 1 share
|
||||||
|
});
|
||||||
|
|
||||||
|
it('status', async function () {
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deposit', async function () {
|
||||||
|
expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal('0');
|
||||||
|
|
||||||
|
// Can deposit 0 (max deposit)
|
||||||
|
const { tx } = await this.vault.deposit(0, recipient, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: recipient,
|
||||||
|
value: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cannot deposit more than 0
|
||||||
|
await expectRevert.unspecified(this.vault.previewDeposit(parseToken(1)));
|
||||||
|
await expectRevert(
|
||||||
|
this.vault.deposit(parseToken(1), recipient, { from: holder }),
|
||||||
|
'ERC20TokenizedVault: deposit more than max',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mint', async function () {
|
||||||
|
expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
|
||||||
|
expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal('0');
|
||||||
|
|
||||||
|
const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: recipient,
|
||||||
|
value: parseShare(1),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('withdraw', async function () {
|
||||||
|
expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0');
|
||||||
|
await expectRevert.unspecified(this.vault.previewWithdraw('1'));
|
||||||
|
|
||||||
|
const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: recipient,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redeem', async function () {
|
||||||
|
expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(parseShare(1));
|
||||||
|
expect(await this.vault.previewRedeem(parseShare(1))).to.be.bignumber.equal('0');
|
||||||
|
|
||||||
|
const { tx } = await this.vault.redeem(parseShare(1), recipient, holder, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: recipient,
|
||||||
|
value: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: parseShare(1),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('full vault: assets & shares', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
await this.token.mint(this.vault.address, parseToken(1)); // 1 tokens
|
||||||
|
await this.vault.mockMint(holder, parseShare(100)); // 100 share
|
||||||
|
});
|
||||||
|
|
||||||
|
it('status', async function () {
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deposit', async function () {
|
||||||
|
expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
|
||||||
|
expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(100));
|
||||||
|
|
||||||
|
const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: parseToken(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: recipient,
|
||||||
|
value: parseShare(100),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mint', async function () {
|
||||||
|
expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
|
||||||
|
expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1).divn(100));
|
||||||
|
|
||||||
|
const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: parseToken(1).divn(100),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: recipient,
|
||||||
|
value: parseShare(1),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('withdraw', async function () {
|
||||||
|
expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal(parseToken(1));
|
||||||
|
expect(await this.vault.previewWithdraw(parseToken(1))).to.be.bignumber.equal(parseShare(100));
|
||||||
|
|
||||||
|
const { tx } = await this.vault.withdraw(parseToken(1), recipient, holder, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: recipient,
|
||||||
|
value: parseToken(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: parseShare(100),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('withdraw with approval', async function () {
|
||||||
|
await expectRevert(
|
||||||
|
this.vault.withdraw(parseToken(1), recipient, holder, { from: other }),
|
||||||
|
'ERC20: insufficient allowance',
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.vault.withdraw(parseToken(1), recipient, holder, { from: spender });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redeem', async function () {
|
||||||
|
expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(parseShare(100));
|
||||||
|
expect(await this.vault.previewRedeem(parseShare(100))).to.be.bignumber.equal(parseToken(1));
|
||||||
|
|
||||||
|
const { tx } = await this.vault.redeem(parseShare(100), recipient, holder, { from: holder });
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: recipient,
|
||||||
|
value: parseToken(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: holder,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: parseShare(100),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redeem with approval', async function () {
|
||||||
|
await expectRevert(
|
||||||
|
this.vault.redeem(parseShare(100), recipient, holder, { from: other }),
|
||||||
|
'ERC20: insufficient allowance',
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.vault.redeem(parseShare(100), recipient, holder, { from: spender });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Scenario inspired by solmate ERC4626 tests:
|
||||||
|
/// https://github.com/Rari-Capital/solmate/blob/main/src/test/ERC4626.t.sol
|
||||||
|
it('multiple mint, deposit, redeem & withdrawal', async function () {
|
||||||
|
// test designed with both asset using similar decimals
|
||||||
|
this.token = await ERC20DecimalsMock.new(name, symbol, 18);
|
||||||
|
this.vault = await ERC20TokenizedVaultMock.new(this.token.address, name + ' Vault', symbol + 'V');
|
||||||
|
|
||||||
|
await this.token.mint(user1, 4000);
|
||||||
|
await this.token.mint(user2, 7001);
|
||||||
|
await this.token.approve(this.vault.address, 4000, { from: user1 });
|
||||||
|
await this.token.approve(this.vault.address, 7001, { from: user2 });
|
||||||
|
|
||||||
|
// 1. Alice mints 2000 shares (costs 2000 tokens)
|
||||||
|
{
|
||||||
|
const { tx } = await this.vault.mint(2000, user1, { from: user1 });
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: user1,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: '2000',
|
||||||
|
});
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: user1,
|
||||||
|
value: '2000',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await this.vault.previewDeposit(2000)).to.be.bignumber.equal('2000');
|
||||||
|
expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
|
||||||
|
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.totalSupply()).to.be.bignumber.equal('2000');
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('2000');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Bob deposits 4000 tokens (mints 4000 shares)
|
||||||
|
{
|
||||||
|
const { tx } = await this.vault.mint(4000, user2, { from: user2 });
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: user2,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: '4000',
|
||||||
|
});
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: user2,
|
||||||
|
value: '4000',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await this.vault.previewDeposit(4000)).to.be.bignumber.equal('4000');
|
||||||
|
expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
|
||||||
|
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('4000');
|
||||||
|
expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('6000');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Vault mutates by +3000 tokens (simulated yield returned from strategy)
|
||||||
|
await this.token.mint(this.vault.address, 3000);
|
||||||
|
|
||||||
|
expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
|
||||||
|
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3000');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('6000');
|
||||||
|
expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('9000');
|
||||||
|
|
||||||
|
// 4. Alice deposits 2000 tokens (mints 1333 shares)
|
||||||
|
{
|
||||||
|
const { tx } = await this.vault.deposit(2000, user1, { from: user1 });
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: user1,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: '2000',
|
||||||
|
});
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: user1,
|
||||||
|
value: '1333',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
|
||||||
|
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('6000');
|
||||||
|
expect(await this.vault.totalSupply()).to.be.bignumber.equal('7333');
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('11000');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Bob mints 2000 shares (costs 3001 assets)
|
||||||
|
// NOTE: Bob's assets spent got rounded up
|
||||||
|
// NOTE: Alices's vault assets got rounded up
|
||||||
|
{
|
||||||
|
const { tx } = await this.vault.mint(2000, user2, { from: user2 });
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: user2,
|
||||||
|
to: this.vault.address,
|
||||||
|
value: '3001',
|
||||||
|
});
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: constants.ZERO_ADDRESS,
|
||||||
|
to: user2,
|
||||||
|
value: '2000',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
|
||||||
|
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('5000');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('9000');
|
||||||
|
expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('14001');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Vault mutates by +3000 tokens
|
||||||
|
// NOTE: Vault holds 17001 tokens, but sum of assetsOf() is 17000.
|
||||||
|
await this.token.mint(this.vault.address, 3000);
|
||||||
|
|
||||||
|
expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
|
||||||
|
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('6071');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10929');
|
||||||
|
expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('17001');
|
||||||
|
|
||||||
|
// 7. Alice redeem 1333 shares (2428 assets)
|
||||||
|
{
|
||||||
|
const { tx } = await this.vault.redeem(1333, user1, user1, { from: user1 });
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: user1,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: '1333',
|
||||||
|
});
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: user1,
|
||||||
|
value: '2428',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
|
||||||
|
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10929');
|
||||||
|
expect(await this.vault.totalSupply()).to.be.bignumber.equal('8000');
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('14573');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Bob withdraws 2929 assets (1608 shares)
|
||||||
|
{
|
||||||
|
const { tx } = await this.vault.withdraw(2929, user2, user2, { from: user2 });
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: user2,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: '1608',
|
||||||
|
});
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: user2,
|
||||||
|
value: '2929',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
|
||||||
|
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000');
|
||||||
|
expect(await this.vault.totalSupply()).to.be.bignumber.equal('6392');
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('11644');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Alice withdraws 3643 assets (2000 shares)
|
||||||
|
// NOTE: Bob's assets have been rounded back up
|
||||||
|
{
|
||||||
|
const { tx } = await this.vault.withdraw(3643, user1, user1, { from: user1 });
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: user1,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: '2000',
|
||||||
|
});
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: user1,
|
||||||
|
value: '3643',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8001');
|
||||||
|
expect(await this.vault.totalSupply()).to.be.bignumber.equal('4392');
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('8001');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Bob redeem 4392 shares (8001 tokens)
|
||||||
|
{
|
||||||
|
const { tx } = await this.vault.redeem(4392, user2, user2, { from: user2 });
|
||||||
|
expectEvent.inTransaction(tx, this.vault, 'Transfer', {
|
||||||
|
from: user2,
|
||||||
|
to: constants.ZERO_ADDRESS,
|
||||||
|
value: '4392',
|
||||||
|
});
|
||||||
|
expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||||
|
from: this.vault.address,
|
||||||
|
to: user2,
|
||||||
|
value: '8001',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.totalSupply()).to.be.bignumber.equal('0');
|
||||||
|
expect(await this.vault.totalAssets()).to.be.bignumber.equal('0');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,12 +1,15 @@
|
|||||||
const { BN, constants } = require('@openzeppelin/test-helpers');
|
const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
|
||||||
const { expect } = require('chai');
|
const { expect } = require('chai');
|
||||||
const { MAX_UINT256 } = constants;
|
const { MAX_UINT256 } = constants;
|
||||||
|
const { Rounding } = require('../../helpers/enums.js');
|
||||||
|
|
||||||
const MathMock = artifacts.require('MathMock');
|
const MathMock = artifacts.require('MathMock');
|
||||||
|
|
||||||
contract('Math', function (accounts) {
|
contract('Math', function (accounts) {
|
||||||
const min = new BN('1234');
|
const min = new BN('1234');
|
||||||
const max = new BN('5678');
|
const max = new BN('5678');
|
||||||
|
const MAX_UINT256_SUB1 = MAX_UINT256.sub(new BN('1'));
|
||||||
|
const MAX_UINT256_SUB2 = MAX_UINT256.sub(new BN('2'));
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
this.math = await MathMock.new();
|
this.math = await MathMock.new();
|
||||||
@ -85,4 +88,98 @@ contract('Math', function (accounts) {
|
|||||||
expect(await this.math.ceilDiv(MAX_UINT256, b)).to.be.bignumber.equal(MAX_UINT256);
|
expect(await this.math.ceilDiv(MAX_UINT256, b)).to.be.bignumber.equal(MAX_UINT256);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('muldiv', function () {
|
||||||
|
it('divide by 0', async function () {
|
||||||
|
await expectRevert.unspecified(this.math.mulDiv(1, 1, 0, Rounding.Down));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('does round down', async function () {
|
||||||
|
it('small values', async function () {
|
||||||
|
expect(await this.math.mulDiv('3', '4', '5', Rounding.Down)).to.be.bignumber.equal('2');
|
||||||
|
expect(await this.math.mulDiv('3', '5', '5', Rounding.Down)).to.be.bignumber.equal('3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('large values', async function () {
|
||||||
|
expect(await this.math.mulDiv(
|
||||||
|
new BN('42'),
|
||||||
|
MAX_UINT256_SUB1,
|
||||||
|
MAX_UINT256,
|
||||||
|
Rounding.Down,
|
||||||
|
)).to.be.bignumber.equal(new BN('41'));
|
||||||
|
|
||||||
|
expect(await this.math.mulDiv(
|
||||||
|
new BN('17'),
|
||||||
|
MAX_UINT256,
|
||||||
|
MAX_UINT256,
|
||||||
|
Rounding.Down,
|
||||||
|
)).to.be.bignumber.equal(new BN('17'));
|
||||||
|
|
||||||
|
expect(await this.math.mulDiv(
|
||||||
|
MAX_UINT256_SUB1,
|
||||||
|
MAX_UINT256_SUB1,
|
||||||
|
MAX_UINT256,
|
||||||
|
Rounding.Down,
|
||||||
|
)).to.be.bignumber.equal(MAX_UINT256_SUB2);
|
||||||
|
|
||||||
|
expect(await this.math.mulDiv(
|
||||||
|
MAX_UINT256,
|
||||||
|
MAX_UINT256_SUB1,
|
||||||
|
MAX_UINT256,
|
||||||
|
Rounding.Down,
|
||||||
|
)).to.be.bignumber.equal(MAX_UINT256_SUB1);
|
||||||
|
|
||||||
|
expect(await this.math.mulDiv(
|
||||||
|
MAX_UINT256,
|
||||||
|
MAX_UINT256,
|
||||||
|
MAX_UINT256,
|
||||||
|
Rounding.Down,
|
||||||
|
)).to.be.bignumber.equal(MAX_UINT256);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('does round up', async function () {
|
||||||
|
it('small values', async function () {
|
||||||
|
expect(await this.math.mulDiv('3', '4', '5', Rounding.Up)).to.be.bignumber.equal('3');
|
||||||
|
expect(await this.math.mulDiv('3', '5', '5', Rounding.Up)).to.be.bignumber.equal('3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('large values', async function () {
|
||||||
|
expect(await this.math.mulDiv(
|
||||||
|
new BN('42'),
|
||||||
|
MAX_UINT256_SUB1,
|
||||||
|
MAX_UINT256,
|
||||||
|
Rounding.Up,
|
||||||
|
)).to.be.bignumber.equal(new BN('42'));
|
||||||
|
|
||||||
|
expect(await this.math.mulDiv(
|
||||||
|
new BN('17'),
|
||||||
|
MAX_UINT256,
|
||||||
|
MAX_UINT256,
|
||||||
|
Rounding.Up,
|
||||||
|
)).to.be.bignumber.equal(new BN('17'));
|
||||||
|
|
||||||
|
expect(await this.math.mulDiv(
|
||||||
|
MAX_UINT256_SUB1,
|
||||||
|
MAX_UINT256_SUB1,
|
||||||
|
MAX_UINT256,
|
||||||
|
Rounding.Up,
|
||||||
|
)).to.be.bignumber.equal(MAX_UINT256_SUB1);
|
||||||
|
|
||||||
|
expect(await this.math.mulDiv(
|
||||||
|
MAX_UINT256,
|
||||||
|
MAX_UINT256_SUB1,
|
||||||
|
MAX_UINT256,
|
||||||
|
Rounding.Up,
|
||||||
|
)).to.be.bignumber.equal(MAX_UINT256_SUB1);
|
||||||
|
|
||||||
|
expect(await this.math.mulDiv(
|
||||||
|
MAX_UINT256,
|
||||||
|
MAX_UINT256,
|
||||||
|
MAX_UINT256,
|
||||||
|
Rounding.Up,
|
||||||
|
)).to.be.bignumber.equal(MAX_UINT256);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user