Skip to content

Commit 141130d

Browse files
authored
Inherit asset decimals in ERC4626 (#3639)
1 parent e45b49e commit 141130d

File tree

5 files changed

+104
-8
lines changed

5 files changed

+104
-8
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
* `GovernorCompatibilityBravo`: remove unused `using` statements. ([#3506](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3506))
1010
* `ERC20`: optimize `_transfer`, `_mint` and `_burn` by using `unchecked` arithmetic when possible. ([#3513](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3513))
1111
* `ERC20FlashMint`: add an internal `_flashFee` function for overriding. ([#3551](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3551))
12+
* `ERC4626`: use the same `decimals()` as the underlying asset by default (if available). ([#3639](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3639))
13+
* `ERC4626`: add internal `_initialConvertToShares` and `_initialConvertToAssets` functions to customize empty vaults behavior. ([#3639](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3639))
1214
* `ERC721`: optimize transfers by making approval clearing implicit instead of emitting an event. ([#3481](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3481))
1315
* `ERC721`: optimize burn by making approval clearing implicit instead of emitting an event. ([#3538](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3538))
1416
* `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))
@@ -21,6 +23,10 @@
2123
* `Create2`: optimize address computation by using assembly instead of `abi.encodePacked`. ([#3600](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3600))
2224
* `Clones`: optimized the assembly to use only the scratch space during deployments, and optimized `predictDeterministicAddress` to use lesser operations. ([#3640](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3640))
2325

26+
### Breaking changes
27+
28+
* `ERC4626`: Conversion from shares to assets (and vice-versa) in an empty vault used to consider the possible mismatch between the underlying asset's and the vault's decimals. This initial conversion rate is now set to 1-to-1 irrespective of decimals, which are meant for usability purposes only. The vault now uses the assets decimals by default, so off-chain the numbers should appear the same. Developers overriding the vault decimals to a value that does not match the underlying asset may want to override the `_initialConvertToShares` and `_initialConvertToAssets` to replicate the previous behavior.
29+
2430
### Deprecations
2531

2632
* `EIP712`: Added the file `EIP712.sol` and deprecated `draft-EIP712.sol` since the EIP is no longer a Draft. Developers are encouraged to update their imports. ([#3621](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3621))

contracts/interfaces/README.adoc

+6
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ are useful to interact with third party contracts that implement them.
2424
- {IERC1363}
2525
- {IERC1820Implementer}
2626
- {IERC1820Registry}
27+
- {IERC1822Proxiable}
2728
- {IERC2612}
2829
- {IERC2981}
2930
- {IERC3156FlashLender}
3031
- {IERC3156FlashBorrower}
32+
- {IERC4626}
3133

3234
== Detailed ABI
3335

@@ -41,10 +43,14 @@ are useful to interact with third party contracts that implement them.
4143

4244
{{IERC1820Registry}}
4345

46+
{{IERC1822Proxiable}}
47+
4448
{{IERC2612}}
4549

4650
{{IERC2981}}
4751

4852
{{IERC3156FlashLender}}
4953

5054
{{IERC3156FlashBorrower}}
55+
56+
{{IERC4626}}

contracts/mocks/ERC4626Mock.sol

+39-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ pragma solidity ^0.8.0;
44

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

7-
// mock class using ERC20
87
contract ERC4626Mock is ERC4626 {
98
constructor(
109
IERC20Metadata asset,
@@ -20,3 +19,42 @@ contract ERC4626Mock is ERC4626 {
2019
_burn(account, amount);
2120
}
2221
}
22+
23+
contract ERC4626DecimalMock is ERC4626Mock {
24+
using Math for uint256;
25+
26+
uint8 private immutable _decimals;
27+
28+
constructor(
29+
IERC20Metadata asset,
30+
string memory name,
31+
string memory symbol,
32+
uint8 decimalsOverride
33+
) ERC4626Mock(asset, name, symbol) {
34+
_decimals = decimalsOverride;
35+
}
36+
37+
function decimals() public view virtual override returns (uint8) {
38+
return _decimals;
39+
}
40+
41+
function _initialConvertToShares(uint256 assets, Math.Rounding rounding)
42+
internal
43+
view
44+
virtual
45+
override
46+
returns (uint256 shares)
47+
{
48+
return assets.mulDiv(10**decimals(), 10**super.decimals(), rounding);
49+
}
50+
51+
function _initialConvertToAssets(uint256 shares, Math.Rounding rounding)
52+
internal
53+
view
54+
virtual
55+
override
56+
returns (uint256 assets)
57+
{
58+
return shares.mulDiv(10**super.decimals(), 10**decimals(), rounding);
59+
}
60+
}

contracts/token/ERC20/extensions/ERC4626.sol

+42-6
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,27 @@ import "../../../utils/math/Math.sol";
2626
abstract contract ERC4626 is ERC20, IERC4626 {
2727
using Math for uint256;
2828

29-
IERC20Metadata private immutable _asset;
29+
IERC20 private immutable _asset;
30+
uint8 private immutable _decimals;
3031

3132
/**
3233
* @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777).
3334
*/
34-
constructor(IERC20Metadata asset_) {
35+
constructor(IERC20 asset_) {
36+
uint8 decimals_;
37+
try IERC20Metadata(address(asset_)).decimals() returns (uint8 value) {
38+
decimals_ = value;
39+
} catch {
40+
decimals_ = super.decimals();
41+
}
42+
3543
_asset = asset_;
44+
_decimals = decimals_;
45+
}
46+
47+
/** @dev See {IERC20Metadata-decimals}. */
48+
function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) {
49+
return _decimals;
3650
}
3751

3852
/** @dev See {IERC4626-asset}. */
@@ -153,19 +167,41 @@ abstract contract ERC4626 is ERC20, IERC4626 {
153167
uint256 supply = totalSupply();
154168
return
155169
(assets == 0 || supply == 0)
156-
? assets.mulDiv(10**decimals(), 10**_asset.decimals(), rounding)
170+
? _initialConvertToShares(assets, rounding)
157171
: assets.mulDiv(supply, totalAssets(), rounding);
158172
}
159173

174+
/**
175+
* @dev Internal conversion function (from assets to shares) to apply when the vault is empty.
176+
*
177+
* NOTE: Make sure to keep this function consistent with {_initialConvertToAssets} when overriding it.
178+
*/
179+
function _initialConvertToShares(
180+
uint256 assets,
181+
Math.Rounding /*rounding*/
182+
) internal view virtual returns (uint256 shares) {
183+
return assets;
184+
}
185+
160186
/**
161187
* @dev Internal conversion function (from shares to assets) with support for rounding direction.
162188
*/
163189
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256 assets) {
164190
uint256 supply = totalSupply();
165191
return
166-
(supply == 0)
167-
? shares.mulDiv(10**_asset.decimals(), 10**decimals(), rounding)
168-
: shares.mulDiv(totalAssets(), supply, rounding);
192+
(supply == 0) ? _initialConvertToAssets(shares, rounding) : shares.mulDiv(totalAssets(), supply, rounding);
193+
}
194+
195+
/**
196+
* @dev Internal conversion function (from shares to assets) to apply when the vault is empty.
197+
*
198+
* NOTE: Make sure to keep this function consistent with {_initialConvertToShares} when overriding it.
199+
*/
200+
function _initialConvertToAssets(
201+
uint256 shares,
202+
Math.Rounding /*rounding*/
203+
) internal view virtual returns (uint256 assets) {
204+
return shares;
169205
}
170206

171207
/**

test/token/ERC20/extensions/ERC4626.test.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { expect } = require('chai');
33

44
const ERC20DecimalsMock = artifacts.require('ERC20DecimalsMock');
55
const ERC4626Mock = artifacts.require('ERC4626Mock');
6+
const ERC4626DecimalMock = artifacts.require('ERC4626DecimalMock');
67

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

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

2021
await this.token.mint(holder, web3.utils.toWei('100'));
2122
await this.token.approve(this.vault.address, constants.MAX_UINT256, { from: holder });
@@ -25,9 +26,18 @@ contract('ERC4626', function (accounts) {
2526
it('metadata', async function () {
2627
expect(await this.vault.name()).to.be.equal(name + ' Vault');
2728
expect(await this.vault.symbol()).to.be.equal(symbol + 'V');
29+
expect(await this.vault.decimals()).to.be.bignumber.equal('18');
2830
expect(await this.vault.asset()).to.be.equal(this.token.address);
2931
});
3032

33+
it('inherit decimals if from asset', async function () {
34+
for (const decimals of [ 0, 9, 12, 18, 36 ].map(web3.utils.toBN)) {
35+
const token = await ERC20DecimalsMock.new('', '', decimals);
36+
const vault = await ERC4626Mock.new(token.address, '', '');
37+
expect(await vault.decimals()).to.be.bignumber.equal(decimals);
38+
}
39+
});
40+
3141
describe('empty vault: no assets & no shares', function () {
3242
it('status', async function () {
3343
expect(await this.vault.totalAssets()).to.be.bignumber.equal('0');

0 commit comments

Comments
 (0)