Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inherit asset decimals in ERC4626 #3639

Merged
merged 17 commits into from Aug 26, 2022
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@
* `GovernorCompatibilityBravo`: remove unused `using` statements. ([#3506](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3506))
* `ERC20`: optimize `_transfer`, `_mint` and `_burn` by using `unchecked` arithmetic when possible. ([#3513](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3513))
* `ERC20FlashMint`: add an internal `_flashFee` function for overriding. ([#3551](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3551))
* `ERC4626`: use the same `decimals()` as the underlying asset by default (if available), while keeping the hability to override the vault's decimals. ([#3639](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3639))
* `ERC721`: optimize transfers by making approval clearing implicit instead of emitting an event. ([#3481](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3481))
* `ERC721`: optimize burn by making approval clearing implicit instead of emitting an event. ([#3538](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3538))
* `ERC721`: Fix balance accounting when a custom `_beforeTokenTransfer` hook results in a transfer of the token under consideration. ([#3611](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3611))
Expand Down
18 changes: 17 additions & 1 deletion contracts/mocks/ERC4626Mock.sol
Expand Up @@ -4,7 +4,6 @@ pragma solidity ^0.8.0;

import "../token/ERC20/extensions/ERC4626.sol";

// mock class using ERC20
contract ERC4626Mock is ERC4626 {
constructor(
IERC20Metadata asset,
Expand All @@ -20,3 +19,20 @@ contract ERC4626Mock is ERC4626 {
_burn(account, amount);
}
}

contract ERC4626DecimalMock is ERC4626Mock {
uint8 private immutable _decimals;

constructor(
IERC20Metadata asset,
string memory name,
string memory symbol,
uint8 decimaloverride
) ERC4626Mock(asset, name, symbol) {
_decimals = decimaloverride;
Amxx marked this conversation as resolved.
Show resolved Hide resolved
}

function decimals() public view virtual override returns (uint8) {
return _decimals;
}
}
17 changes: 13 additions & 4 deletions contracts/token/ERC20/extensions/ERC4626.sol
Expand Up @@ -26,15 +26,24 @@ import "../../../utils/math/Math.sol";
abstract contract ERC4626 is ERC20, IERC4626 {
using Math for uint256;

IERC20Metadata private immutable _asset;
IERC20 private immutable _asset;

/**
* @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777).
*/
constructor(IERC20Metadata asset_) {
constructor(IERC20 asset_) {
_asset = asset_;
}

/** @dev See {IERC4626-asset}. */
function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) {
try IERC20Metadata(address(_asset)).decimals() returns (uint8 value) {
Amxx marked this conversation as resolved.
Show resolved Hide resolved
return value;
} catch {
return super.decimals();
}
}

/** @dev See {IERC4626-asset}. */
function asset() public view virtual override returns (address) {
return address(_asset);
Expand Down Expand Up @@ -153,7 +162,7 @@ abstract contract ERC4626 is ERC20, IERC4626 {
uint256 supply = totalSupply();
return
(assets == 0 || supply == 0)
? assets.mulDiv(10**decimals(), 10**_asset.decimals(), rounding)
? assets.mulDiv(10**decimals(), 10**ERC4626.decimals(), rounding)
Amxx marked this conversation as resolved.
Show resolved Hide resolved
: assets.mulDiv(supply, totalAssets(), rounding);
}

Expand All @@ -164,7 +173,7 @@ abstract contract ERC4626 is ERC20, IERC4626 {
uint256 supply = totalSupply();
return
(supply == 0)
? shares.mulDiv(10**_asset.decimals(), 10**decimals(), rounding)
? shares.mulDiv(10**ERC4626.decimals(), 10**decimals(), rounding)
: shares.mulDiv(totalAssets(), supply, rounding);
}

Expand Down
12 changes: 11 additions & 1 deletion test/token/ERC20/extensions/ERC4626.test.js
Expand Up @@ -3,6 +3,7 @@ const { expect } = require('chai');

const ERC20DecimalsMock = artifacts.require('ERC20DecimalsMock');
const ERC4626Mock = artifacts.require('ERC4626Mock');
const ERC4626DecimalMock = artifacts.require('ERC4626DecimalMock');

const parseToken = (token) => (new BN(token)).mul(new BN('1000000000000'));
const parseShare = (share) => (new BN(share)).mul(new BN('1000000000000000000'));
Expand All @@ -15,7 +16,7 @@ contract('ERC4626', function (accounts) {

beforeEach(async function () {
this.token = await ERC20DecimalsMock.new(name, symbol, 12);
this.vault = await ERC4626Mock.new(this.token.address, name + ' Vault', symbol + 'V');
this.vault = await ERC4626DecimalMock.new(this.token.address, name + ' Vault', symbol + 'V', 18);

await this.token.mint(holder, web3.utils.toWei('100'));
await this.token.approve(this.vault.address, constants.MAX_UINT256, { from: holder });
Expand All @@ -25,9 +26,18 @@ contract('ERC4626', function (accounts) {
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.decimals()).to.be.bignumber.equal('18');
expect(await this.vault.asset()).to.be.equal(this.token.address);
});

it('inherit decimals if from asset', async function () {
for (const decimals of [ 0, 9, 12, 18, 36 ].map(web3.utils.toBN)) {
const token = await ERC20DecimalsMock.new('', '', decimals);
const vault = await ERC4626Mock.new(token.address, '', '');
expect(await vault.decimals()).to.be.bignumber.equal(decimals);
}
});

describe('empty vault: no assets & no shares', function () {
it('status', async function () {
expect(await this.vault.totalAssets()).to.be.bignumber.equal('0');
Expand Down