openzeppelin-contracts
openzeppelin-contracts copied to clipboard
ERC4626 enriched with Liabilities and Equity
🧐 Motivation One of the most fundamental tools of accounting is the balance sheet. And the basis of the balance sheet is that Assets = Liabilities + Equity. The two sides must balance. Obviously if there are no liabilities, then Assets = Equity, but this should not be assumed. ERC4626 should include these concepts.
📝 Details In the default ERC4626 implementation, the conversion functions use totalAssets() to compute share values. This is semantically incorrect, from an accounting standpoint; the correct divisor should be totalEquity(). These are no longer the same thing if the vault has liabilities of any kind. Liabilities could include accrued fees, loans to the portfolio, or even senior tranches.
I propose a new abstract contract ERC4626Accounting that fixes this issue, and adds three more nonstandard but useful functions: totalEquity(), totalNAV(), and totalLiabilities(). totalAssets() is redefined to be totalNAV() plus the actual assets in the vault, this allows it to be useful in more real-world implementations. totalLiabilities() and totalNAV() default to zero, and totalEquity() is totalAssets() - totalLiabilities(). The conversion functions have been rewritten to use totalEquity() instead of totalAssets().
Code is pasted below, and ready to be submitted as PR, so happy for constructive feedback. Also happy to write tests and/or docs to get it to completion. Wanted to see if there was any discussion before submitting.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ERC4626.sol";
import "../../../interfaces/IERC20.sol";
import "../../../utils/math/Math.sol";
/**
* @dev Extension of the ERC4626 "Tokenized Vault Standard" as defined in
* https://eips.ethereum.org/EIPS/eip-4626[EIP-4626].
*
* This extension provides basic balance sheet accounting functions that are required
* for many real-world implementations of ERC4626 vaults, including:
* - totalEquity()
* - totalLiabilities() and
* - totalNAV()
*
* This implementation redefines the share price functions to be in terms of totalEquity() instead of totalAssets()
*
* CAUTION: see ERC4626.sol to learn about the donation attack and potential mitigations
*
*/
abstract contract ERC4626Acounting is ERC4626 {
using Math for uint256;
/** @dev See {IERC4626-totalAssets}. */
function totalAssets() public view virtual override returns (uint256) {
return IERC20(asset()).balanceOf(address(this)) + totalNAV();
}
// total asset value of outside investments
function totalNAV() public virtual view returns (uint256) {
return 0;
}
// total liabilities, e.g. accrued fees, or loans to the vault
function totalLiabilities() public view virtual returns (uint256) {
return 0;
}
// total equity value of the shareholders
function totalEquity() public view virtual returns (uint256) {
uint256 assets = totalAssets();
uint256 liabilities = totalLiabilities();
return (liabilities > assets) ? 0 : assets - liabilities;
}
/**
* @dev Internal conversion function (from assets to shares) with support for rounding direction. Usees totalEquity() instead of totalAssets().
*
* Will revert if assets > 0, totalSupply > 0 and totalAssets = 0. That corresponds to a case where any asset
* would represent an infinite amount of shares.
*/
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual override returns (uint256) {
uint256 supply = totalSupply();
return
(assets == 0 || supply == 0)
? _initialConvertToShares(assets, rounding)
: assets.mulDiv(supply, totalEquity(), rounding);
}
/**
* @dev Internal conversion function (from shares to assets) with support for rounding direction. Usees totalEquity() instead of totalAssets().
*/
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) {
uint256 supply = totalSupply();
return
(supply == 0) ? _initialConvertToAssets(shares, rounding) : shares.mulDiv(totalEquity(), supply, rounding);
}
}
Any comments @frangio or @Amxx? Should I submit a PR?
You make good points, but I wouldn't want us to add additional public functions (totalEquity
, etc.) without standardization.
the conversion functions use totalAssets() to compute share values. This is semantically incorrect, from an accounting standpoint
I would also note that even though this might be true "from an accounting standpoint", it is not necessarily the EIP's interpretation of totalAssets
.
I'm interested in exploring something like your proposal here as an internal interface if it helps implement concrete ERC4626 vaults. However, it's very important to us that any proposed changes should be motivated by real-world use cases and concerns, with examples of how it applies to existing projects. I'd love to see some of that around this topic.
Good points, and thanks for the response. As far as I can tell, nobody else has tried to implement a vault that has liabilities, so this issue hasn't really come up for anyone but me. My sense is if we want this standard to be used by firms that are moving real-world assets to the blockchain, we should probably use the terms correctly from an accounting standpoint. This feels a little like when finance people mix up "disk" and "memory" - they're sorta the same, but when you use them interchangeably, it's clear you don't really understand what you're talking about. I regret that I was not able to make these points during the relatively short comment period ERC4626 had, but I think my solution is reasonable in that it does not break the standard, and allows for a safer and more correct implementation. Similar to SafeTransfer not being in ERC20, but. basically everyone uses it. In any case, I'm happy to wait and see if anyone else uses ERC4626 to implement vaults with liabilities, and then re-propose.
Hello, being an experienced accountant in IFRS I believe I can shed some lights in the real world use of this and potential differences to the blockchain. It will be a long text, but I believe it will be of great use for this discussion.
In accounting we usually use the accrual accounting, that is, we register our rights and obligation (assets and liabilities) when they occur, which can be before the actual payment. There are a few exemples that make this clear:
- You sold goods which can be returned in 7 days. You usually have a return rate of 5%. You should register a liability of 5% of your sales, and recognize it as revenue (that goes to equity later) when the 7 days pass.
- You have a debt that accruals interest. Corporate bonds, for example, can have payments every six months. You accrual those interest monthly (affecting your liabilities and P&L) even though the payment is not due yet
- You sell and buy with payment in 30 days. You register those rights and obligations at the time of buying or selling, but the payment is much later.
With accrual accounting you eventualy have a balance sheet with assets and liabilities and the shareholder's share of the company is the difference between the two, which is usually called equity. If you want to value the shareholder's share of the company you should divide the equity by the shareholder shares. It is important to notice that accounting basis of value, with a few exceptions, is the cost, so if we do a that calculation we would have the book value of the shareholder's equity share. The market value could be a lot different.
When we go to the blockchain, even though it could be seen as a huge decentralized ledger, things go a bit different as, usually, the basis for accounting in the blockchain is not accrual, its cash. In that way, accounting in the blockchain is closer to a cash book than to an accounting book. This occurs, between other things, because in the blockchain you usually work with zero trust.
That being said, the payment splitter contract, in my point of view, should only be used to split payments after all things considered. It is the same that happens after a company declares dividends for example. When a company declares dividends it takes the company's P&L and apply a percentage, after constituting reserves. The resulting dividends payable then goes through the same logic of payment splitter, considering amount of shares, and not shares value.
So, if I had any logic to reduce shareholder's payments, I would do that in another contract, and send the payment splitter only the amount due.
Thanks @lhemerly for the comment, but I'm not sure thinking of ERC4626 vaults as mere payment splitters is the right frame. Many vaults (think Yearn, etc) have ongoing investments and redemptions, so the concept of having the accounting in a separate contract and sending the payment splitter the amount due seems over complicated, and is not in fact how many implementations work right now. If you look at the code above, it's pretty straightforward, and seems to be supported by your description of accrual accounting.