diff --git a/EIPS/eip-5252.md b/EIPS/eip-5252.md new file mode 100644 index 0000000000000..d2fc038b4246d --- /dev/null +++ b/EIPS/eip-5252.md @@ -0,0 +1,231 @@ +--- +eip: 5252 +title: Account-bound Finance +description: An EIP-5114 extension that aids in preventing arbitrary loss of funds +author: Hyungsuk Kang (@hskang9), Viktor Pernjek (@smuxx) +discussions-to: https://ethereum-magicians.org/t/pr-5252-discussion-account-bound-finance/10027 +status: Draft +type: Standards Track +category: ERC +created: 2022-06-29 +requires: 20, 721, 1155, 5114 +--- + +## Abstract +This EIP proposes a form of smart contract design pattern and a new type of account abstraction on how one's finance should be managed, ensuring transparency of managing investments and protection with self-sovereignty even from its financial operators. This EIP enables greater self-sovereignty of one's assets using a personal finance contract for each individual. The seperation between an investor's funds and the operation fee is clearly specified in the personal smart contract, so investors can ensure safety from arbitrary loss of funds by the operating team's control. + +This EIP extends [EIP-5114](./eip-5114.md) to further enable transferring fund to other accounts for mobility between managing multiple wallets. + +## Motivation + +Decentralized finance (DeFi) faces a trust issue. Smart contracts are often proxies, with the actual logic of the contract hidden away in a separate logic contract. Many projects include a multi-signature "wallet" with unnecessarily-powerful permissions. And it is not possible to independently verify that stablecoins have enough real-world assets to continue maintaining their peg, creating a large loss of funds (such as happened in the official bankruptcy announcement of Celsius and UST de-pegging and anchor protocol failure). One should not trust exchanges or other third parties with one's own investments with the operators' clout in Web3.0. + +Smart contracts are best implemented as a promise between two parties written in code, but current DeFi contracts are often formed using less than 7 smart contracts to manage their whole investors' funds, and often have a trusted key that has full control. This is evidently an issue, as investors have to trust contract operators with their funds, meaning that users do not actually own their funds. + +The pattern with personal finance contract also offers more transparency than storing mixed fund financial data in the operating team's contract. With a personal finance contract, an account's activity is easier to track than one global smart contract's activity. The pattern introduces a Non-Fungiible Account-Bound Token (ABT) to store credentials from the personal finance contract. + +#### Offchain-identity vs Soul-bound token on credentials + +This EIP provides a better alternative to off-chain identity solutions which take over the whole system because their backends eventually rely on the trust of the operator, not cryptographic proof (e.g. Proof-of-work, Proof-of-stake, etc). Off-chain identity as credentials are in direct opposition to the whole premise of crypto. Soulbound tokens are a better, verifiable credential, and data stored off-chain is only to store token metadata. + +## Specification +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +The specification consists of two patterns for **Interaction** and **Governance**. + +### Interaction + +#### Interfaces + +The interaction pattern consists of 4 components for interaction; manager, factory, finance, account-bound token, and extension. + +Interaction contract pattern is defined with these contracts: +- A soul-bound or account bound token contract to give access to interact with a financial contract with credentials +- A manager contract that interacts first contact with an investor +- A factory contract that creates a financial contract for each user +- A finance contract that can interact with the investor + +#### Requirements + +A soul-bound or account bound token contract is defined with these properties: +1. It SHALL be non-fungible and MUST satisfy [EIP-721](./eip-721.md). +2. Credentials SHOULD be represented with its metadata with `tokenURI()` function. +3. It MUST only reference factory to verify its minting. +4. If it is transferrable, it is account-bound. If not, it is soul-bound. + +A manager contract is defined with these properties: +1. It MUST be the only kind of contract which calls factory to create. +2. It SHOULD store all related configurations for financial parameters. + +A factory contract is defined with these properties: +1. It SHALL clone the finance contract with uniform implementation. +2. It MUST be the only contract that can mint account-bound token. +3. It MUST keep an recent id of account bound token. + +A finance contract is defined with these properties: +1. A finance contract MUST only be initialized once from factory contract in constructor. +2. Funds in the contract SHALL NOT be transferred to other contracts nor accounts unless sender who owns soul-bound or account bound token signs to do so. +3. Every state-changing function of the smart contract MUST only accept sender who owns soul-bound or account bound-token except global function(e.g. liquidation). +4. Global function SHOULD be commented as `/* global */` to clarify the function is can be accessed with anyone. +4. Each finance contract SHOULD be able to represent transaction that has happened only with those who had account-bound token. +5. If soul-bound token is used for access, the finance contract MUST be able to represent transaction that has happened only between whom had the private key and the finance contract. + +#### Contracts + +
+Diagram +
Fig 1 - Contract Diagram of EIP-5252
+
+ + +**`Manager`**: **`Manager`** contract acts as an entry point to interact with the investor. The contract also stores parameters for **`Finance`** contract. + +**`Factory`**: **`Factory`** contract manages contract bytecode to create for managing investor's fund and clones **`Finance`** contract on **`Manager`** contract's approval. It also mints account-bound tokens to interact with the `Finance` contract. + +**`Finance`**: **`Finance`** contract specifies all rules on managing an investor's fund. The contract is only accessible with an account that has an Account-bound token. When an investor deposits a fund to **`Manager`** contract, the contract sends the fund to **`Finance`** contract account after separating fees for operation. + +**`Account-bound token`**: **`Account-bound token`** contract in this EIP can bring the **`Finance`** contract's data and add metadata. For example, if there is a money market lending +**`Finance`** contract, its **`Account-bound token`** can show how much balance is in agreement using SVG. + +**`Extension`**: **`Extension`** contract is another contract that can utilize locked funds in **`Finance`** contract. The contract can access with **`Finance`** contract on operator's approval managed in **`Manager`** contract. Example use case of `Extension` can be a membership. + +**`Metadata`**: **`Metadata`** contract is the contract where it stores metadata related to account credentials. Credential related data are stored with specific key. Images are usually displayed as SVG, but offchain image is possible. + +--- + +### Governance + +The governance pattern consists of 2 components; influencer and governor. + +#### Interfaces + +#### Requirements + +An influencer contract is defined with these properties: +1. The contract SHALL manage multiplier for votes. +2. The contract SHALL set a decimal to calculated normalized scores. +3. The contract SHALL set a function where governance can decide factor parameters. + +A governor contract is defined with these properties: +1. The contract MUST satisfy Governor contract from OpenZeppelin. +2. The contract SHALL refer influencer contract for multiplier +3. The contract MUST limit transfer of account bound token once claimed for double vote prevention. + +#### From Token Governance To Contribution Based Governance + +| | Token Governance | Credential-based Governance | +|---------------|----------------------------------|--------------------------------------------------| +| Enforcement | More tokens, more power | More contribution, More power | +| Incentives | More tokens, more incentives | More contribution, more incentives | +| Penalty | No penalty | Loss of power | +| Assignment | One who holds the token | One who has the most influence | + +
Token Governance vs Credential Based Governance
+ +Token governance is not sustainable in that it gives **more** power to "those who most want to rule". Any individual who gets more than 51% of the token supply can forcefully take control. + + +New governance that considers contributions to the protocol is needed because: + +- **Rulers can be penalized on breaking the protocol** +- **Rulers can be more effectively incentivized on maintaining the protocol** + +The power should be given to "those who are most responsible". Instead of locked or owned tokens, voting power is determined with contributions marked in Account Bound Tokens (ABT). This EIP defines this form of voting power as **`Influence`**. + +#### Calculating Influence + +**`Influence`** is a multiplier on staked tokens that brings more voting power of a DAO to its contributors. To get **`Influence`**, a score is calculated on weighted contribution matrix. Then, the score is normalized to give a member's position in whole distribution. Finally, the multiplier is determined on the position in every community members. + +#### Calculating score + +The weights represent relative importance on each factor. The total importance is the total sum of the factors. More factors that can be normalized at the time of submitting proposal can be added by community. + +| | Description | +|----|------------------------------------------------| +| α | Contribution value per each **`Finance`** contract from current proposal| +| β | Time they maintained **`Finance`** per each contract from current timestamp of a proposal| + +```math +(score per each ABT) = α * (contribution value) + β * (time that abt was maintained from now) +``` + +#### Normalization + +Normalization is applied for data integrity on user's contribution in a DAO. +Normalized score can be calculated from the state of submitting a proposal + +```math +(Normalized score per each ABT) = α * (contribution value)/(total contribution value at submitting tx) + β * (time that abt was maintained)/(time passed from genesis to proposal creation) +``` + +and have a value between 0 and 1 (since α + β = 1). + +#### Multiplier + +The multiplier is determined linearly from base factor (b) and multiplier(m). + +The equation for influence is : + +```math +(influence) = m * (sum(normalized_score)) +``` + +#### Example + +For example, if a user has 3 **`Account-bound tokens`** with normalized score of each 1.0, 0.5, 0.3 and the locked token is 100, and multiplier is 0.5 and base factor is 1.5. Then the total influence is + +```math +0.5 * {(1.0 + 0.5 + 0.3) / 3} + 1.5 = 1.8 + + The total voting power would be + +```math +(voting power) = 1.8 * sqrt(100) = 18 +``` + +#### Stakers vs Enforcers + +| | Stakers | Enforcers | +|--------------|-----------------------|-----------------------------------------------------------------------------------------| +| Role | stake governance token for voting | Contributed on the system, can make proposal to change rule, more voting power like 1.5 | +| Populations | many | small | +| Contribution | Less effect | More effect | +| Influence | sqrt(locked token) | Influence * sqrt(locked token) | + +
Fig 1 - Stakers vs Enforcers
+ +**Stakers**: Stakers are people who vote to enforcers' proposals and get dividend for staked tokens + +**Enforcers**: Enforcers are people who takes risk on managing protocol and contributes to the protocol by making a proposal and change to it. + +#### Contracts + +**`Influencer`**: An **`Influencer`** contract stores influence configurations and measures the contribution of a user from his activities done in a registered Account Bound Token contract. The contract puts a lock on that Account Bound Token until the proposal is finalized. + +**`Governor`**: **`Governor`** contract is compatible with the current governor contract in OpenZeppelin. For its special use case, it configures factors where the influencer manages and has access to changing parameters of **`Manager`** configs. Only the `Enforcer` can propose new parameters. + +## Rationale + +#### Gas saving for end user +The gas cost of using multiple contracts (as opposed to a single one) actually saves gas long-run if the clone factory pattern is applied. One contract storing users' states globally means each user is actually paying for the storage cost of other users after interacting with the contract. This, for example, means that MakerDAO's contract operating cost is sometimes over 0.1 ETH, limitimg users' minimum deposit for CDP in order to save gas costs. To solve inefficient n-times charging gas cost interaction for future users, one contract per user is used. + +#### Separation between investor's and operation fund +The separation between an investor's funds and operation fee is clearly specified in the smart contract, so investors can ensure safety from arbitrary loss of funds by the operating team's control. + +## Backwards Compatibility +This EIP has no known backward compatibility issues. + +## Reference Implementation + +[Reference implementation](../assets/eip-5252/README.md) is a simple deposit account contract as `Finance` contract and its contribution value α is measured with deposit amount with ETH. + +## Security Considerations + +- **`Factory`** contracts must ensure that each **`Finance`** contract is registered in the factory and check that **`Finance`** contracts are sending transactions related to their bounded owner. + +- Reentrancy attack guard should be applied or change state before delegatecall in each user function in **`Manager`** contract or **`Finance`** contract. Otherwise, **`Finance`** can be generated as double and ruin whole indices. + +- Once a user locks influence on a proposal's vote, an **`Account Bound Token`** cannot be transferred to another wallet. Otherwise, double influence can happen. + +## Copyright +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-5252/.gitignore b/assets/eip-5252/.gitignore new file mode 100644 index 0000000000000..b41feef092ab7 --- /dev/null +++ b/assets/eip-5252/.gitignore @@ -0,0 +1,14 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types +yarn.lock +package-lock.json +yarn-error.log +.DS_Store + +#Hardhat files +cache +artifacts diff --git a/assets/eip-5252/README.md b/assets/eip-5252/README.md new file mode 100644 index 0000000000000..5136b9b0f3854 --- /dev/null +++ b/assets/eip-5252/README.md @@ -0,0 +1,13 @@ +# EIP 5252 implementation + +This project is a reference implementation of EIP-5252. + +Try running some of the following tasks: + +```shell +npx hardhat help +npx hardhat test +GAS_REPORT=true npx hardhat test +npx hardhat node +npx hardhat run scripts/deploy.ts +``` diff --git a/assets/eip-5252/contracts/ABT.sol b/assets/eip-5252/contracts/ABT.sol new file mode 100644 index 0000000000000..a9896e07fc70f --- /dev/null +++ b/assets/eip-5252/contracts/ABT.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +import "./ERC721A.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./interfaces/IFactory.sol"; +import "./interfaces/IFinance.sol"; +import "./interfaces/IDescriptor.sol"; + +contract ABT is ERC721A, AccessControl { + // Create a new role identifier for the minter role + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + // factory address + address public factory; + // SVG for ABT + address public descriptor; + + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721A, AccessControl) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function setDescriptor(address descriptor_) public { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "ABT: Caller is not a default admin"); + descriptor = descriptor_; + } + + function tokenURI(uint256 tokenId) public view virtual override returns (string memory tokenURI) { + require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); + tokenURI = IDescriptor(descriptor).tokenURI(tokenId); + } + + constructor(address factory_) + ERC721A("Account-Bound Token", "ABT") { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + + _setupRole(MINTER_ROLE, _msgSender()); + _setupRole(BURNER_ROLE, _msgSender()); + factory = factory_; + } + + function setFactory(address factory_) public { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "ABT: Caller is not a default admin"); + factory = factory_; + } + + function mint(address to) external { + // Check that the calling account has the minter role + require(_msgSender() == factory, "ABT: Caller is not factory"); + _safeMint(to, 1); + } + + function burn(uint256 tokenId_) external { + require(hasRole(BURNER_ROLE, _msgSender()), "ABT: must have burner role to burn"); + _burn(tokenId_); + } + + function exists(uint256 tokenId_) external view returns (bool) { + return _exists(tokenId_); + } + + function transfer( + address to, + uint256 tokenId + ) public virtual { + transferFrom(msg.sender, to, tokenId); + } +} diff --git a/assets/eip-5252/contracts/ERC721A.sol b/assets/eip-5252/contracts/ERC721A.sol new file mode 100644 index 0000000000000..92ebd127e2d6e --- /dev/null +++ b/assets/eip-5252/contracts/ERC721A.sol @@ -0,0 +1,616 @@ +// SPDX-License-Identifier: CC0-1.0 + +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import '@openzeppelin/contracts/token/ERC721/IERC721.sol'; +import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol'; +import '@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol'; +import '@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/utils/Context.sol'; +import '@openzeppelin/contracts/utils/Strings.sol'; +import '@openzeppelin/contracts/utils/introspection/ERC165.sol'; + +error ApprovalCallerNotOwnerNorApproved(); +error ApprovalQueryForNonexistentToken(); +error ApproveToCaller(); +error ApprovalToCurrentOwner(); +error BalanceQueryForZeroAddress(); +error MintedQueryForZeroAddress(); +error BurnedQueryForZeroAddress(); +error AuxQueryForZeroAddress(); +error MintToZeroAddress(); +error MintZeroQuantity(); +error OwnerIndexOutOfBounds(); +error OwnerQueryForNonexistentToken(); +error TokenIndexOutOfBounds(); +error TransferCallerNotOwnerNorApproved(); +error TransferFromIncorrectOwner(); +error TransferToNonERC721ReceiverImplementer(); +error TransferToZeroAddress(); +error URIQueryForNonexistentToken(); + +/** + * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including + * the Metadata extension. Built to optimize for lower gas during batch mints. + * + * Assumes serials are sequentially minted starting at _startTokenId() (defaults to 0, e.g. 0, 1, 2, 3..). + * + * Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply. + * + * Assumes that the maximum token id cannot exceed 2**256 - 1 (max value of uint256). + */ +contract ERC721A is Context, ERC165, IERC721, IERC721Metadata { + using Address for address; + using Strings for uint256; + + // Compiler will pack this into a single 256bit word. + struct TokenOwnership { + // The address of the owner. + address addr; + // Keeps track of the start time of ownership with minimal overhead for tokenomics. + uint64 startTimestamp; + // Whether the token has been burned. + bool burned; + } + + // Compiler will pack this into a single 256bit word. + struct AddressData { + // Realistically, 2**64-1 is more than enough. + uint64 balance; + // Keeps track of mint count with minimal overhead for tokenomics. + uint64 numberMinted; + // Keeps track of burn count with minimal overhead for tokenomics. + uint64 numberBurned; + // For miscellaneous variable(s) pertaining to the address + // (e.g. number of whitelist mint slots used). + // If there are multiple variables, please pack them into a uint64. + uint64 aux; + } + + // The tokenId of the next token to be minted. + uint256 internal _currentIndex; + + // The number of tokens burned. + uint256 internal _burnCounter; + + // Token name + string private _name; + + // Token symbol + string private _symbol; + + // Mapping from token ID to ownership details + // An empty struct value does not necessarily mean the token is unowned. See ownershipOf implementation for details. + mapping(uint256 => TokenOwnership) internal _ownerships; + + // Mapping owner address to address data + mapping(address => AddressData) private _addressData; + + // Mapping from token ID to approved address + mapping(uint256 => address) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + _currentIndex = _startTokenId(); + } + + /** + * To change the starting tokenId, please override this function. + */ + function _startTokenId() internal view virtual returns (uint256) { + return 0; + } + + /** + * @dev See {IERC721Enumerable-totalSupply}. + * @dev Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens. + */ + function totalSupply() public view returns (uint256) { + // Counter underflow is impossible as _burnCounter cannot be incremented + // more than _currentIndex - _startTokenId() times + unchecked { + return _currentIndex - _burnCounter - _startTokenId(); + } + } + + /** + * Returns the total amount of tokens minted in the contract. + */ + function _totalMinted() internal view returns (uint256) { + // Counter underflow is impossible as _currentIndex does not decrement, + // and it is initialized to _startTokenId() + unchecked { + return _currentIndex - _startTokenId(); + } + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC721Metadata).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @dev See {IERC721-balanceOf}. + */ + function balanceOf(address owner) public view override returns (uint256) { + if (owner == address(0)) revert BalanceQueryForZeroAddress(); + return uint256(_addressData[owner].balance); + } + + /** + * Returns the number of tokens minted by `owner`. + */ + function _numberMinted(address owner) internal view returns (uint256) { + if (owner == address(0)) revert MintedQueryForZeroAddress(); + return uint256(_addressData[owner].numberMinted); + } + + /** + * Returns the number of tokens burned by or on behalf of `owner`. + */ + function _numberBurned(address owner) internal view returns (uint256) { + if (owner == address(0)) revert BurnedQueryForZeroAddress(); + return uint256(_addressData[owner].numberBurned); + } + + /** + * Returns the auxiliary data for `owner`. (e.g. number of whitelist mint slots used). + */ + function _getAux(address owner) internal view returns (uint64) { + if (owner == address(0)) revert AuxQueryForZeroAddress(); + return _addressData[owner].aux; + } + + /** + * Sets the auxiliary data for `owner`. (e.g. number of whitelist mint slots used). + * If there are multiple variables, please pack them into a uint64. + */ + function _setAux(address owner, uint64 aux) internal { + if (owner == address(0)) revert AuxQueryForZeroAddress(); + _addressData[owner].aux = aux; + } + + /** + * Gas spent here starts off proportional to the maximum mint batch size. + * It gradually moves to O(1) as tokens get transferred around in the collection over time. + */ + function ownershipOf(uint256 tokenId) internal view returns (TokenOwnership memory) { + uint256 curr = tokenId; + + unchecked { + if (_startTokenId() <= curr && curr < _currentIndex) { + TokenOwnership memory ownership = _ownerships[curr]; + if (!ownership.burned) { + if (ownership.addr != address(0)) { + return ownership; + } + // Invariant: + // There will always be an ownership that has an address and is not burned + // before an ownership that does not have an address and is not burned. + // Hence, curr will not underflow. + while (true) { + curr--; + ownership = _ownerships[curr]; + if (ownership.addr != address(0)) { + return ownership; + } + } + } + } + } + revert OwnerQueryForNonexistentToken(); + } + + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) public view override returns (address) { + return ownershipOf(tokenId).addr; + } + + /** + * @dev See {IERC721Metadata-name}. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev See {IERC721Metadata-symbol}. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ''; + } + + /** + * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each + * token will be the concatenation of the `baseURI` and the `tokenId`. Empty + * by default, can be overridden in child contracts. + */ + function _baseURI() internal view virtual returns (string memory) { + return ''; + } + + /** + * @dev See {IERC721-approve}. + */ + function approve(address to, uint256 tokenId) public override { + address owner = ERC721A.ownerOf(tokenId); + if (to == owner) revert ApprovalToCurrentOwner(); + + if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) { + revert ApprovalCallerNotOwnerNorApproved(); + } + + _approve(to, tokenId, owner); + } + + /** + * @dev See {IERC721-getApproved}. + */ + function getApproved(uint256 tokenId) public view override returns (address) { + if (!_exists(tokenId)) revert ApprovalQueryForNonexistentToken(); + + return _tokenApprovals[tokenId]; + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) public override { + if (operator == _msgSender()) revert ApproveToCaller(); + + _operatorApprovals[_msgSender()][operator] = approved; + emit ApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC721-isApprovedForAll}. + */ + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return _operatorApprovals[owner][operator]; + } + + /** + * @dev See {IERC721-transferFrom}. + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override { + _transfer(from, to, tokenId); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override { + safeTransferFrom(from, to, tokenId, ''); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) public virtual override { + _transfer(from, to, tokenId); + if (to.isContract() && !_checkContractOnERC721Received(from, to, tokenId, _data)) { + revert TransferToNonERC721ReceiverImplementer(); + } + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted (`_mint`), + */ + function _exists(uint256 tokenId) internal view returns (bool) { + return _startTokenId() <= tokenId && tokenId < _currentIndex && + !_ownerships[tokenId].burned; + } + + function _safeMint(address to, uint256 quantity) internal { + _safeMint(to, quantity, ''); + } + + /** + * @dev Safely mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called for each safe transfer. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event. + */ + function _safeMint( + address to, + uint256 quantity, + bytes memory _data + ) internal { + _mint(to, quantity, _data, true); + } + + /** + * @dev Mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event. + */ + function _mint( + address to, + uint256 quantity, + bytes memory _data, + bool safe + ) internal { + uint256 startTokenId = _currentIndex; + if (to == address(0)) revert MintToZeroAddress(); + if (quantity == 0) revert MintZeroQuantity(); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are incredibly unrealistic. + // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1 + // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1 + unchecked { + _addressData[to].balance += uint64(quantity); + _addressData[to].numberMinted += uint64(quantity); + + _ownerships[startTokenId].addr = to; + _ownerships[startTokenId].startTimestamp = uint64(block.timestamp); + + uint256 updatedIndex = startTokenId; + uint256 end = updatedIndex + quantity; + + if (safe && to.isContract()) { + do { + emit Transfer(address(0), to, updatedIndex); + if (!_checkContractOnERC721Received(address(0), to, updatedIndex++, _data)) { + revert TransferToNonERC721ReceiverImplementer(); + } + } while (updatedIndex != end); + // Reentrancy protection + if (_currentIndex != startTokenId) revert(); + } else { + do { + emit Transfer(address(0), to, updatedIndex++); + } while (updatedIndex != end); + } + _currentIndex = updatedIndex; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + */ + function _transfer( + address from, + address to, + uint256 tokenId + ) private { + TokenOwnership memory prevOwnership = ownershipOf(tokenId); + + bool isApprovedOrOwner = (_msgSender() == prevOwnership.addr || + isApprovedForAll(prevOwnership.addr, _msgSender()) || + getApproved(tokenId) == _msgSender()); + + if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); + if (prevOwnership.addr != from) revert TransferFromIncorrectOwner(); + if (to == address(0)) revert TransferToZeroAddress(); + + _beforeTokenTransfers(from, to, tokenId, 1); + + // Clear approvals from the previous owner + _approve(address(0), tokenId, prevOwnership.addr); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. + unchecked { + _addressData[from].balance -= 1; + _addressData[to].balance += 1; + + _ownerships[tokenId].addr = to; + _ownerships[tokenId].startTimestamp = uint64(block.timestamp); + + // If the ownership slot of tokenId+1 is not explicitly set, that means the transfer initiator owns it. + // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. + uint256 nextTokenId = tokenId + 1; + if (_ownerships[nextTokenId].addr == address(0)) { + // This will suffice for checking _exists(nextTokenId), + // as a burned slot cannot contain the zero address. + if (nextTokenId < _currentIndex) { + _ownerships[nextTokenId].addr = prevOwnership.addr; + _ownerships[nextTokenId].startTimestamp = prevOwnership.startTimestamp; + } + } + } + + emit Transfer(from, to, tokenId); + _afterTokenTransfers(from, to, tokenId, 1); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId) internal virtual { + TokenOwnership memory prevOwnership = ownershipOf(tokenId); + + _beforeTokenTransfers(prevOwnership.addr, address(0), tokenId, 1); + + // Clear approvals from the previous owner + _approve(address(0), tokenId, prevOwnership.addr); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. + unchecked { + _addressData[prevOwnership.addr].balance -= 1; + _addressData[prevOwnership.addr].numberBurned += 1; + + // Keep track of who burned the token, and the timestamp of burning. + _ownerships[tokenId].addr = prevOwnership.addr; + _ownerships[tokenId].startTimestamp = uint64(block.timestamp); + _ownerships[tokenId].burned = true; + + // If the ownership slot of tokenId+1 is not explicitly set, that means the burn initiator owns it. + // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. + uint256 nextTokenId = tokenId + 1; + if (_ownerships[nextTokenId].addr == address(0)) { + // This will suffice for checking _exists(nextTokenId), + // as a burned slot cannot contain the zero address. + if (nextTokenId < _currentIndex) { + _ownerships[nextTokenId].addr = prevOwnership.addr; + _ownerships[nextTokenId].startTimestamp = prevOwnership.startTimestamp; + } + } + } + + emit Transfer(prevOwnership.addr, address(0), tokenId); + _afterTokenTransfers(prevOwnership.addr, address(0), tokenId, 1); + + // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. + unchecked { + _burnCounter++; + } + } + + /** + * @dev Approve `to` to operate on `tokenId` + * + * Emits a {Approval} event. + */ + function _approve( + address to, + uint256 tokenId, + address owner + ) private { + _tokenApprovals[tokenId] = to; + emit Approval(owner, to, tokenId); + } + + /** + * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target contract. + * + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param _data bytes optional data to send along with the call + * @return bool whether the call correctly returned the expected magic value + */ + function _checkContractOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) private returns (bool) { + try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) { + return retval == IERC721Receiver(to).onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert TransferToNonERC721ReceiverImplementer(); + } else { + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + + /** + * @dev Hook that is called before a set of serially-ordered token ids are about to be transferred. This includes minting. + * And also called before burning one token. + * + * startTokenId - the first token id to be transferred + * quantity - the amount to be transferred + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, `tokenId` will be burned by `from`. + * - `from` and `to` are never both zero. + */ + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual {} + + /** + * @dev Hook that is called after a set of serially-ordered token ids have been transferred. This includes + * minting. + * And also called after one token has been burned. + * + * startTokenId - the first token id to be transferred + * quantity - the amount to be transferred + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` has been + * transferred to `to`. + * - When `from` is zero, `tokenId` has been minted for `to`. + * - When `to` is zero, `tokenId` has been burned by `from`. + * - `from` and `to` are never both zero. + */ + function _afterTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual {} +} \ No newline at end of file diff --git a/assets/eip-5252/contracts/Factory.sol b/assets/eip-5252/contracts/Factory.sol new file mode 100644 index 0000000000000..335e01a27550c --- /dev/null +++ b/assets/eip-5252/contracts/Factory.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./Finance.sol"; +import "./libraries/CloneFactory.sol"; +import "./interfaces/IFactory.sol"; + +contract Factory is AccessControl, IFactory { + // Vaults + address[] public allFinances; + /// Address of cdp nft registry + address public override abt; + /// Address of Wrapped Ether + address public override WETH; + /// Address of manager + address public override manager; + /// version number of impl + uint256 version; + /// address of vault impl + address public impl; + + constructor() { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + _createImpl(); + } + + /// Vault can issue stablecoin, it just manages the position + function createFinance( + address weth_, + uint256 amount_, + address recipient + ) external override returns (address vault, uint256 id) { + require(msg.sender == manager, "Factory: IA"); + uint256 gIndex = allFinancesLength(); + address proxy = CloneFactory._createClone(impl); + IFinance(proxy).initialize(manager, gIndex, abt, amount_, weth_); + allFinances.push(proxy); + IABT(abt).mint(recipient); + return (proxy, gIndex); + } + + // Set immutable, consistent, one rule for vault implementation + function _createImpl() internal { + address addr; + bytes memory bytecode = type(Finance).creationCode; + bytes32 salt = keccak256(abi.encodePacked("finance", version)); + assembly { + addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + if iszero(extcodesize(addr)) { + revert(0, 0) + } + } + impl = addr; + } + + function isClone(address vault) external view returns (bool cloned) { + cloned = CloneFactory._isClone(impl, vault); + } + + function initialize( + address abt_, + address weth_, + address manager_, + uint256 version_ + ) public { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "IA"); // Invalid Access + abt = abt_; + WETH = weth_; + manager = manager_; + version = version_; + } + + function getFinance(uint256 financeId_) + external + view + override + returns (address) + { + return allFinances[financeId_]; + } + + function financeCodeHash() + external + pure + override + returns (bytes32 vaultCode) + { + return keccak256(hex"3d602d80600a3d3981f3"); + } + + function allFinancesLength() public view returns (uint256) { + return allFinances.length; + } +} diff --git a/assets/eip-5252/contracts/Finance.sol b/assets/eip-5252/contracts/Finance.sol new file mode 100644 index 0000000000000..69d4be5560717 --- /dev/null +++ b/assets/eip-5252/contracts/Finance.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +import "./interfaces/IERC20Minimal.sol"; +import "./libraries/TransferHelper.sol"; +import "./interfaces/IFinance.sol"; +import "./interfaces/IABT.sol"; +import "./interfaces/IWETH.sol"; +import "./libraries/Initializable.sol"; +import "./interfaces/IManager.sol"; +import "./interfaces/IInfluencer.sol"; + +contract Finance is IFinance, Initializable { + /// Address of a manager + address public override manager; + /// Address of a factory + address public override factory; + /// Address of a factory + address public override influencer; + /// Address of account bound token + address public override abt; + /// Finance global identifier + uint256 public override financeId; + /// Address of wrapped eth + address public override WETH; + /// Finance Creation Date + uint256 public override createdAt; + /// Finance Last Updated Date + uint256 public override lastUpdated; + /// deposited amount to the account + uint256 public override deposit; + + modifier onlyFinanceOwner() { + require( + IABT(abt).ownerOf(financeId) == msg.sender, + "Finance: Finance is not owned by you" + ); + _; + } + + // called once by the factory at time of deployment + function initialize( + address manager_, + uint256 financeId_, + address abt_, + uint256 amount_, + address weth_ + ) external override initializer { + financeId = financeId_; + abt = abt_; + WETH = weth_; + manager = manager_; + factory = msg.sender; + deposit = amount_; + lastUpdated = block.timestamp; + createdAt = block.timestamp; + influencer = IManager(manager_).influencer(); + } + + function depositNative() external payable onlyFinanceOwner { + // wrap deposit + deposit += msg.value; + IInfluencer(influencer).deposit(msg.value); + IWETH(WETH).deposit{value: msg.value}(); + emit DepositFundNative(financeId, msg.value); + } + + /// Withdraw collateral as native currency + function withdrawNative(uint256 amount_) external virtual onlyFinanceOwner { + deposit -= amount_; + IInfluencer(influencer).withdraw(amount_); + // unwrap collateral + IWETH(WETH).withdraw(amount_); + // send withdrawn native currency + TransferHelper.safeTransferETH(msg.sender, address(this).balance); + emit WithdrawFundNative(financeId, amount_); + } + + receive() external payable { + assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract + } +} diff --git a/assets/eip-5252/contracts/Manager.sol b/assets/eip-5252/contracts/Manager.sol new file mode 100644 index 0000000000000..3b1142c497ef9 --- /dev/null +++ b/assets/eip-5252/contracts/Manager.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./interfaces/IWETH.sol"; +import "./interfaces/IManager.sol"; +import "./interfaces/IFactory.sol"; +import "./interfaces/IERC20Minimal.sol"; + +contract Manager is AccessControl, IManager { + + // Configs + /// key: Collateral address, value: Liquidation Fee Ratio (LFR) in percent(%) with 5 decimal precision(100.00000%) + mapping (address => uint) internal ExampleConfig; + + address public override factory; + + constructor() { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + function initializeConfig(address something, uint example) public { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "IA"); // Invalid Access + ExampleConfig[something] = example; + emit ConfigInitialized(something, example); + } + + function initialize(address stablecoin_, address factory_, address liquidator_) public { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "IA"); // Invalid Access + factory = factory_; + } + + function createFinanceNative(uint amount_) payable public returns(bool success) { + address WETH = IFactory(factory).WETH(); + // check validity + + // create vault + (address vlt, uint256 id) = IFactory(factory).createFinance(WETH, amount_, _msgSender()); + require(vlt != address(0), "VAULTMANAGER: FE"); // Factory error + // wrap native currency + IWETH(WETH).deposit{value: address(this).balance}(); + uint256 weth = IERC20Minimal(WETH).balanceOf(address(this)); + // then transfer collateral native currency to the finance contract, manage collateral from there. + require(IWETH(WETH).transfer(vlt, weth)); + emit FinanceCreated(id, WETH, msg.sender, vlt, msg.value); + return true; + } + + + function getExampleConfig(address something) external view override returns (uint) { + return ExampleConfig[something]; + } +} + diff --git a/assets/eip-5252/contracts/governance/Governor.sol b/assets/eip-5252/contracts/governance/Governor.sol new file mode 100644 index 0000000000000..5d06e65b2863c --- /dev/null +++ b/assets/eip-5252/contracts/governance/Governor.sol @@ -0,0 +1,109 @@ +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/governance/Governor.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; +import "../interfaces/IInfluencer.sol"; +import "@openzeppelin/contracts/governance/utils/IVotes.sol"; + +contract MyGovernor is Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorTimelockControl { + constructor(IVotes _token, TimelockController _timelock) + Governor("MyGovernor") + GovernorSettings(1 /* 1 block */, 45818 /* 1 week */, 0) + GovernorVotes(_token) + GovernorVotesQuorumFraction(4) + GovernorTimelockControl(_timelock) + {} + + // The following functions are overrides required by Solidity. + + function votingDelay() + public + view + override(IGovernor, GovernorSettings) + returns (uint256) + { + return super.votingDelay(); + } + + function votingPeriod() + public + view + override(IGovernor, GovernorSettings) + returns (uint256) + { + return super.votingPeriod(); + } + + function quorum(uint256 blockNumber) + public + view + override(IGovernor, GovernorVotesQuorumFraction) + returns (uint256) + { + return super.quorum(blockNumber); + } + + function state(uint256 proposalId) + public + view + override(Governor, GovernorTimelockControl) + returns (ProposalState) + { + return super.state(proposalId); + } + + function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) + public + override(Governor, IGovernor) + returns (uint256) + { + // check if sender is enforcer + return super.propose(targets, values, calldatas, description); + } + + function proposalThreshold() + public + view + override(Governor, GovernorSettings) + returns (uint256) + { + return super.proposalThreshold(); + } + + function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) + internal + override(Governor, GovernorTimelockControl) + { + super._execute(proposalId, targets, values, calldatas, descriptionHash); + } + + function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) + internal + override(Governor, GovernorTimelockControl) + returns (uint256) + { + return super._cancel(targets, values, calldatas, descriptionHash); + } + + function _executor() + internal + view + override(Governor, GovernorTimelockControl) + returns (address) + { + return super._executor(); + } + + function supportsInterface(bytes4 interfaceId) + public + view + override(Governor, GovernorTimelockControl) + returns (bool) + { + return super.supportsInterface(interfaceId); + } +} diff --git a/assets/eip-5252/contracts/governance/Influencer.sol b/assets/eip-5252/contracts/governance/Influencer.sol new file mode 100644 index 0000000000000..d34000f4c4d14 --- /dev/null +++ b/assets/eip-5252/contracts/governance/Influencer.sol @@ -0,0 +1,44 @@ +pragma solidity ^0.8.0; + +import "../interfaces/IABT.sol"; +import "../interfaces/IFactory.sol"; +import "../interfaces/IFinance.sol"; +import "../interfaces/IERC20Minimal.sol"; + +contract Influencer { + + uint256 totalContributionValue; + + mapping(string => Weight) weights; + + struct Weight { + uint256 percentage; + uint256 decimal; + } + + function getInfluence(address abt_, uint256 id_) public returns (uint multiplier) { + return _getInfluence(abt_, id_); + } + + + function _getInfluence(address abt_, uint256 id_) internal returns (uint influence) { + // get Finance address + address factory = IABT(abt_).factory(); + address finance = IFactory(factory).getFinance(id_); + address WETH = IFinance(finance).WETH(); + // normalize finance value + uint256 norm_alpha = IERC20Minimal(WETH).balanceOf(finance) / totalContributionValue * 100; + uint256 norm_beta = block.timestamp - IFinance(finance).createdAt() / block.timestamp * 100; + + // Divide with each decimal + uint256 influence_dec = weights["alpha"].percentage * norm_alpha + weights["beta"].percentage * norm_beta; + return influence_dec / weights["alpha"].decimal / weights["beta"].decimal; + } + + function setWeight(string memory key, uint256 percentage, uint256 decimal) public { + weights[key] = Weight({ + percentage: percentage, + decimal: decimal + }); + } +} \ No newline at end of file diff --git a/assets/eip-5252/contracts/governance/Vote.sol b/assets/eip-5252/contracts/governance/Vote.sol new file mode 100644 index 0000000000000..003ebb1c6674a --- /dev/null +++ b/assets/eip-5252/contracts/governance/Vote.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; +import "@openzeppelin/contracts/governance/Governor.sol"; +import "../interfaces/IInfluencer.sol"; +import "../interfaces/IABT.sol"; + +contract MyToken is ERC20, ERC20Permit, ERC20Votes, Governor { + constructor() ERC20("GovToken", "GOV") ERC20Permit("Governance Token") {} + + mapping(address => uint256[]) private _multiplier; + address public influencer; + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal override(ERC20, ERC20Votes) { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC20, ERC20Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC20, ERC20Votes) + { + super._burn(account, amount); + } + + function _sqrt(uint256 x) internal returns (uint256 y) { + uint256 z = (x + 1) / 2; + y = x; + while (z < y) { + y = z; + z = (x / z + z) / 2; + } + } + + function getVotes(address account) + public + view + virtual + override + returns (uint256) + { + uint256 pos = _checkpoints[account].length; + uint256 vote = pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + // 0 as None, Multiplied with + uint256 multiplied = _multiplier[pos - 1] > 0 + ? _sqrt(vote) + : _sqrt(vote * _multiplier[pos - 1]); + return multiplied; + } + + function mulInfluence(address abt, uint256 id) public { + require(IABT(abt).ownerOf(id) == msg.sender, "Vote: not abt owner"); + uint256 pos = _checkpoints[msg.sender].length; + _multiplier[pos - 1] = IInfluencer.getInfluence(abt, id); + } + + function setInfluencer(address influencer_) public onlyGovernance { + influencer = influencer_; + } +} diff --git a/assets/eip-5252/contracts/interfaces/IABT.sol b/assets/eip-5252/contracts/interfaces/IABT.sol new file mode 100644 index 0000000000000..e6b3d5fa69a0b --- /dev/null +++ b/assets/eip-5252/contracts/interfaces/IABT.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +interface IABT { + function mint(address to) external; + function burn(uint256 tokenId_) external; + function exists(uint256 tokenId_) external view returns (bool); + function ownerOf(uint256 tokenId) external view returns (address owner); + function factory() external view returns (address factory); +} diff --git a/assets/eip-5252/contracts/interfaces/IDescriptor.sol b/assets/eip-5252/contracts/interfaces/IDescriptor.sol new file mode 100644 index 0000000000000..eecf26858e3b6 --- /dev/null +++ b/assets/eip-5252/contracts/interfaces/IDescriptor.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.8.0; + +interface IDescriptor { + function tokenURI(uint256 tokenId) external view returns (string memory); +} diff --git a/assets/eip-5252/contracts/interfaces/IERC20Minimal.sol b/assets/eip-5252/contracts/interfaces/IERC20Minimal.sol new file mode 100644 index 0000000000000..5e58531d09601 --- /dev/null +++ b/assets/eip-5252/contracts/interfaces/IERC20Minimal.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity >=0.5.0; + +interface IERC20Minimal { + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function decimals() external view returns (uint8); +} diff --git a/assets/eip-5252/contracts/interfaces/IFactory.sol b/assets/eip-5252/contracts/interfaces/IFactory.sol new file mode 100644 index 0000000000000..e764bc1b06cd0 --- /dev/null +++ b/assets/eip-5252/contracts/interfaces/IFactory.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +interface IFactory { + + /// View funcs + /// NFT token address + function abt() external view returns (address); + /// Address of wrapped eth + function WETH() external view returns (address); + /// Address of a manager + function manager() external view returns (address); + + /// Getters + /// Get Config of CDP + function financeCodeHash() external pure returns (bytes32); + function createFinance(address weth, uint256 amount_, address recipient) external returns (address vault, uint256 id); + function getFinance(uint financeId_) external view returns (address); + + /// Event + event FinanceCreated(uint256 vaultId, address collateral, address debt, address creator, address vault, uint256 cAmount, uint256 dAmount); +} diff --git a/assets/eip-5252/contracts/interfaces/IFinance.sol b/assets/eip-5252/contracts/interfaces/IFinance.sol new file mode 100644 index 0000000000000..5f9749c926b4d --- /dev/null +++ b/assets/eip-5252/contracts/interfaces/IFinance.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +interface IFinance { + event DepositFundNative(uint256 vaultID, uint256 amount); + event WithdrawFundNative(uint256 vaultID, uint256 amount); + /// Getters + /// Address of a factory + function factory() external view returns (address); + /// Address of a manager + function manager() external view returns (address); + function influencer() external view returns (address); + /// Address of account bound token + function abt() external view returns (address); + /// Finance global identifier + function financeId() external view returns (uint256); + /// Finance Last Updated Date + function lastUpdated() external view returns (uint256); + /// Finance creation date + function createdAt() external view returns (uint256); + /// address of wrapped eth + function WETH() external view returns (address); + /// deposit amount of finance account + function deposit() external view returns (uint256); + + /// Functions + function initialize( + address manager_, + uint256 financeId_, + address abt_, + uint256 amount_, + address weth_ + ) external; + +} diff --git a/assets/eip-5252/contracts/interfaces/IInfluencer.sol b/assets/eip-5252/contracts/interfaces/IInfluencer.sol new file mode 100644 index 0000000000000..c732c48164c7c --- /dev/null +++ b/assets/eip-5252/contracts/interfaces/IInfluencer.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity >=0.5.0; + +interface IInfluencer { + function isEnforcer(address sender) external; +} diff --git a/assets/eip-5252/contracts/interfaces/IManager.sol b/assets/eip-5252/contracts/interfaces/IManager.sol new file mode 100644 index 0000000000000..3f7bfa14812d1 --- /dev/null +++ b/assets/eip-5252/contracts/interfaces/IManager.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.8.0; + +interface IManager { + function factory() external view returns (address); + function influencer() external view returns (address); + function getExampleConfig(address something) external view returns (uint); + event ConfigInitialized(address something, uint example); + event FinanceCreated(uint id, address weth, address sender, address finance, uint input); +} diff --git a/assets/eip-5252/contracts/interfaces/IWETH.sol b/assets/eip-5252/contracts/interfaces/IWETH.sol new file mode 100644 index 0000000000000..6cf78eaabdbe4 --- /dev/null +++ b/assets/eip-5252/contracts/interfaces/IWETH.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity >=0.5.0; + +interface IWETH { + function deposit() external payable; + function transfer(address to, uint value) external returns (bool); + function withdraw(uint) external; +} diff --git a/assets/eip-5252/contracts/libraries/CloneFactory.sol b/assets/eip-5252/contracts/libraries/CloneFactory.sol new file mode 100644 index 0000000000000..911e6b5b75c6c --- /dev/null +++ b/assets/eip-5252/contracts/libraries/CloneFactory.sol @@ -0,0 +1,82 @@ +library CloneFactory { + function _createClone(address target) internal returns (address result) { + // convert address to 20 bytes + bytes20 targetBytes = bytes20(target); + + // actual code // + // 3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 + + // creation code // + // copy runtime code into memory and return it + // 3d602d80600a3d3981f3 + + // runtime code // + // code to delegatecall to address + // 363d3d373d3d3d363d73 address 5af43d82803e903d91602b57fd5bf3 + + assembly { + /* + reads the 32 bytes of memory starting at pointer stored in 0x40 + + In solidity, the 0x40 slot in memory is special: it contains the "free memory pointer" + which points to the end of the currently allocated memory. + */ + let clone := mload(0x40) + // store 32 bytes to memory starting at "clone" + mstore( + clone, + 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 + ) + + /* + | 20 bytes | + 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 + ^ + pointer + */ + // store 32 bytes to memory starting at "clone" + 20 bytes + // 0x14 = 20 + mstore(add(clone, 0x14), targetBytes) + + /* + | 20 bytes | 20 bytes | + 0x3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe + ^ + pointer + */ + // store 32 bytes to memory starting at "clone" + 40 bytes + // 0x28 = 40 + mstore( + add(clone, 0x28), + 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 + ) + + /* + | 20 bytes | 20 bytes | 15 bytes | + 0x3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 + */ + // create new contract + // send 0 Ether + // code starts at pointer stored in "clone" + // code size 0x37 (55 bytes) + result := create(0, clone, 0x37) + } + } + + function _isClone(address target, address query) internal view returns (bool result) { + bytes20 targetBytes = bytes20(target); + assembly { + let clone := mload(0x40) + mstore(clone, 0x363d3d373d3d3d363d7300000000000000000000000000000000000000000000) + mstore(add(clone, 0xa), targetBytes) + mstore(add(clone, 0x1e), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) + + let other := add(clone, 0x40) + extcodecopy(query, other, 0, 0x2d) + result := and( + eq(mload(clone), mload(other)), + eq(mload(add(clone, 0xd)), mload(add(other, 0xd))) + ) + } + } +} diff --git a/assets/eip-5252/contracts/libraries/Initializable.sol b/assets/eip-5252/contracts/libraries/Initializable.sol new file mode 100644 index 0000000000000..55a3978639978 --- /dev/null +++ b/assets/eip-5252/contracts/libraries/Initializable.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +contract Initializable { + bool private _initialized = false; + + modifier initializer() { + // solhint-disable-next-line reason-string + require(!_initialized); + _; + _initialized = true; + } + + function initialized() external view returns (bool) { + return _initialized; + } +} diff --git a/assets/eip-5252/contracts/libraries/SVG.sol b/assets/eip-5252/contracts/libraries/SVG.sol new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/assets/eip-5252/contracts/libraries/TransferHelper.sol b/assets/eip-5252/contracts/libraries/TransferHelper.sol new file mode 100644 index 0000000000000..550db796334d8 --- /dev/null +++ b/assets/eip-5252/contracts/libraries/TransferHelper.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false +library TransferHelper { + function safeApprove(address token, address to, uint value) internal { + // bytes4(keccak256(bytes("approve(address,uint256)"))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), "AF"); + } + + function safeTransfer(address token, address to, uint value) internal { + // bytes4(keccak256(bytes("transfer(address,uint256)"))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), "TF"); + } + + function safeTransferFrom(address token, address from, address to, uint value) internal { + // bytes4(keccak256(bytes("transferFrom(address,address,uint256)"))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), "TFF"); + } + + function safeTransferETH(address to, uint value) internal { + (bool success,) = to.call{value:value}(new bytes(0)); + require(success, "ETF"); + } +} diff --git a/assets/eip-5252/hardhat.config.ts b/assets/eip-5252/hardhat.config.ts new file mode 100644 index 0000000000000..414e974b9574b --- /dev/null +++ b/assets/eip-5252/hardhat.config.ts @@ -0,0 +1,8 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; + +const config: HardhatUserConfig = { + solidity: "0.8.9", +}; + +export default config; diff --git a/assets/eip-5252/media/media.svg b/assets/eip-5252/media/media.svg new file mode 100644 index 0000000000000..c74c943c9f941 --- /dev/null +++ b/assets/eip-5252/media/media.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/eip-5252/package.json b/assets/eip-5252/package.json new file mode 100644 index 0000000000000..af7ea0c2593e3 --- /dev/null +++ b/assets/eip-5252/package.json @@ -0,0 +1,12 @@ +{ + "name": "hardhat-project", + "devDependencies": { + "@nomicfoundation/hardhat-toolbox": "^1.0.2", + "hardhat": "^2.10.1", + "ts-node": "^10.9.1", + "typescript": "^4.7.4" + }, + "dependencies": { + "@openzeppelin/contracts": "^4.7.1" + } +} diff --git a/assets/eip-5252/scripts/deploy.ts b/assets/eip-5252/scripts/deploy.ts new file mode 100644 index 0000000000000..90e8908a29a0d --- /dev/null +++ b/assets/eip-5252/scripts/deploy.ts @@ -0,0 +1,23 @@ +import { ethers } from "hardhat"; + +async function main() { + const currentTimestampInSeconds = Math.round(Date.now() / 1000); + const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; + const unlockTime = currentTimestampInSeconds + ONE_YEAR_IN_SECS; + + const lockedAmount = ethers.utils.parseEther("1"); + + const Lock = await ethers.getContractFactory("Lock"); + const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); + + await lock.deployed(); + + console.log("Lock with 1 ETH deployed to:", lock.address); +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/assets/eip-5252/test/Lock.ts b/assets/eip-5252/test/Lock.ts new file mode 100644 index 0000000000000..3127221fc4217 --- /dev/null +++ b/assets/eip-5252/test/Lock.ts @@ -0,0 +1,124 @@ +import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +describe("Lock", function () { + // We define a fixture to reuse the same setup in every test. + // We use loadFixture to run this setup once, snapshot that state, + // and reset Hardhat Network to that snapshopt in every test. + async function deployOneYearLockFixture() { + const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; + const ONE_GWEI = 1_000_000_000; + + const lockedAmount = ONE_GWEI; + const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; + + // Contracts are deployed using the first signer/account by default + const [owner, otherAccount] = await ethers.getSigners(); + + const Lock = await ethers.getContractFactory("Lock"); + const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); + + return { lock, unlockTime, lockedAmount, owner, otherAccount }; + } + + describe("Deployment", function () { + it("Should set the right unlockTime", async function () { + const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); + + expect(await lock.unlockTime()).to.equal(unlockTime); + }); + + it("Should set the right owner", async function () { + const { lock, owner } = await loadFixture(deployOneYearLockFixture); + + expect(await lock.owner()).to.equal(owner.address); + }); + + it("Should receive and store the funds to lock", async function () { + const { lock, lockedAmount } = await loadFixture( + deployOneYearLockFixture + ); + + expect(await ethers.provider.getBalance(lock.address)).to.equal( + lockedAmount + ); + }); + + it("Should fail if the unlockTime is not in the future", async function () { + // We don't use the fixture here because we want a different deployment + const latestTime = await time.latest(); + const Lock = await ethers.getContractFactory("Lock"); + await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( + "Unlock time should be in the future" + ); + }); + }); + + describe("Withdrawals", function () { + describe("Validations", function () { + it("Should revert with the right error if called too soon", async function () { + const { lock } = await loadFixture(deployOneYearLockFixture); + + await expect(lock.withdraw()).to.be.revertedWith( + "You can't withdraw yet" + ); + }); + + it("Should revert with the right error if called from another account", async function () { + const { lock, unlockTime, otherAccount } = await loadFixture( + deployOneYearLockFixture + ); + + // We can increase the time in Hardhat Network + await time.increaseTo(unlockTime); + + // We use lock.connect() to send a transaction from another account + await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( + "You aren't the owner" + ); + }); + + it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { + const { lock, unlockTime } = await loadFixture( + deployOneYearLockFixture + ); + + // Transactions are sent using the first signer by default + await time.increaseTo(unlockTime); + + await expect(lock.withdraw()).not.to.be.reverted; + }); + }); + + describe("Events", function () { + it("Should emit an event on withdrawals", async function () { + const { lock, unlockTime, lockedAmount } = await loadFixture( + deployOneYearLockFixture + ); + + await time.increaseTo(unlockTime); + + await expect(lock.withdraw()) + .to.emit(lock, "Withdrawal") + .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg + }); + }); + + describe("Transfers", function () { + it("Should transfer the funds to the owner", async function () { + const { lock, unlockTime, lockedAmount, owner } = await loadFixture( + deployOneYearLockFixture + ); + + await time.increaseTo(unlockTime); + + await expect(lock.withdraw()).to.changeEtherBalances( + [owner, lock], + [lockedAmount, -lockedAmount] + ); + }); + }); + }); +}); diff --git a/assets/eip-5252/tsconfig.json b/assets/eip-5252/tsconfig.json new file mode 100644 index 0000000000000..e5f1a64007abf --- /dev/null +++ b/assets/eip-5252/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +}