diff --git a/contracts/BackedTokenImplementationWithMultiplierAndAutoFeeAccrual.sol b/contracts/BackedTokenImplementationWithMultiplierAndAutoFeeAccrual.sol new file mode 100644 index 0000000..adcb373 --- /dev/null +++ b/contracts/BackedTokenImplementationWithMultiplierAndAutoFeeAccrual.sol @@ -0,0 +1,550 @@ +/** + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2021-2022 Backed Finance AG + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Disclaimer and Terms of Use + * + * These ERC-20 tokens have not been registered under the U.S. Securities Act of 1933, as + * amended or with any securities regulatory authority of any State or other jurisdiction + * of the United States and (i) may not be offered, sold or delivered within the United States + * to, or for the account or benefit of U.S. Persons, and (ii) may be offered, sold or otherwise + * delivered at any time only to transferees that are Non-United States Persons (as defined by + * the U.S. Commodities Futures Trading Commission). + * For more information and restrictions please refer to the issuer's [Website](https://www.backedassets.fi/legal-documentation) + */ + +pragma solidity 0.8.9; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "./ERC20PermitDelegateTransferWithMultiplier.sol"; +import "./SanctionsList.sol"; + +/** + * @dev + * + * This token contract is following the ERC20 standard. + * It inherits ERC20PermitDelegateTransferWithMultiplier.sol, which extends the basic ERC20 to also allow permit, delegateTransfer and delegateTransferShares EIP-712 functionality. + * Enforces Sanctions List via the Chainalysis standard interface. + * The contract contains four roles: + * - A minter, that can mint new tokens. + * - A burner, that can burn its own tokens, or contract's tokens. + * - A pauser, that can pause or restore all transfers in the contract. + * - A multiplierUpdated, that can update value of a multiplier. + * - An owner, that can set the four above, and also the sanctionsList pointer. + * The owner can also set who can use the EIP-712 functionality, either specific accounts via a whitelist, or everyone. + * + */ + +contract BackedTokenImplementationWithMultiplierAndAutoFeeAccrual is + OwnableUpgradeable, + ERC20PermitDelegateTransferWithMultiplier +{ + string public constant VERSION = "1.1.0"; + + // Roles: + address public minter; + address public burner; + address public pauser; + + // EIP-712 Delegate Functionality: + bool public delegateMode; + mapping(address => bool) public delegateWhitelist; + + // Pause: + bool public isPaused; + + // SanctionsList: + SanctionsList public sanctionsList; + + // Terms: + string public terms; + + // V2 + + // Roles: + address public multiplierUpdater; + + // Management Fee + uint256 public lastTimeFeeApplied; + uint256 public feePerPeriod; // in 1e18 precision + uint256 public periodLength; + + // Events: + event NewMinter(address indexed newMinter); + event NewBurner(address indexed newBurner); + event NewPauser(address indexed newPauser); + event NewMultiplierUpdater(address indexed newMultiplierUpdater); + event NewSanctionsList(address indexed newSanctionsList); + event DelegateWhitelistChange( + address indexed whitelistAddress, + bool status + ); + event DelegateModeChange(bool delegateMode); + event PauseModeChange(bool pauseMode); + event NewTerms(string newTerms); + + modifier allowedDelegate() { + require( + delegateMode || delegateWhitelist[_msgSender()], + "BackedToken: Unauthorized delegate" + ); + _; + } + + modifier updateMultiplier() { + (uint256 newMultiplier, uint256 periodsPassed) = getCurrentMultiplier(); + lastTimeFeeApplied = lastTimeFeeApplied + periodLength * periodsPassed; + if (multiplier() != newMultiplier) { + _updateMultiplier(newMultiplier); + } + _; + } + + // constructor, call initializer to lock the implementation instance. + constructor() { + initialize( + "Backed Token Implementation", + "BTI", + block.timestamp, + 24 * 3600 + ); + } + + function initialize( + string memory name_, + string memory symbol_, + uint256 firstFeeAccrualTime_, + uint256 periodLength_ + ) public initializer { + __ERC20_init(name_, symbol_); + __Ownable_init(); + _buildDomainSeparator(); + _setTerms("https://www.backedassets.fi/legal-documentation"); // Default Terms + lastTimeFeeApplied = firstFeeAccrualTime_; + periodLength = periodLength_; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf( + address account + ) public view virtual override returns (uint256) { + (uint256 multiplier, ) = getCurrentMultiplier(); + return (sharesOf(account) * multiplier) / 1e18; + } + + /** + * @dev Retrieves most up to date value of multiplier + * + * Note Probably it should be renamed into multiplier and allow getting stored version of multiplier + * via getStoredMultiplier() method + */ + function getCurrentMultiplier() + public + view + virtual + returns (uint256 newMultiplier, uint256 periodsPassed) + { + periodsPassed = (block.timestamp - lastTimeFeeApplied) / periodLength; + newMultiplier = multiplier(); + if (feePerPeriod >= 0) { + for (uint256 index = 0; index < periodsPassed; index++) { + newMultiplier = (newMultiplier * (1e18 - feePerPeriod)) / 1e18; + } + } + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + (uint256 multiplier, ) = getCurrentMultiplier(); + return _totalShares * multiplier / 1e18; + } + + /** + * @dev Update allowance with a signed permit. Allowed only if + * the sender is whitelisted, or the delegateMode is set to true + * + * @param owner Token owner's address (Authorizer) + * @param spender Spender's address + * @param value Amount of allowance + * @param deadline Expiration time, seconds since the epoch + * @param v v part of the signature + * @param r r part of the signature + * @param s s part of the signature + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public override allowedDelegate { + super.permit(owner, spender, value, deadline, v, r, s); + } + + /** + * @dev Perform an intended transfer on one account's behalf, from another account, + * who actually pays fees for the transaction. Allowed only if the sender + * is whitelisted, or the delegateMode is set to true + * + * @param owner The account that provided the signature and from which the tokens will be taken + * @param to The account that will receive the tokens + * @param value The amount of tokens to transfer + * @param deadline Expiration time, seconds since the epoch + * @param v v part of the signature + * @param r r part of the signature + * @param s s part of the signature + */ + function delegatedTransfer( + address owner, + address to, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public override allowedDelegate updateMultiplier { + super.delegatedTransfer(owner, to, value, deadline, v, r, s); + } + + /** + * @dev Perform an intended shares transfer on one account's behalf, from another account, + * who actually pays fees for the transaction. Allowed only if the sender + * is whitelisted, or the delegateMode is set to true + * + * @param owner The account that provided the signature and from which the tokens will be taken + * @param to The account that will receive the tokens + * @param value The amount of token shares to transfer + * @param deadline Expiration time, seconds since the epoch + * @param v v part of the signature + * @param r r part of the signature + * @param s s part of the signature + */ + function delegatedTransferShares( + address owner, + address to, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override updateMultiplier { + super.delegatedTransferShares(owner, to, value, deadline, v, r, s); + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer( + address to, + uint256 amount + ) public virtual override updateMultiplier returns (bool) { + return super.transfer(to, amount); + } + + /** + * @dev Transfers underlying shares to destination account + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `sharesAmount`. + */ + function transferShares( + address to, + uint256 sharesAmount + ) public virtual override updateMultiplier returns (bool) { + return super.transferShares(to, sharesAmount); + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual override updateMultiplier returns (bool) { + return this.transferFrom(from, to, amount); + } + + /** + * @dev Function to mint tokens. Allowed only for minter + * + * @param account The address that will receive the minted tokens + * @param amount The amount of tokens to mint + */ + function mint( + address account, + uint256 amount + ) external virtual updateMultiplier { + require(_msgSender() == minter, "BackedToken: Only minter"); + uint256 sharesAmount = getSharesByUnderlyingAmount(amount); + _mintShares(account, sharesAmount); + } + + /** + * @dev Function to burn tokens. Allowed only for burner. The burned tokens + * must be from the burner (msg.sender), or from the contract itself + * + * @param account The account from which the tokens will be burned + * @param amount The amount of tokens to be burned + */ + function burn(address account, uint256 amount) external updateMultiplier { + require(_msgSender() == burner, "BackedToken: Only burner"); + require( + account == _msgSender() || account == address(this), + "BackedToken: Cannot burn account" + ); + uint256 sharesAmount = getSharesByUnderlyingAmount(amount); + _burnShares(account, sharesAmount); + } + + /** + * @dev Function to set the new fee. Allowed only for owner + * + * @param newFeePerPeriod The new fee per period value + */ + function updateFeePerPeriod(uint256 newFeePerPeriod) external onlyOwner { + feePerPeriod = newFeePerPeriod; + } + + /** + * @dev Function to set the pause in order to block or restore all + * transfers. Allowed only for pauser + * + * Emits a { PauseModeChange } event + * + * @param newPauseMode The new pause mode + */ + function setPause(bool newPauseMode) external { + require(_msgSender() == pauser, "BackedToken: Only pauser"); + isPaused = newPauseMode; + emit PauseModeChange(newPauseMode); + } + + /** + * @dev Function to change the contract minter. Allowed only for owner + * + * Emits a { NewMinter } event + * + * @param newMinter The address of the new minter + */ + function setMinter(address newMinter) external onlyOwner { + minter = newMinter; + emit NewMinter(newMinter); + } + + /** + * @dev Function to change the contract burner. Allowed only for owner + * + * Emits a { NewBurner } event + * + * @param newBurner The address of the new burner + */ + function setBurner(address newBurner) external onlyOwner { + burner = newBurner; + emit NewBurner(newBurner); + } + + /** + * @dev Function to change the contract pauser. Allowed only for owner + * + * Emits a { NewPauser } event + * + * @param newPauser The address of the new pauser + */ + function setPauser(address newPauser) external onlyOwner { + pauser = newPauser; + emit NewPauser(newPauser); + } + + /** + * @dev Function to change the contract multiplier updater. Allowed only for owner + * + * Emits a { NewMultiplierUpdater } event + * + * @param newMultiplierUpdater The address of the new multiplier updater + */ + function setMultiplierUpdater(address newMultiplierUpdater) external onlyOwner { + multiplierUpdater = newMultiplierUpdater; + emit NewMultiplierUpdater(newMultiplierUpdater); + } + + /** + * @dev Function to change the contract multiplier, only if oldMultiplier did not change in the meantime. Allowed only for owner + * + * Emits a { MultiplierChanged } event + * + * @param newMultiplier New multiplier value + */ + function updateMultiplierValue( + uint256 newMultiplier, + uint256 oldMultiplier + ) external updateMultiplier { + require( + _msgSender() == multiplierUpdater, + "BackedToken: Only multiplier updater" + ); + require( + multiplier() == oldMultiplier, + "BackedToken: Multiplier changed in the meantime" + ); + _updateMultiplier(newMultiplier); + } + + /** + * @dev Function to change the contract Senctions List. Allowed only for owner + * + * Emits a { NewSanctionsList } event + * + * @param newSanctionsList The address of the new Senctions List following the Chainalysis standard + */ + function setSanctionsList(address newSanctionsList) external onlyOwner { + // Check the proposed sanctions list contract has the right interface: + require( + !SanctionsList(newSanctionsList).isSanctioned(address(this)), + "BackedToken: Wrong List interface" + ); + + sanctionsList = SanctionsList(newSanctionsList); + emit NewSanctionsList(newSanctionsList); + } + + /** + * @dev EIP-712 Function to change the delegate status of account. + * Allowed only for owner + * + * Emits a { DelegateWhitelistChange } event + * + * @param whitelistAddress The address for which to change the delegate status + * @param status The new delegate status + */ + function setDelegateWhitelist( + address whitelistAddress, + bool status + ) external onlyOwner { + delegateWhitelist[whitelistAddress] = status; + emit DelegateWhitelistChange(whitelistAddress, status); + } + + /** + * @dev EIP-712 Function to change the contract delegate mode. Allowed + * only for owner + * + * Emits a { DelegateModeChange } event + * + * @param _delegateMode The new delegate mode for the contract + */ + function setDelegateMode(bool _delegateMode) external onlyOwner { + delegateMode = _delegateMode; + + emit DelegateModeChange(_delegateMode); + } + + /** + * @dev Function to change the contract terms. Allowed only for owner + * + * Emits a { NewTerms } event + * + * @param newTerms A string with the terms. Usually a web or IPFS link. + */ + function setTerms(string memory newTerms) external onlyOwner { + _setTerms(newTerms); + } + + // Implement setTerms, to allow also to use from initializer: + function _setTerms(string memory newTerms) internal virtual { + terms = newTerms; + emit NewTerms(newTerms); + } + + // Implement the pause and SanctionsList functionality before transfer: + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override { + // Check not paused: + require(!isPaused, "BackedToken: token transfer while paused"); + + // Check Sanctions List, but do not prevent minting burning: + if (from != address(0) && to != address(0)) { + require( + !sanctionsList.isSanctioned(from), + "BackedToken: sender is sanctioned" + ); + require( + !sanctionsList.isSanctioned(to), + "BackedToken: receiver is sanctioned" + ); + } + + super._beforeTokenTransfer(from, to, amount); + } + + // Implement the SanctionsList functionality for spender: + function _spendAllowance( + address owner, + address spender, + uint256 amount + ) internal virtual override { + require( + !sanctionsList.isSanctioned(spender), + "BackedToken: spender is sanctioned" + ); + + super._spendAllowance(owner, spender, amount); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} diff --git a/contracts/ERC20UpgradeableWithMultiplier.sol b/contracts/ERC20UpgradeableWithMultiplier.sol new file mode 100644 index 0000000..33413eb --- /dev/null +++ b/contracts/ERC20UpgradeableWithMultiplier.sol @@ -0,0 +1,497 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20UpgradeableWithMultiplier is Initializable, ContextUpgradeable, IERC20Upgradeable, IERC20MetadataUpgradeable { + mapping(address => uint256) private _shares; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 internal _totalShares; + + string private _name; + string private _symbol; + + /** + * @dev Defines ratio between a single share of a token to balance of a token. + * Defined in 1e18 precision. + * + */ + uint256 private _multiplier; + + /** + * @dev Emitted when `value` token shares are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event TransferShares(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when multiplier value is updated + */ + event MultiplierUpdated(uint256 value); + + /** + * @dev Sets the values for {name} and {symbol}. + * + * The default value of {decimals} is 18. To select a different value for + * {decimals} you should overload it. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + function __ERC20_init(string memory name_, string memory symbol_) internal onlyInitializing { + __ERC20_init_unchained(name_, symbol_); + } + + function __ERC20_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing { + _name = name_; + _symbol = symbol_; + _multiplier = 1e18; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the value {ERC20} uses, unless this function is + * overridden; + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual override returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalShares * _multiplier / 1e18; + } + + /** + * @dev Returns ratio of shares to underlying amount in 18 decimals precision + */ + function multiplier() public view virtual returns (uint256) { + return _multiplier; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _shares[account] * _multiplier / 1e18; + } + + /** + * @dev Returns amount of shares owned by given account + */ + function sharesOf(address account) public view virtual returns (uint256) { + return _shares[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address to, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + /** + * @dev Transfers underlying shares to destination account + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `sharesAmount`. + */ + function transferShares(address to, uint256 sharesAmount) public virtual returns (bool) { + address owner = _msgSender(); + _transferShares(owner, to, sharesAmount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, _allowances[owner][spender] + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + address owner = _msgSender(); + uint256 currentAllowance = _allowances[owner][spender]; + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @return the amount of shares that corresponds to `_underlyingAmount` underlying amount. + */ + function getSharesByUnderlyingAmount(uint256 _underlyingAmount) public view returns (uint256) { + return _underlyingAmount + * 1e18 + / _multiplier; + } + + /** + * @return the amount of underlying that corresponds to `_sharesAmount` token shares. + */ + function getUnderlyingAmountByShares(uint256 _sharesAmount) public view returns (uint256) { + return _sharesAmount + * _multiplier + / 1e18; + } + + /** + * @notice Moves `_sharesAmount` shares from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `from` must hold at least `_sharesAmount` shares. + * - the contract must not be paused. + */ + function _transferShares(address from, address to, uint256 _sharesAmount) internal { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + uint256 amount = getUnderlyingAmountByShares(_sharesAmount); + _beforeTokenTransfer(from, to, amount); + + uint256 currentSenderShares = _shares[from]; + require(currentSenderShares >= _sharesAmount , "ERC20: transfer amount exceeds balance"); + + unchecked { + _shares[from] = currentSenderShares - (_sharesAmount); + } + _shares[to] = _shares[to] + (_sharesAmount); + + emit Transfer(from, to, amount); + emit TransferShares(from, to, _sharesAmount); + + _afterTokenTransfer(from, to, amount); + } + + /** + * @dev Moves `amount` of tokens from `sender` to `recipient`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * Emits a {TransferShares} event. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + */ + function _transfer( + address from, + address to, + uint256 amount + ) internal virtual { + uint256 _sharesToTransfer = getSharesByUnderlyingAmount(amount); + _transferShares(from, to, _sharesToTransfer); + } + + /** @dev Creates `amount` token shares and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * Emits a {TransferShares} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mintShares(address account, uint256 sharesAmount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + uint256 amount = getUnderlyingAmountByShares(sharesAmount); + + _beforeTokenTransfer(address(0), account, amount); + + _totalShares += sharesAmount; + _shares[account] += sharesAmount; + emit Transfer(address(0), account, amount); + emit TransferShares(address(0), account, sharesAmount); + + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * Emits a {TransferShares} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `sharesAmount` token shares. + */ + function _burnShares(address account, uint256 sharesAmount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + uint256 amount = getUnderlyingAmountByShares(sharesAmount); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _shares[account]; + require(accountBalance >= sharesAmount, "ERC20: burn amount exceeds balance"); + unchecked { + _shares[account] = accountBalance - sharesAmount; + } + _totalShares -= sharesAmount; + + emit Transfer(account, address(0), amount); + emit TransferShares(account, address(0), sharesAmount); + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Spend `amount` form the allowance of `owner` toward `spender`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance( + address owner, + address spender, + uint256 amount + ) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= amount, "ERC20: insufficient allowance"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + /** + * @dev Updates currently stored multiplier with a new value + * + * Emit an {MultiplierUpdated} event. + */ + function _updateMultiplier( + uint256 newMultiplier + ) internal virtual { + _multiplier = newMultiplier; + emit MultiplierUpdated(newMultiplier); + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[44] private __gap; +} diff --git a/contracts/WrappedBackedToken.sol b/contracts/WrappedBackedToken.sol new file mode 100644 index 0000000..67f3b49 --- /dev/null +++ b/contracts/WrappedBackedToken.sol @@ -0,0 +1,222 @@ +/** + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2021-2022 Backed Finance AG + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Disclaimer and Terms of Use + * + * These ERC-20 tokens have not been registered under the U.S. Securities Act of 1933, as + * amended or with any securities regulatory authority of any State or other jurisdiction + * of the United States and (i) may not be offered, sold or delivered within the United States + * to, or for the account or benefit of U.S. Persons, and (ii) may be offered, sold or otherwise + * delivered at any time only to transferees that are Non-United States Persons (as defined by + * the U.S. Commodities Futures Trading Commission). + * For more information and restrictions please refer to the issuer's [Website](https://www.backedassets.fi/legal-documentation) + */ + +pragma solidity 0.8.9; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "./SanctionsList.sol"; + +/** + * @dev + * + * This token contract is following the ERC20 standard. + * It inherits ERC4626Upgradeable, which extends the basic ERC20 to be a representation of changing underlying token. + * Enforces Sanctions List via the Chainalysis standard interface. + * The contract contains one role: + * - A pauser, that can pause or restore all transfers in the contract. + * - An owner, that can set the above, and also the sanctionsList pointer. + * The owner can also set who can use the EIP-712 functionality, either specific accounts via a whitelist, or everyone. + * + */ + +contract WrappedBackedToken is OwnableUpgradeable, ERC4626Upgradeable { + string constant public VERSION = "1.1.0"; + + // Roles: + address public pauser; + + // EIP-712 Delegate Functionality: + bool public delegateMode; + mapping(address => bool) public delegateWhitelist; + + // Pause: + bool public isPaused; + + // SanctionsList: + SanctionsList public sanctionsList; + + // Terms: + string public terms; + + // Events: + event NewPauser(address indexed newPauser); + event NewSanctionsList(address indexed newSanctionsList); + event DelegateWhitelistChange(address indexed whitelistAddress, bool status); + event DelegateModeChange(bool delegateMode); + event PauseModeChange(bool pauseMode); + event NewTerms(string newTerms); + + modifier allowedDelegate { + require(delegateMode || delegateWhitelist[_msgSender()], "BackedToken: Unauthorized delegate"); + _; + } + + + // constructor, call initializer to lock the implementation instance. + constructor () { + initialize("Wrapped Backed Token Implementation", "wBTI", address(0x0000000000000000000000000000000000000000)); + } + + function initialize(string memory name_, string memory symbol_, address underlying_) public initializer { + __ERC20_init(name_, symbol_); + __ERC4626_init(IERC20Upgradeable(underlying_)); + __Ownable_init(); + _setTerms("https://www.backedassets.fi/legal-documentation"); // Default Terms + } + + /** + * @dev Function to set the pause in order to block or restore all + * transfers. Allowed only for pauser + * + * Emits a { PauseModeChange } event + * + * @param newPauseMode The new pause mode + */ + function setPause(bool newPauseMode) external { + require(_msgSender() == pauser, "BackedToken: Only pauser"); + isPaused = newPauseMode; + emit PauseModeChange(newPauseMode); + } + + /** + * @dev Function to change the contract pauser. Allowed only for owner + * + * Emits a { NewPauser } event + * + * @param newPauser The address of the new pauser + */ + function setPauser(address newPauser) external onlyOwner { + pauser = newPauser; + emit NewPauser(newPauser); + } + + /** + * @dev Function to change the contract Senctions List. Allowed only for owner + * + * Emits a { NewSanctionsList } event + * + * @param newSanctionsList The address of the new Senctions List following the Chainalysis standard + */ + function setSanctionsList(address newSanctionsList) external onlyOwner { + // Check the proposed sanctions list contract has the right interface: + require(!SanctionsList(newSanctionsList).isSanctioned(address(this)), "BackedToken: Wrong List interface"); + + sanctionsList = SanctionsList(newSanctionsList); + emit NewSanctionsList(newSanctionsList); + } + + + /** + * @dev EIP-712 Function to change the delegate status of account. + * Allowed only for owner + * + * Emits a { DelegateWhitelistChange } event + * + * @param whitelistAddress The address for which to change the delegate status + * @param status The new delegate status + */ + function setDelegateWhitelist(address whitelistAddress, bool status) external onlyOwner { + delegateWhitelist[whitelistAddress] = status; + emit DelegateWhitelistChange(whitelistAddress, status); + } + + /** + * @dev EIP-712 Function to change the contract delegate mode. Allowed + * only for owner + * + * Emits a { DelegateModeChange } event + * + * @param _delegateMode The new delegate mode for the contract + */ + function setDelegateMode(bool _delegateMode) external onlyOwner { + delegateMode = _delegateMode; + + emit DelegateModeChange(_delegateMode); + } + + /** + * @dev Function to change the contract terms. Allowed only for owner + * + * Emits a { NewTerms } event + * + * @param newTerms A string with the terms. Usually a web or IPFS link. + */ + function setTerms(string memory newTerms) external onlyOwner { + _setTerms(newTerms); + } + + // Implement setTerms, tp allow also to use from initializer: + function _setTerms(string memory newTerms) internal virtual { + terms = newTerms; + emit NewTerms(newTerms); + } + + // Implement the pause and SanctionsList functionality before transfer: + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override { + // Check not paused: + require(!isPaused, "BackedToken: token transfer while paused"); + + // Check Sanctions List, but do not prevent minting burning: + if (from != address(0) && to != address(0)) { + require(!sanctionsList.isSanctioned(from), "BackedToken: sender is sanctioned"); + require(!sanctionsList.isSanctioned(to), "BackedToken: receiver is sanctioned"); + } + + super._beforeTokenTransfer(from, to, amount); + } + + // Implement the SanctionsList functionality for spender: + function _spendAllowance( + address owner, + address spender, + uint256 amount + ) internal virtual override { + require(!sanctionsList.isSanctioned(spender), "BackedToken: spender is sanctioned"); + + super._spendAllowance(owner, spender, amount); + } + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} diff --git a/test/BackedTokenImplementationWithMultiplierAndAutoFeeAccrual.ts b/test/BackedTokenImplementationWithMultiplierAndAutoFeeAccrual.ts new file mode 100644 index 0000000..7863587 --- /dev/null +++ b/test/BackedTokenImplementationWithMultiplierAndAutoFeeAccrual.ts @@ -0,0 +1,245 @@ +import { ProxyAdmin__factory } from '../typechain/factories/ProxyAdmin__factory'; +import { ProxyAdmin } from '../typechain/ProxyAdmin'; +import { BackedTokenImplementationWithMultiplierAndAutoFeeAccrual__factory } from '../typechain/factories/BackedTokenImplementationWithMultiplierAndAutoFeeAccrual__factory'; +import { BackedTokenImplementationWithMultiplierAndAutoFeeAccrual } from '../typechain/BackedTokenImplementationWithMultiplierAndAutoFeeAccrual'; +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; + +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumber, Signer } from "ethers"; +import { + BackedTokenProxy__factory, + SanctionsListMock__factory, + // eslint-disable-next-line node/no-missing-import +} from "../typechain"; +import { cacheBeforeEach } from "./helpers"; +import Decimal from 'decimal.js'; + +type SignerWithAddress = { + signer: Signer; + address: string; +}; + +// BackedTokenImplementationWithMultiplierAndAutoFeeAccrual specifications +// Vast majority of comparisons are done with adjustment for precision of calculations, thus we are rather comparing difference of values, +// rather than values themselves +describe.only("BackedTokenImplementationWithMultiplierAndAutoFeeAccrual", function () { + const accrualPeriodLength = 24 * 3600; + const annualFee = 0.5; + const multiplierAdjustmentPerPeriod = nthRoot(annualFee, 365).mul(Decimal.pow(10, 18)); + const baseFeePerPeriod = Decimal.pow(10, 18).minus(multiplierAdjustmentPerPeriod).toFixed(0); + const baseTime = 2_000_000_000; + + // General config: + let token: BackedTokenImplementationWithMultiplierAndAutoFeeAccrual; + let proxyAdmin: ProxyAdmin; + let accounts: Signer[]; + + let owner: SignerWithAddress; + let actor: SignerWithAddress; + + cacheBeforeEach(async () => { + accounts = await ethers.getSigners(); + + const getSigner = async (index: number): Promise => ({ + signer: accounts[index], + address: await accounts[index].getAddress(), + }); + + owner = await getSigner(0); + actor = await getSigner(1); + + await helpers.time.setNextBlockTimestamp(baseTime); + + const tokenImplementationFactory = new BackedTokenImplementationWithMultiplierAndAutoFeeAccrual__factory(owner.signer); + const tokenImplementation = await tokenImplementationFactory.deploy(); + const proxyAdminFactory = new ProxyAdmin__factory(owner.signer) + proxyAdmin = await proxyAdminFactory.deploy(); + const tokenProxy = await new BackedTokenProxy__factory(owner.signer).deploy(tokenImplementation.address, proxyAdmin.address, tokenImplementation.interface.encodeFunctionData( + 'initialize', + [ + "Backed Test Token", + "bTest", + baseTime, + accrualPeriodLength + ] + )); + token = BackedTokenImplementationWithMultiplierAndAutoFeeAccrual__factory.connect(tokenProxy.address, owner.signer); + await token.setMinter(owner.address); + await token.setBurner(owner.address); + await token.setPauser(owner.address); + await token.setMultiplierUpdater(owner.address); + await token.setSanctionsList((await new SanctionsListMock__factory(owner.signer).deploy()).address); + await token.updateFeePerPeriod(baseFeePerPeriod); + + }); + describe('#updateModifier', () => { + describe('when time moved by 365 days forward', () => { + const periodsPassed = 365; + const baseMintedAmount = ethers.BigNumber.from(10).pow(18); + let mintedShares: BigNumber; + cacheBeforeEach(async () => { + await token.mint(owner.address, baseMintedAmount); + mintedShares = await token.sharesOf(owner.address); + await helpers.time.setNextBlockTimestamp(baseTime + periodsPassed * accrualPeriodLength); + await helpers.mine() + }) + + describe('#updateMultiplierValue', () => { + it('Should update stored multiplier value', async () => { + const { newMultiplier: currentMultiplier } = await token.getCurrentMultiplier(); + const newMultiplierValue = currentMultiplier.div(2); + await token.updateMultiplierValue(newMultiplierValue, currentMultiplier) + expect(await token.multiplier()).to.be.equal(newMultiplierValue); + expect(await token.lastTimeFeeApplied()).to.be.equal(baseTime + periodsPassed * accrualPeriodLength); + }); + it('Should reject update, if wrong past value was passed', async () => { + await expect(token.updateMultiplierValue(0, 1)).to.be.reverted; + }); + it('Should reject update, if wrong account is used', async () => { + const { newMultiplier: currentMultiplier } = await token.getCurrentMultiplier(); + await expect(token.connect(actor.signer).updateMultiplierValue(1, currentMultiplier)).to.be.reverted + }); + }); + + describe('#balanceOf', () => { + it('Should decrease balance of the user by fee accrued in 365 days', async () => { + expect((await token.balanceOf(owner.address)).sub(baseMintedAmount.mul(annualFee * 100).div(100)).abs()).to.lte( + BigNumber.from(10).pow(3) + ) + }) + }); + + describe('#totalSupply', () => { + it('Should decrease total supply of the token by the fee accrued in 365 days', async () => { + expect((await token.totalSupply()).sub(baseMintedAmount.mul(annualFee * 100).div(100)).abs()).to.lte( + BigNumber.from(10).pow(3) + ) + }) + }); + + describe('#transfer', () => { + it('Should not allow transfer of previous balance of user', async () => { + await expect(token.transfer(actor.address, baseMintedAmount)).to.be.reverted; + }) + it('Should allow transfer of current balance of user', async () => { + await expect(token.transfer(actor.address, (await token.balanceOf(actor.address)))).to.not.be.reverted; + }) + }); + describe('#transferShares', () => { + it('Should allow transfer of shares of user', async () => { + await expect(token.transfer(actor.address, mintedShares)).to.be.reverted; + }) + }); + describe('#mint', () => { + const newlyMintedTokens = ethers.BigNumber.from(10).pow(18) + cacheBeforeEach(async () => { + await token.mint(actor.address, newlyMintedTokens); + }) + it('Should mint requested number of tokens', async () => { + expect((await token.balanceOf(actor.address)).sub(newlyMintedTokens).abs()).to.be.lte(1); + }) + it('Should mint number of shares according to multiplier', async () => { + expect(await token.sharesOf(actor.address)).to.be.eq(newlyMintedTokens.mul(ethers.BigNumber.from(10).pow(18)).div((await token.getCurrentMultiplier()).newMultiplier)); + }) + }); + }) + }) + describe('#transferShares', () => { + const baseMintedAmount = ethers.BigNumber.from(10).pow(18); + cacheBeforeEach(async () => { + await token.mint(owner.address, baseMintedAmount); + }) + describe('When transfering shares to another account', () => { + const sharesToTransfer = ethers.BigNumber.from(10).pow(18); + const subject = () => token.transferShares(actor.address, sharesToTransfer) + let userBalance: BigNumber; + cacheBeforeEach(async () => { + userBalance = await token.getUnderlyingAmountByShares(sharesToTransfer); + }) + it('Should move requested shares of tokens', async () => { + await subject(); + expect((await token.sharesOf(actor.address))).to.be.eq(sharesToTransfer); + }) + it('Should increase balance of destination wallet', async () => { + await subject(); + expect((await token.balanceOf(actor.address))).to.be.eq(userBalance); + }) + }) + }); + describe('#delegatedTransferShares', () => { + const baseMintedAmount = ethers.BigNumber.from(10).pow(18); + cacheBeforeEach(async () => { + await token.mint(owner.address, baseMintedAmount); + }) + describe('When transfering shares from another account', () => { + const sharesToTransfer = ethers.BigNumber.from(10).pow(18); + let signature: string; + let deadline: number; + let nonce: BigNumber; + const subject = async () => { + const sig = ethers.utils.splitSignature(signature) + return token.connect(actor.signer).delegatedTransferShares(owner.address, actor.address, sharesToTransfer, deadline, sig.v, sig.r, sig.s); + } + let userBalance: BigNumber; + cacheBeforeEach(async () => { + userBalance = await token.getUnderlyingAmountByShares(sharesToTransfer); + + deadline = baseTime * 2; + nonce = await token.nonces(owner.address); + const domain = { + name: await token.name(), + version: "1", + chainId: await owner.signer.getChainId(), + verifyingContract: token.address + }; + const types = { + DELEGATED_TRANSFER_SHARES: [ + { + type: 'address', + name: 'owner' + }, + { + type: 'address', + name: 'to' + }, + { + type: 'uint256', + name: 'value' + }, + { + type: 'uint256', + name: 'nonce' + }, + { + type: 'uint256', + name: 'deadline' + } + ] + }; + const msg = { + owner: owner.address, + to: actor.address, + value: sharesToTransfer, + nonce: nonce, + deadline: deadline + }; + + const signer = await ethers.getSigner(owner.address); + signature = await signer._signTypedData(domain, types, msg); + }) + it('Should move requested shares of tokens', async () => { + await subject(); + expect((await token.sharesOf(actor.address))).to.be.eq(sharesToTransfer); + }) + it('Should increase balance of destination wallet', async () => { + await subject(); + expect((await token.balanceOf(actor.address))).to.be.eq(userBalance); + }) + }) + }); +}); +function nthRoot(annualFee: number, n: number) { + return Decimal.pow(1 - annualFee, new Decimal(1).div(n)); +} +