From 6036dff7f0ddeecf2f5af4d24efc9eabf4b155c0 Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Mon, 26 Feb 2024 15:58:24 +0000 Subject: [PATCH 01/15] (Hub): group mint --- src/circles/DiscountedBalances.sol | 3 +- src/groups/BaseMintPolicy.sol | 6 ++ src/groups/IMintPolicy.sol | 12 ++++ src/hub/Hub.sol | 102 ++++++++++++++++------------- 4 files changed, 75 insertions(+), 48 deletions(-) create mode 100644 src/groups/BaseMintPolicy.sol create mode 100644 src/groups/IMintPolicy.sol diff --git a/src/circles/DiscountedBalances.sol b/src/circles/DiscountedBalances.sol index df689d1..7f71ba8 100644 --- a/src/circles/DiscountedBalances.sol +++ b/src/circles/DiscountedBalances.sol @@ -95,8 +95,9 @@ contract DiscountedBalances { /** * @dev stores the discounted balances of the accounts privately. + * Mapping from Circles identifiers to accounts to the discounted balance. */ - mapping(uint256 id => mapping(address account => DiscountedBalance)) private discountedBalances; + mapping(uint256 => mapping(address => DiscountedBalance)) private discountedBalances; /** * @dev Store a lookup table T(n) for computing issuance. diff --git a/src/groups/BaseMintPolicy.sol b/src/groups/BaseMintPolicy.sol new file mode 100644 index 0000000..59393e3 --- /dev/null +++ b/src/groups/BaseMintPolicy.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import "./IMintPolicy.sol"; + +abstract contract MintPolicy is IMintPolicy {} diff --git a/src/groups/IMintPolicy.sol b/src/groups/IMintPolicy.sol new file mode 100644 index 0000000..5a3cd2b --- /dev/null +++ b/src/groups/IMintPolicy.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +interface IMintPolicy { + function beforeMintPolicy( + address minter, + address group, + address[] calldata collateral, + uint256[] calldata amounts, + bytes calldata _data + ) external returns (bool); +} diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 70fcc9f..1c7ee1c 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -7,6 +7,7 @@ import "openzeppelin-contracts/contracts/utils/Create2.sol"; import "../migration/IHub.sol"; import "../migration/IToken.sol"; import "../circles/Circles.sol"; +import "../groups/IMintPolicy.sol"; /** * @title Hub v2 contract for Circles @@ -208,34 +209,6 @@ contract Hub is Circles { emit InviteHuman(msg.sender, _human); } - /** - * @notice Invite human as organization allows to register a human avatar as an organization. - * @param _human address of the human to invite - * @param _donationReceiver address of where to send the donation to with 2300 gas (using transfer) - */ - function inviteHumanAsOrganization(address _human, address payable _donationReceiver) external payable { - require(msg.value > MINIMUM_DONATION, "Donation must be at least 0.1 xDai."); - // The donation is understood to be a reputational requirement for the organization. - // It is obvious that one can send to self over a different address, but that is reputationally worthless. - // Nonetheless, we require to not directly send to self, mostly to avoid "plausible denial" arguments. - require(_donationReceiver != msg.sender, "Donation receiver cannot be the caller."); - require(isOrganization(msg.sender), "Only organizations can invite."); - - _registerHuman(_human); - - // set trust for a year, but organization can edit this later - _trust(msg.sender, _human, uint96(block.timestamp + 365 days)); - - // invited receives the welcome bonus in their personal Circles - _mint(_human, toTokenId(_human), WELCOME_BONUS, ""); - - // send the donation to the donation receiver but with minimal gas - // to avoid reentrancy attacks - _donationReceiver.transfer(msg.value); - - emit InviteHuman(msg.sender, _human); - } - /** * @notice Register group allows to register a group avatar. * @param _mint mint address will be called before minting group circles @@ -287,7 +260,7 @@ contract Hub is Circles { * @param _cidV0Digest IPFS CIDv0 digest for the organization metadata */ function registerOrganization(string calldata _name, bytes32 _cidV0Digest) external { - require(_isValidName(_name), "Invalid organization name."); + require(isValidName(_name), "Invalid organization name."); _insertAvatar(msg.sender); // store the name for the organization @@ -338,22 +311,45 @@ contract Hub is Circles { // graph transfers SHOULD allow personal -> group conversion en route - // msg.sender holds collateral, and MUST be accepted by group - // maybe less - function groupMint(address _group, uint256[] calldata _collateral, uint256[] calldata _amounts) external { - // check group and collateral exist - // de-demurrage amounts - // loop over collateral + /** + * @notice Group mint allows to mint group Circles by providing the required collateral. + * @param _group address of the group avatar to mint Circles of + * @param _collateral array of (personal or group) avatar addresses to be used as collateral + * @param _amounts array of amounts of collateral to be used for minting + * @param _data (optional) additional data to be passed to the treasury and minter + */ + function groupMint( + address _group, + address[] calldata _collateral, + uint256[] calldata _amounts, + bytes calldata _data + ) external { + require(_collateral.length == _amounts.length, "Collateral and amount arrays must have equal length"); + require(_collateral.length > 0, "At least one collateral must be provided"); + require(isGroup(_group), "Group is not registered as an avatar."); + + // note: we don't need to check whether collateral circle ids are registered, + // because only for registered collateral do non-zero balances exist to transfer, + // so it suffices to check that all amounts are non-zero during summing. + uint256 sumAmounts = 0; + uint256[] memory collateralCirclesIds = new uint256[](_collateral.length); + for (uint256 i = 0; i < _amounts.length; i++) { + require(isTrusted(_group, _collateral[i]), "Collateral must be trusted"); + require(_amounts[i] > 0, "Non-zero collateral must be provided."); + sumAmounts += _amounts[i]; + collateralCirclesIds[i] = toTokenId(_collateral[i]); + } - //require( - //mintPolicies[_group].beforeMintPolicy(msg.sender, _group, _collateral, _amounts), ""); + // Rely on the mint policy to determine whether the collateral is valid for minting + require( + IMintPolicy(mintPolicies[_group]).beforeMintPolicy(msg.sender, _group, _collateral, _amounts, _data), + "Mint policy rejected mint." + ); - safeBatchTransferFrom(msg.sender, treasuries[_group], _collateral, _amounts, ""); // treasury.on1155Received should only implement but nothing protocol related + // note: treasury.on1155Received must implement but no further requirements + safeBatchTransferFrom(msg.sender, treasuries[_group], collateralCirclesIds, _amounts, _data); - uint256 sumAmounts; - // TODO sum up amounts - sumAmounts = _amounts[0]; - _mint(msg.sender, toTokenId(_group), sumAmounts, ""); + _mint(msg.sender, toTokenId(_group), sumAmounts, _data); } function stop() external { @@ -389,6 +385,8 @@ contract Hub is Circles { //require("nett sources have approved operator"); } + // Public functions + function getDeterministicAddress(uint256 _tokenId, bytes32 _bytecodeHash) public view returns (address) { return Create2.computeAddress(keccak256(abi.encodePacked(_tokenId)), _bytecodeHash); } @@ -469,7 +467,7 @@ contract Hub is Circles { } /** - * Checks if an avatar is registered as an organization. + * @notice Checks if an avatar is registered as an organization. * @param _organization address of the organization to check */ function isOrganization(address _organization) public view returns (bool) { @@ -477,6 +475,16 @@ contract Hub is Circles { && mintTimes[_organization].lastMintTime == uint256(0); } + /** + * @notice Returns true if the truster trusts the trustee. + * @param _truster Address of the trusting account + * @param _trustee Address of the trusted account + */ + function isTrusted(address _truster, address _trustee) public view returns (bool) { + // trust up until expiry timestamp + return uint256(trustMarkers[_truster][_trustee].expiry) > block.timestamp; + } + /** * uri returns the IPFS URI for the ERC1155 token. * If the @@ -500,7 +508,7 @@ contract Hub is Circles { * should provide the full display name with unicode characters. * Names are not checked for uniqueness. */ - function _isValidName(string memory _name) public pure returns (bool) { + function isValidName(string memory _name) public pure returns (bool) { bytes memory nameBytes = bytes(_name); if (nameBytes.length > 32 || nameBytes.length == 0) return false; // Check length @@ -529,7 +537,7 @@ contract Hub is Circles { * the length as max 16 bytes and the allowed characters: 0-9, A-Z, a-z, * hyphen, underscore. */ - function _isValidSymbol(string memory _symbol) public pure returns (bool) { + function isValidSymbol(string memory _symbol) public pure returns (bool) { bytes memory symbolBytes = bytes(_symbol); if (symbolBytes.length == 0 || symbolBytes.length > 16) { return false; // Check length is within range @@ -595,9 +603,9 @@ contract Hub is Circles { // todo: same check treasury is an ERC1155Receiver for receiving collateral require(_treasury != address(0), "Treasury address can not be zero."); // name must be ASCII alphanumeric and some special characters - require(_isValidName(_name), "Invalid group name."); + require(isValidName(_name), "Invalid group name."); // symbol must be ASCII alphanumeric and some special characters - require(_isValidSymbol(_symbol), "Invalid group symbol."); + require(isValidSymbol(_symbol), "Invalid group symbol."); // insert avatar into linked list; reverts if it already exists _insertAvatar(_avatar); From e245c95cd5fef37acc6393bbeafa04cf71ee916c Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Mon, 26 Feb 2024 19:08:01 +0000 Subject: [PATCH 02/15] (treasury): standard treasury --- src/groups/BaseMintPolicy.sol | 20 +++++++++++++++++++- src/groups/IMintPolicy.sol | 2 +- src/hub/Hub.sol | 2 +- src/treasury/standardTreasury.sol | 25 +++++++++++++++++++++++++ src/treasury/standardVault.sol | 1 + 5 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 src/treasury/standardTreasury.sol create mode 100644 src/treasury/standardVault.sol diff --git a/src/groups/BaseMintPolicy.sol b/src/groups/BaseMintPolicy.sol index 59393e3..d53d179 100644 --- a/src/groups/BaseMintPolicy.sol +++ b/src/groups/BaseMintPolicy.sol @@ -3,4 +3,22 @@ pragma solidity >=0.8.13; import "./IMintPolicy.sol"; -abstract contract MintPolicy is IMintPolicy {} +abstract contract MintPolicy is IMintPolicy { + /** + * @notice Simple mint policy that always returns true + * @param minter Address of the minter + * @param group Address of the group + * @param collateral Array of collateral addresses + * @param amounts Array of collateral amounts + * @param data Optional data bytes passed to mint policy + */ + function beforeMintPolicy( + address minter, + address group, + address[] calldata collateral, + uint256[] calldata amounts, + bytes calldata data + ) external virtual override returns (bool) { + return true; + } +} diff --git a/src/groups/IMintPolicy.sol b/src/groups/IMintPolicy.sol index 5a3cd2b..a78fa78 100644 --- a/src/groups/IMintPolicy.sol +++ b/src/groups/IMintPolicy.sol @@ -7,6 +7,6 @@ interface IMintPolicy { address group, address[] calldata collateral, uint256[] calldata amounts, - bytes calldata _data + bytes calldata data ) external returns (bool); } diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 1c7ee1c..69c8922 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -316,7 +316,7 @@ contract Hub is Circles { * @param _group address of the group avatar to mint Circles of * @param _collateral array of (personal or group) avatar addresses to be used as collateral * @param _amounts array of amounts of collateral to be used for minting - * @param _data (optional) additional data to be passed to the treasury and minter + * @param _data (optional) additional data to be passed to the mint policy, treasury and minter */ function groupMint( address _group, diff --git a/src/treasury/standardTreasury.sol b/src/treasury/standardTreasury.sol new file mode 100644 index 0000000..89ddbbf --- /dev/null +++ b/src/treasury/standardTreasury.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import "../proxy/ProxyFactory.sol"; + +contract standardTreasury is ERC1155Holder, ProxyFactory { + function onERC1155Received(address, address, uint256, uint256, bytes memory) + public + virtual + override + returns (bytes4) + { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived(address, address, uint256[] memory, uint256[] memory, bytes memory) + public + virtual + override + returns (bytes4) + { + return this.onERC1155BatchReceived.selector; + } +} diff --git a/src/treasury/standardVault.sol b/src/treasury/standardVault.sol new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/treasury/standardVault.sol @@ -0,0 +1 @@ + From 54d04bd01a90e2230f09d6cc8e060f78753ced4e Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Tue, 27 Feb 2024 16:29:27 +0000 Subject: [PATCH 03/15] (Hub): treasury needs group information --- src/hub/Hub.sol | 31 ++++++++++++++++++++++++++++--- src/hub/IHub.sol | 1 + src/treasury/standardTreasury.sol | 20 ++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 69c8922..0b0c246 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -8,6 +8,7 @@ import "../migration/IHub.sol"; import "../migration/IToken.sol"; import "../circles/Circles.sol"; import "../groups/IMintPolicy.sol"; +import "./IHub.sol"; /** * @title Hub v2 contract for Circles @@ -20,7 +21,7 @@ import "../groups/IMintPolicy.sol"; * It further allows to wrap any token into an inflationary or demurraged * ERC20 Circles contract. */ -contract Hub is Circles { +contract Hub is Circles, IHubV2 { // Type declarations /** @@ -34,6 +35,16 @@ contract Hub is Circles { uint96 expiry; } + struct Metadata { + MetadataType metadataType; + bytes metadata; + bytes erc1155UserData; + } + + struct GroupMintMetadata { + address group; + } + // Constants /** @@ -53,6 +64,13 @@ contract Hub is Circles { */ address public constant SENTINEL = address(0x1); + // Enums + + enum MetadataType { + NoMetadata, + GroupMint // safeTransferFrom initiated from group mint, appends GroupMintMetadata + } + // State variables /** @@ -346,9 +364,16 @@ contract Hub is Circles { "Mint policy rejected mint." ); - // note: treasury.on1155Received must implement but no further requirements - safeBatchTransferFrom(msg.sender, treasuries[_group], collateralCirclesIds, _amounts, _data); + // abi encode the group address into the data to send onwards to the treasury + bytes memory metadataGroup = abi.encode(GroupMintMetadata({group: _group})); + bytes memory dataWithGroup = abi.encode( + Metadata({metadataType: MetadataType.GroupMint, metadata: metadataGroup, erc1155UserData: _data}) + ); + + // note: treasury.on1155Received must implement and unpack the GroupMintMetadata to know the group + safeBatchTransferFrom(msg.sender, treasuries[_group], collateralCirclesIds, _amounts, dataWithGroup); + // mint group Circles to the sender and send the original _data onwards _mint(msg.sender, toTokenId(_group), sumAmounts, _data); } diff --git a/src/hub/IHub.sol b/src/hub/IHub.sol index 946b1b5..ed2d01f 100644 --- a/src/hub/IHub.sol +++ b/src/hub/IHub.sol @@ -3,4 +3,5 @@ pragma solidity >=0.8.13; interface IHubV2 { function avatars(address _avatar) external view returns (address); + function mintPolicies(address _avatar) external view returns (address); } diff --git a/src/treasury/standardTreasury.sol b/src/treasury/standardTreasury.sol index 89ddbbf..5dab59b 100644 --- a/src/treasury/standardTreasury.sol +++ b/src/treasury/standardTreasury.sol @@ -5,10 +5,29 @@ import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "../proxy/ProxyFactory.sol"; contract standardTreasury is ERC1155Holder, ProxyFactory { + // State variables + + address public immutable hub; + + // modifier + + modifier onlyHub() { + require(msg.sender == hub, "Treasury: caller is not the hub"); + _; + } + + // Constructor + + constructor(address _hub) { + require(_hub != address(0), "Hub address cannot be 0"); + hub = _hub; + } + function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual override + onlyHub returns (bytes4) { return this.onERC1155Received.selector; @@ -18,6 +37,7 @@ contract standardTreasury is ERC1155Holder, ProxyFactory { public virtual override + onlyHub returns (bytes4) { return this.onERC1155BatchReceived.selector; From c3da860ede8cec21f7c0f2580e4d70a2ff8592e7 Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Tue, 27 Feb 2024 16:33:26 +0000 Subject: [PATCH 04/15] (Definitions): move definitions to separate contract for use in treasury --- src/hub/Hub.sol | 26 +++++++------------------- src/hub/MetadataDefinitions.sol | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 src/hub/MetadataDefinitions.sol diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 0b0c246..9cced90 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -9,6 +9,7 @@ import "../migration/IToken.sol"; import "../circles/Circles.sol"; import "../groups/IMintPolicy.sol"; import "./IHub.sol"; +import "./MetadataDefinitions.sol"; /** * @title Hub v2 contract for Circles @@ -35,16 +36,6 @@ contract Hub is Circles, IHubV2 { uint96 expiry; } - struct Metadata { - MetadataType metadataType; - bytes metadata; - bytes erc1155UserData; - } - - struct GroupMintMetadata { - address group; - } - // Constants /** @@ -64,13 +55,6 @@ contract Hub is Circles, IHubV2 { */ address public constant SENTINEL = address(0x1); - // Enums - - enum MetadataType { - NoMetadata, - GroupMint // safeTransferFrom initiated from group mint, appends GroupMintMetadata - } - // State variables /** @@ -365,9 +349,13 @@ contract Hub is Circles, IHubV2 { ); // abi encode the group address into the data to send onwards to the treasury - bytes memory metadataGroup = abi.encode(GroupMintMetadata({group: _group})); + bytes memory metadataGroup = abi.encode(MetadataDefinitions.GroupMintMetadata({group: _group})); bytes memory dataWithGroup = abi.encode( - Metadata({metadataType: MetadataType.GroupMint, metadata: metadataGroup, erc1155UserData: _data}) + MetadataDefinitions.Metadata({ + metadataType: MetadataDefinitions.MetadataType.GroupMint, + metadata: metadataGroup, + erc1155UserData: _data + }) ); // note: treasury.on1155Received must implement and unpack the GroupMintMetadata to know the group diff --git a/src/hub/MetadataDefinitions.sol b/src/hub/MetadataDefinitions.sol new file mode 100644 index 0000000..8607f5e --- /dev/null +++ b/src/hub/MetadataDefinitions.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +contract MetadataDefinitions { + // Type declarations + + struct Metadata { + MetadataType metadataType; + bytes metadata; + bytes erc1155UserData; + } + + struct GroupMintMetadata { + address group; + } + + // Enums + + enum MetadataType { + NoMetadata, + GroupMint // safeTransferFrom initiated from group mint, appends GroupMintMetadata + } +} From 43b15aa3331cb12d6dee90962f8f022eedd71615 Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Tue, 27 Feb 2024 19:11:45 +0000 Subject: [PATCH 05/15] (treasury): start making vault master copy --- src/hub/Hub.sol | 6 +-- src/treasury/standardTreasury.sol | 69 ++++++++++++++++++++++++------- src/treasury/standardVault.sol | 53 ++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 19 deletions(-) diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 9cced90..4f0e1df 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity >=0.8.13; -import "openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import "openzeppelin-contracts/contracts/utils/Create2.sol"; +import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/utils/Create2.sol"; import "../migration/IHub.sol"; import "../migration/IToken.sol"; import "../circles/Circles.sol"; diff --git a/src/treasury/standardTreasury.sol b/src/treasury/standardTreasury.sol index 5dab59b..60dc22a 100644 --- a/src/treasury/standardTreasury.sol +++ b/src/treasury/standardTreasury.sol @@ -2,14 +2,33 @@ pragma solidity >=0.8.13; import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import "../proxy/ProxyFactory.sol"; +import "../hub/MetadataDefinitions.sol"; + +contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { + // Constants + + /** + * @dev The call prefix for the setup function on the vault contract + */ + bytes4 public constant STANDARD_VAULT_SETUP_CALLPREFIX = bytes4(keccak256("setup(address)")); -contract standardTreasury is ERC1155Holder, ProxyFactory { // State variables address public immutable hub; - // modifier + /** + * @notice Mapping of group address to vault address + * @dev The vault is the contract that holds the group's collateral + * todo: we could use deterministic vault addresses as to not store them + * but then we still need to check whether the correct code has been deployed + * so we might as well deploy and store the addresses? + */ + mapping(address => address) public vaults; + + // Modifiers modifier onlyHub() { require(msg.sender == hub, "Treasury: caller is not the hub"); @@ -23,23 +42,41 @@ contract standardTreasury is ERC1155Holder, ProxyFactory { hub = _hub; } - function onERC1155Received(address, address, uint256, uint256, bytes memory) - public - virtual - override - onlyHub - returns (bytes4) - { + // Public functions + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } + + function onERC1155Received( + address, /*_operator*/ + address, /*_from*/ + uint256, /*_id*/ + uint256, /*_value*/ + bytes memory /*_data*/ + ) public virtual override onlyHub returns (bytes4) { return this.onERC1155Received.selector; } - function onERC1155BatchReceived(address, address, uint256[] memory, uint256[] memory, bytes memory) - public - virtual - override - onlyHub - returns (bytes4) - { + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] memory _ids, + uint256[] memory _values, + bytes memory _data + ) public virtual override onlyHub returns (bytes4) { + MetadataDefinitions.Metadata memory metadata = abi.decode(_data, (MetadataDefinitions.Metadata)); + require(metadata.metadataType == MetadataDefinitions.MetadataType.GroupMint, "Treasury: Invalid metadata type"); + MetadataDefinitions.GroupMintMetadata memory groupMintMetadata = + abi.decode(metadata.metadata, (MetadataDefinitions.GroupMintMetadata)); + return this.onERC1155BatchReceived.selector; } + + // Internal functions + + function _getVault(address _group) internal returns (address) {} } diff --git a/src/treasury/standardVault.sol b/src/treasury/standardVault.sol index 8b13789..779e09a 100644 --- a/src/treasury/standardVault.sol +++ b/src/treasury/standardVault.sol @@ -1 +1,54 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; + +contract standardVault is ERC165, IERC1155Receiver { + // State variables + + address public standardTreasury; + + // Constructor + + constructor() { + standardTreasury = address(1); + } + + // External functions + + function setup(address _standardTreasury) external { + require(standardTreasury == address(0), "Vault contract has already been setup."); + require(_standardTreasury != address(0), "Treasury address must not be zero address"); + standardTreasury = _standardTreasury; + } + + // Public functions + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } + + function onERC1155Received( + address, /*_operator*/ + address, /*_from*/ + uint256, /*_id*/ + uint256, /*_value*/ + bytes memory /*_data*/ + ) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, /*_operator*/ + address, /*_from*/ + uint256[] memory, /*_ids*/ + uint256[] memory, /*_values*/ + bytes memory /*_data*/ + ) public virtual override returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } +} From 35c20073d084f980a1cb5b11ae5fba0ea991f4a1 Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Wed, 28 Feb 2024 19:48:56 +0000 Subject: [PATCH 06/15] (treasury): complete standard treasury and vaults basic logic --- src/graph/Graph.sol | 4 +- src/groups/IMintPolicy.sol | 8 +++ src/hub/Hub.sol | 16 +++++ src/hub/IHub.sol | 9 ++- src/proxy/ProxyFactory.sol | 2 +- src/treasury/IStandardVault.sol | 7 ++ src/treasury/standardTreasury.sol | 102 +++++++++++++++++++++++++----- src/treasury/standardVault.sol | 34 ++++++++-- 8 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 src/treasury/IStandardVault.sol diff --git a/src/graph/Graph.sol b/src/graph/Graph.sol index f656643..580e94f 100644 --- a/src/graph/Graph.sol +++ b/src/graph/Graph.sol @@ -230,7 +230,7 @@ contract Graph is ProxyFactory, IGraph { function registerAvatar() external notOnTrustGraph(msg.sender) { bytes memory avatarCircleNodeSetupData = abi.encodeWithSelector(AVATAR_CIRCLE_SETUP_CALLPREFIX, msg.sender); IAvatarCircleNode avatarCircleNode = - IAvatarCircleNode(address(createProxy(address(masterCopyAvatarCircleNode), avatarCircleNodeSetupData))); + IAvatarCircleNode(address(_createProxy(address(masterCopyAvatarCircleNode), avatarCircleNodeSetupData))); avatarToCircle[msg.sender] = avatarCircleNode; _insertAvatarCircleNode(avatarCircleNode); @@ -246,7 +246,7 @@ contract Graph is ProxyFactory, IGraph { bytes memory groupCircleNodeSetupData = abi.encodeWithSelector(GROUP_CIRCLE_SETUP_CALLPREFIX, msg.sender, _exitFee_64x64); IGroupCircleNode groupCircleNode = - IGroupCircleNode(address(createProxy(address(masterCopyGroupCircleNode), groupCircleNodeSetupData))); + IGroupCircleNode(address(_createProxy(address(masterCopyGroupCircleNode), groupCircleNodeSetupData))); groupToCircle[msg.sender] = groupCircleNode; _insertGroupCircleNode(groupCircleNode); diff --git a/src/groups/IMintPolicy.sol b/src/groups/IMintPolicy.sol index a78fa78..0190e57 100644 --- a/src/groups/IMintPolicy.sol +++ b/src/groups/IMintPolicy.sol @@ -9,4 +9,12 @@ interface IMintPolicy { uint256[] calldata amounts, bytes calldata data ) external returns (bool); + + function beforeRedeemPolicy(address operator, address redeemer, address group, uint256 value, bytes calldata data) + external + returns (uint256[] memory ids, uint256[] memory values); + + function beforeBurnPolicy(address burner, address group, uint256 value, bytes calldata data) + external + returns (bool); } diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 4f0e1df..f198c4f 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -379,6 +379,22 @@ contract Hub is Circles, IHubV2 { return (mintTime.lastMintTime == INDEFINITE_FUTURE); } + function burn(uint256 _id, uint256 _amount) external { + // todo: by construction we can not have an id with non-zero balance, + // that was not converted from a group address. + // for now, do a redundant check that the id is identical to the recovered address + address group = address(uint160(_id)); + require(uint256(uint160(group)) == _id, "Invalid Circles identifier."); + + IMintPolicy policy = IMintPolicy(mintPolicies[group]); + if (address(policy) != address(0) && treasuries[group] != msg.sender) { + // if Circles are a group Circles and if the burner is not the associated treasury, + // then the mint policy must approve the burn + require(policy.beforeBurnPolicy(msg.sender, group, _amount, ""), "Burn policy rejected burn."); + } + _burn(msg.sender, _id, _amount); + } + // check if path transfer can be fully ERC1155 compatible // note: matrix math needs to consider mints, otherwise it won't add up diff --git a/src/hub/IHub.sol b/src/hub/IHub.sol index ed2d01f..c2c8b19 100644 --- a/src/hub/IHub.sol +++ b/src/hub/IHub.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity >=0.8.13; -interface IHubV2 { - function avatars(address _avatar) external view returns (address); - function mintPolicies(address _avatar) external view returns (address); +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +interface IHubV2 is IERC1155 { + function avatars(address avatar) external view returns (address); + function mintPolicies(address avatar) external view returns (address); + function burn(uint256 id, uint256 amount) external; } diff --git a/src/proxy/ProxyFactory.sol b/src/proxy/ProxyFactory.sol index c6e7b94..312ea11 100644 --- a/src/proxy/ProxyFactory.sol +++ b/src/proxy/ProxyFactory.sol @@ -14,7 +14,7 @@ contract ProxyFactory { /// execute a message call to the new proxy within one transaction. /// @param masterCopy Address of master copy. /// @param data Payload for message call sent to new proxy contract. - function createProxy(address masterCopy, bytes memory data) internal returns (Proxy proxy) { + function _createProxy(address masterCopy, bytes memory data) internal returns (Proxy proxy) { proxy = new Proxy(masterCopy); if (data.length > 0) { // solhint-disable-next-line no-inline-assembly diff --git a/src/treasury/IStandardVault.sol b/src/treasury/IStandardVault.sol new file mode 100644 index 0000000..368c4b7 --- /dev/null +++ b/src/treasury/IStandardVault.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +interface IStandardVault { + function returnCollateral(address receiver, uint256[] calldata ids, uint256[] calldata values, bytes calldata data) + external; +} diff --git a/src/treasury/standardTreasury.sol b/src/treasury/standardTreasury.sol index 60dc22a..189cc0d 100644 --- a/src/treasury/standardTreasury.sol +++ b/src/treasury/standardTreasury.sol @@ -6,6 +6,9 @@ import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import "../proxy/ProxyFactory.sol"; import "../hub/MetadataDefinitions.sol"; +import "../hub/IHub.sol"; +import "../groups/IMintPolicy.sol"; +import "./IStandardVault.sol"; contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { // Constants @@ -13,11 +16,19 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { /** * @dev The call prefix for the setup function on the vault contract */ - bytes4 public constant STANDARD_VAULT_SETUP_CALLPREFIX = bytes4(keccak256("setup(address)")); + bytes4 public constant STANDARD_VAULT_SETUP_CALLPREFIX = bytes4(keccak256("setup()")); // State variables - address public immutable hub; + /** + * @notice Address of the hub contract + */ + IHubV2 public immutable hub; + + /** + * @notice Address of the mastercopy standard vault contract + */ + address public immutable mastercopyStandardVault; /** * @notice Mapping of group address to vault address @@ -26,20 +37,22 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { * but then we still need to check whether the correct code has been deployed * so we might as well deploy and store the addresses? */ - mapping(address => address) public vaults; + mapping(address => IStandardVault) public vaults; // Modifiers modifier onlyHub() { - require(msg.sender == hub, "Treasury: caller is not the hub"); + require(msg.sender == address(hub), "Treasury: caller is not the hub"); _; } // Constructor - constructor(address _hub) { - require(_hub != address(0), "Hub address cannot be 0"); + constructor(IHubV2 _hub, address _mastercopyStandardVault) { + require(address(_hub) != address(0), "Hub address cannot be 0"); + require(_mastercopyStandardVault != address(0), "Mastercopy standard vault address cannot be 0"); hub = _hub; + mastercopyStandardVault = _mastercopyStandardVault; } // Public functions @@ -51,32 +64,89 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); } - function onERC1155Received( - address, /*_operator*/ - address, /*_from*/ - uint256, /*_id*/ - uint256, /*_value*/ - bytes memory /*_data*/ - ) public virtual override onlyHub returns (bytes4) { + /** + * @dev Exclusively use single received for receiving group Circles to redeem them + * for collateral Circles according to the group mint policy + */ + function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes memory _data) + public + virtual + override + returns (bytes4) + { + address group = _validateCirclesIdToGroup(_id); + IStandardVault vault = vaults[group]; + require(address(vault) != address(0), "Treasury: Group has no vault"); + + // query the hub for the mint policy + IMintPolicy policy = IMintPolicy(hub.mintPolicies(group)); + require(address(policy) != address(0), "Treasury: Invalid group without mint policy"); + + // query the mint policy for the redemption values + uint256[] memory redemptionIds; + uint256[] memory redemptionValues; + (redemptionIds, redemptionValues) = policy.beforeRedeemPolicy(_operator, _from, group, _value, _data); + + // ensure the redemption values sum up to the correct amount + uint256 sum = 0; + for (uint256 i = 0; i < redemptionValues.length; i++) { + sum += redemptionValues[i]; + } + require(sum == _value, "Treasury: Invalid redemption values from policy"); + + // burn the group Circles + hub.burn(_id, _value); + + // return collateral Circles to the redeemer of group Circles + vault.returnCollateral(_from, redemptionIds, redemptionValues, _data); + return this.onERC1155Received.selector; } + /** + * @dev Exclusively use batch received for receiving collateral Circles + * from the hub contract during group minting + */ function onERC1155BatchReceived( - address _operator, + address, /*_operator*/ address _from, uint256[] memory _ids, uint256[] memory _values, bytes memory _data ) public virtual override onlyHub returns (bytes4) { + // decode the data to get the group address MetadataDefinitions.Metadata memory metadata = abi.decode(_data, (MetadataDefinitions.Metadata)); require(metadata.metadataType == MetadataDefinitions.MetadataType.GroupMint, "Treasury: Invalid metadata type"); MetadataDefinitions.GroupMintMetadata memory groupMintMetadata = abi.decode(metadata.metadata, (MetadataDefinitions.GroupMintMetadata)); - + // ensure the vault exists + address vault = address(_ensureVault(groupMintMetadata.group)); + // forward the Circles to the vault + hub.safeBatchTransferFrom(address(this), vault, _ids, _values, metadata.erc1155UserData); return this.onERC1155BatchReceived.selector; } // Internal functions - function _getVault(address _group) internal returns (address) {} + function _validateCirclesIdToGroup(uint256 _id) internal pure returns (address) { + address group = address(uint160(_id)); + require(uint256(uint160(group)) == _id, "Treasury: Invalid group Circles id"); + return group; + } + + function _ensureVault(address _group) internal returns (IStandardVault) { + IStandardVault vault = vaults[_group]; + if (address(vault) == address(0)) { + vault = _deployVault(); + vaults[_group] = vault; + } + return vault; + } + + // todo: this could be done with deterministic deployment, but same comment, not worth it + function _deployVault() internal returns (IStandardVault) { + bytes memory vaultSetupData = abi.encodeWithSelector(STANDARD_VAULT_SETUP_CALLPREFIX); + IStandardVault vault = IStandardVault(address(_createProxy(mastercopyStandardVault, vaultSetupData))); + return vault; + } } diff --git a/src/treasury/standardVault.sol b/src/treasury/standardVault.sol index 779e09a..6fd1804 100644 --- a/src/treasury/standardVault.sol +++ b/src/treasury/standardVault.sol @@ -3,12 +3,23 @@ pragma solidity >=0.8.13; import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import "../hub/IHub.sol"; +import "./IStandardVault.sol"; -contract standardVault is ERC165, IERC1155Receiver { +contract standardVault is ERC165, IERC1155Receiver, IStandardVault { // State variables address public standardTreasury; + IHubV2 public hub; + + // Modifiers + + modifier onlyTreasury() { + require(msg.sender == standardTreasury, "Vault: caller is not the treasury"); + _; + } + // Constructor constructor() { @@ -17,10 +28,22 @@ contract standardVault is ERC165, IERC1155Receiver { // External functions - function setup(address _standardTreasury) external { - require(standardTreasury == address(0), "Vault contract has already been setup."); - require(_standardTreasury != address(0), "Treasury address must not be zero address"); - standardTreasury = _standardTreasury; + function setup(IHubV2 _hub) external { + require(address(hub) == address(0), "Vault: already initialized"); + standardTreasury = msg.sender; + hub = _hub; + } + + function returnCollateral( + address _receiver, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external onlyTreasury { + require(_receiver != address(0), "Vault: receiver cannot be 0 address"); + + // return the collateral to the receiver + hub.safeBatchTransferFrom(address(this), _receiver, _ids, _values, _data); } // Public functions @@ -49,6 +72,7 @@ contract standardVault is ERC165, IERC1155Receiver { uint256[] memory, /*_values*/ bytes memory /*_data*/ ) public virtual override returns (bytes4) { + // todo: register which collateral is stored in this vault? return this.onERC1155BatchReceived.selector; } } From 0c3af1af406adaad444dc93a5fe68e5dfcb0c02d Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Wed, 28 Feb 2024 19:51:29 +0000 Subject: [PATCH 07/15] (MetadataDefinitions): ... why is fmt passing locally, but not on CI --- src/hub/MetadataDefinitions.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hub/MetadataDefinitions.sol b/src/hub/MetadataDefinitions.sol index 8607f5e..2192cc0 100644 --- a/src/hub/MetadataDefinitions.sol +++ b/src/hub/MetadataDefinitions.sol @@ -18,6 +18,6 @@ contract MetadataDefinitions { enum MetadataType { NoMetadata, - GroupMint // safeTransferFrom initiated from group mint, appends GroupMintMetadata + GroupMint } } From 40bba2d8b2c30a90a92c0e4a74bf5a41a802e3e6 Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Wed, 28 Feb 2024 20:00:18 +0000 Subject: [PATCH 08/15] (Treasury): correct vault setup calldata --- src/treasury/standardTreasury.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/treasury/standardTreasury.sol b/src/treasury/standardTreasury.sol index 189cc0d..a873ff3 100644 --- a/src/treasury/standardTreasury.sol +++ b/src/treasury/standardTreasury.sol @@ -16,7 +16,7 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { /** * @dev The call prefix for the setup function on the vault contract */ - bytes4 public constant STANDARD_VAULT_SETUP_CALLPREFIX = bytes4(keccak256("setup()")); + bytes4 public constant STANDARD_VAULT_SETUP_CALLPREFIX = bytes4(keccak256("setup(address)")); // State variables @@ -145,7 +145,7 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { // todo: this could be done with deterministic deployment, but same comment, not worth it function _deployVault() internal returns (IStandardVault) { - bytes memory vaultSetupData = abi.encodeWithSelector(STANDARD_VAULT_SETUP_CALLPREFIX); + bytes memory vaultSetupData = abi.encodeWithSelector(STANDARD_VAULT_SETUP_CALLPREFIX, hub); IStandardVault vault = IStandardVault(address(_createProxy(mastercopyStandardVault, vaultSetupData))); return vault; } From 03e667e305b4054aa68a43c72547d7ad33861b04 Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Wed, 28 Feb 2024 20:03:21 +0000 Subject: [PATCH 09/15] (root): remove remappings.txt --- remappings.txt | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 remappings.txt diff --git a/remappings.txt b/remappings.txt deleted file mode 100644 index c36eea0..0000000 --- a/remappings.txt +++ /dev/null @@ -1,4 +0,0 @@ -ds-test/=lib/forge-std/lib/ds-test/src/ -erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ -forge-std/=lib/forge-std/src/ -@openzeppelin/=lib/openzeppelin-contracts/ From 5cdac051edc6a44dca6bf0784fca3fbfa196b91a Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Thu, 29 Feb 2024 11:42:09 +0000 Subject: [PATCH 10/15] (test): probably won't work, set a commit for foundry --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91bcba0..d48aa48 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: nightly-5b7e4cb3c882b28f3c32ba580de27ce7381f415a - name: Check Code Formatting run: | From ab12b78ccb7cefd56c462e13b436a3765d53d7ed Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Thu, 29 Feb 2024 11:59:05 +0000 Subject: [PATCH 11/15] (test): use a different version of solc 0.8.21 - although locally 0.8.23 as before builds just fine... --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d48aa48..7f04798 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: - name: Run Forge build run: | forge --version - forge build --sizes + forge build --sizes --use 0.8.21 id: build - name: Run Forge tests From f7d0c772633b69643a95872e1a4f69c8748d8ea6 Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Thu, 29 Feb 2024 12:01:49 +0000 Subject: [PATCH 12/15] (forge): add remappings.txt --- remappings.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 remappings.txt diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..f810763 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,5 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +ds-test/=lib/forge-std/lib/ds-test/src/ +erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ +forge-std/=lib/forge-std/src/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ From e8215cd6ca3503c4e1e5a65ae34c67961823787e Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Thu, 29 Feb 2024 13:15:51 +0000 Subject: [PATCH 13/15] (CI): disable --sizes on forge build for now; worry about contract size later --- .github/workflows/test.yml | 4 ++-- remappings.txt | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 remappings.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f04798..0bbac1d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly-5b7e4cb3c882b28f3c32ba580de27ce7381f415a + version: nightly - name: Check Code Formatting run: | @@ -35,7 +35,7 @@ jobs: - name: Run Forge build run: | forge --version - forge build --sizes --use 0.8.21 + forge build id: build - name: Run Forge tests diff --git a/remappings.txt b/remappings.txt deleted file mode 100644 index f810763..0000000 --- a/remappings.txt +++ /dev/null @@ -1,5 +0,0 @@ -@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ -ds-test/=lib/forge-std/lib/ds-test/src/ -erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ -forge-std/=lib/forge-std/src/ -openzeppelin-contracts/=lib/openzeppelin-contracts/ From 2edf51dcfb2b754ae9751e2365f734be27aa062a Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Thu, 29 Feb 2024 19:12:24 +0000 Subject: [PATCH 14/15] (groups): base redemption policy is: return what was asked for --- src/groups/BaseMintPolicy.sol | 61 ++++++++++++++++++++++++++----- src/groups/Definitions.sol | 11 ++++++ src/groups/IMintPolicy.sol | 7 +++- src/treasury/IStandardVault.sol | 2 + src/treasury/standardTreasury.sol | 12 +++++- src/treasury/standardVault.sol | 54 +++++++++++++-------------- 6 files changed, 107 insertions(+), 40 deletions(-) create mode 100644 src/groups/Definitions.sol diff --git a/src/groups/BaseMintPolicy.sol b/src/groups/BaseMintPolicy.sol index d53d179..424a6fc 100644 --- a/src/groups/BaseMintPolicy.sol +++ b/src/groups/BaseMintPolicy.sol @@ -2,23 +2,64 @@ pragma solidity >=0.8.13; import "./IMintPolicy.sol"; +import "./Definitions.sol"; abstract contract MintPolicy is IMintPolicy { /** * @notice Simple mint policy that always returns true - * @param minter Address of the minter - * @param group Address of the group - * @param collateral Array of collateral addresses - * @param amounts Array of collateral amounts - * @param data Optional data bytes passed to mint policy + * @param _minter Address of the minter + * @param _group Address of the group + * @param _collateral Array of collateral addresses + * @param _amounts Array of collateral amounts + * @param _data Optional data bytes passed to mint policy */ function beforeMintPolicy( - address minter, - address group, - address[] calldata collateral, - uint256[] calldata amounts, - bytes calldata data + address _minter, + address _group, + address[] calldata _collateral, + uint256[] calldata _amounts, + bytes calldata _data ) external virtual override returns (bool) { return true; } + + function beforeBurnPolicy(address _burner, address _group, uint256 _value, bytes calldata _data) + external + virtual + override + returns (bool) + { + return true; + } + + function beforeRedeemPolicy( + address _operator, + address _redeemer, + address _group, + uint256 _value, + bytes calldata _data + ) + external + virtual + override + returns ( + uint256[] memory _ids, + uint256[] memory _values, + uint256[] memory _burnIds, + uint256[] memory _burnValues + ) + { + // simplest policy is to return the collateral as the caller requests it in data + BaseMintPolicyDefinitions.BaseRedemptionPolicy memory redemption = + abi.decode(_data, (BaseMintPolicyDefinitions.BaseRedemptionPolicy)); + + // and no collateral gets burnt upon redemption + _burnIds = new uint256[](0); + _burnValues = new uint256[](0); + + // standard treasury checks whether the total sums add up to the amount of group Circles redeemed + // so we can simply decode and pass the request back to treasury. + // The redemption will fail if it does not contain (sufficient of) these Circles + return (redemption.redemptionIds, redemption.redemptionValues, _burnIds, _burnValues); + } } diff --git a/src/groups/Definitions.sol b/src/groups/Definitions.sol new file mode 100644 index 0000000..5639c35 --- /dev/null +++ b/src/groups/Definitions.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +contract BaseMintPolicyDefinitions { + // Type declarations + + struct BaseRedemptionPolicy { + uint256[] redemptionIds; + uint256[] redemptionValues; + } +} diff --git a/src/groups/IMintPolicy.sol b/src/groups/IMintPolicy.sol index 0190e57..776120d 100644 --- a/src/groups/IMintPolicy.sol +++ b/src/groups/IMintPolicy.sol @@ -12,7 +12,12 @@ interface IMintPolicy { function beforeRedeemPolicy(address operator, address redeemer, address group, uint256 value, bytes calldata data) external - returns (uint256[] memory ids, uint256[] memory values); + returns ( + uint256[] memory redemptionIds, + uint256[] memory redemptionValues, + uint256[] memory burnIds, + uint256[] memory burnValues + ); function beforeBurnPolicy(address burner, address group, uint256 value, bytes calldata data) external diff --git a/src/treasury/IStandardVault.sol b/src/treasury/IStandardVault.sol index 368c4b7..20b1dd8 100644 --- a/src/treasury/IStandardVault.sol +++ b/src/treasury/IStandardVault.sol @@ -4,4 +4,6 @@ pragma solidity >=0.8.13; interface IStandardVault { function returnCollateral(address receiver, uint256[] calldata ids, uint256[] calldata values, bytes calldata data) external; + + function burnCollateral(uint256[] calldata ids, uint256[] calldata values) external; } diff --git a/src/treasury/standardTreasury.sol b/src/treasury/standardTreasury.sol index a873ff3..d8d569f 100644 --- a/src/treasury/standardTreasury.sol +++ b/src/treasury/standardTreasury.sol @@ -85,13 +85,19 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { // query the mint policy for the redemption values uint256[] memory redemptionIds; uint256[] memory redemptionValues; - (redemptionIds, redemptionValues) = policy.beforeRedeemPolicy(_operator, _from, group, _value, _data); + uint256[] memory burnIds; + uint256[] memory burnValues; + (redemptionIds, redemptionValues, burnIds, burnValues) = + policy.beforeRedeemPolicy(_operator, _from, group, _value, _data); // ensure the redemption values sum up to the correct amount uint256 sum = 0; for (uint256 i = 0; i < redemptionValues.length; i++) { sum += redemptionValues[i]; } + for (uint256 i = 0; i < burnValues.length; i++) { + sum += burnValues[i]; + } require(sum == _value, "Treasury: Invalid redemption values from policy"); // burn the group Circles @@ -100,6 +106,10 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { // return collateral Circles to the redeemer of group Circles vault.returnCollateral(_from, redemptionIds, redemptionValues, _data); + // burn the collateral Circles from the vault + vault.burnCollateral(burnIds, burnValues); + + // return the ERC1155 selector for acceptance of the (redeemed) group Circles return this.onERC1155Received.selector; } diff --git a/src/treasury/standardVault.sol b/src/treasury/standardVault.sol index 6fd1804..af5dd41 100644 --- a/src/treasury/standardVault.sol +++ b/src/treasury/standardVault.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity >=0.8.13; -import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "../hub/IHub.sol"; import "./IStandardVault.sol"; -contract standardVault is ERC165, IERC1155Receiver, IStandardVault { +contract standardVault is ERC1155Holder, IStandardVault { // State variables address public standardTreasury; @@ -22,18 +21,33 @@ contract standardVault is ERC165, IERC1155Receiver, IStandardVault { // Constructor + /** + * @notice Constructor to create a standard vault master copy. + */ constructor() { + // set the standard treasury to a blocked address for the master copy deployment standardTreasury = address(1); } // External functions + /** + * @notice Setup the vault + * @param _hub Address of the hub contract + */ function setup(IHubV2 _hub) external { require(address(hub) == address(0), "Vault: already initialized"); standardTreasury = msg.sender; hub = _hub; } + /** + * Return the collateral to the receiver can only be called by the treasury + * @param _receiver Receivere address of the collateral + * @param _ids Circles identifiers of the collateral + * @param _values Values of the collateral to be returned + * @param _data Optional data bytes passed to the receiver + */ function returnCollateral( address _receiver, uint256[] calldata _ids, @@ -46,33 +60,17 @@ contract standardVault is ERC165, IERC1155Receiver, IStandardVault { hub.safeBatchTransferFrom(address(this), _receiver, _ids, _values, _data); } - // Public functions - /** - * @dev See {IERC165-supportsInterface}. + * @notice Burn collateral from the vault can only ve called by the treasury + * @param _ids Circles identifiers of the collateral + * @param _values Values of the collateral to be burnt */ - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { - return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); - } - - function onERC1155Received( - address, /*_operator*/ - address, /*_from*/ - uint256, /*_id*/ - uint256, /*_value*/ - bytes memory /*_data*/ - ) public virtual override returns (bytes4) { - return this.onERC1155Received.selector; - } + function burnCollateral(uint256[] calldata _ids, uint256[] calldata _values) external onlyTreasury { + require(_ids.length == _values.length, "Vault: ids and values length mismatch"); - function onERC1155BatchReceived( - address, /*_operator*/ - address, /*_from*/ - uint256[] memory, /*_ids*/ - uint256[] memory, /*_values*/ - bytes memory /*_data*/ - ) public virtual override returns (bytes4) { - // todo: register which collateral is stored in this vault? - return this.onERC1155BatchReceived.selector; + // burn the collateral from the vault + for (uint256 i = 0; i < _ids.length; i++) { + hub.burn(_ids[i], _values[i]); + } } } From 67b1d3ce13872fd82687c1331847e8f6b6395fc8 Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Thu, 29 Feb 2024 19:44:09 +0000 Subject: [PATCH 15/15] (docs): add docs to completed functions --- src/groups/BaseMintPolicy.sol | 39 +++++++++++++++---------------- src/groups/Definitions.sol | 3 +++ src/hub/Hub.sol | 18 ++++++++++++-- src/hub/IHub.sol | 2 +- src/treasury/IStandardVault.sol | 3 +-- src/treasury/standardTreasury.sol | 28 +++++++++++++++++++--- src/treasury/standardVault.sol | 17 ++++++++++++-- 7 files changed, 80 insertions(+), 30 deletions(-) diff --git a/src/groups/BaseMintPolicy.sol b/src/groups/BaseMintPolicy.sol index 424a6fc..060dfc6 100644 --- a/src/groups/BaseMintPolicy.sol +++ b/src/groups/BaseMintPolicy.sol @@ -5,38 +5,37 @@ import "./IMintPolicy.sol"; import "./Definitions.sol"; abstract contract MintPolicy is IMintPolicy { + // External functions + /** * @notice Simple mint policy that always returns true - * @param _minter Address of the minter - * @param _group Address of the group - * @param _collateral Array of collateral addresses - * @param _amounts Array of collateral amounts - * @param _data Optional data bytes passed to mint policy */ function beforeMintPolicy( - address _minter, - address _group, - address[] calldata _collateral, - uint256[] calldata _amounts, - bytes calldata _data + address, /*_minter*/ + address, /*_group*/ + address[] calldata, /*_collateral*/ + uint256[] calldata, /*_amounts*/ + bytes calldata /*_data*/ ) external virtual override returns (bool) { return true; } - function beforeBurnPolicy(address _burner, address _group, uint256 _value, bytes calldata _data) - external - virtual - override - returns (bool) - { + /** + * @notice Simple burn policy that always returns true + */ + function beforeBurnPolicy(address, address, uint256, bytes calldata) external virtual override returns (bool) { return true; } + /** + * @notice Simple redeem policy that returns the redemption ids and values as requested in the data + * @param _data Optional data bytes passed to redeem policy + */ function beforeRedeemPolicy( - address _operator, - address _redeemer, - address _group, - uint256 _value, + address, /*_operator*/ + address, /*_redeemer*/ + address, /*_group*/ + uint256, /*_value*/ bytes calldata _data ) external diff --git a/src/groups/Definitions.sol b/src/groups/Definitions.sol index 5639c35..0790db7 100644 --- a/src/groups/Definitions.sol +++ b/src/groups/Definitions.sol @@ -4,6 +4,9 @@ pragma solidity >=0.8.13; contract BaseMintPolicyDefinitions { // Type declarations + /** + * @notice Base redemption policy to user specify desired collateral to redeem + */ struct BaseRedemptionPolicy { uint256[] redemptionIds; uint256[] redemptionValues; diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index f198c4f..0b2f66f 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -365,6 +365,10 @@ contract Hub is Circles, IHubV2 { _mint(msg.sender, toTokenId(_group), sumAmounts, _data); } + /** + * @notice Stop allows to stop future mints of personal Circles for this avatar. + * Must be called by the avatar itself. This action is irreversible. + */ function stop() external { require(isHuman(msg.sender), "Only human can call stop."); MintTime storage mintTime = mintTimes[msg.sender]; @@ -373,13 +377,23 @@ contract Hub is Circles, IHubV2 { mintTime.lastMintTime = INDEFINITE_FUTURE; } + /** + * Stopped checks whether the avatar has stopped future mints of personal Circles. + * @param _human address of avatar of the human to check whether it is stopped + */ function stopped(address _human) external view returns (bool) { require(isHuman(_human), "Only personal Circles can stopped or not stopped."); MintTime storage mintTime = mintTimes[msg.sender]; return (mintTime.lastMintTime == INDEFINITE_FUTURE); } - function burn(uint256 _id, uint256 _amount) external { + /** + * @notice Burn allows to burn Circles owned by the caller. + * @param _id Circles identifier of the Circles to burn + * @param _amount amount of Circles to burn + * @param _data (optional) additional data to be passed to the burn policy if they are group Circles + */ + function burn(uint256 _id, uint256 _amount, bytes calldata _data) external { // todo: by construction we can not have an id with non-zero balance, // that was not converted from a group address. // for now, do a redundant check that the id is identical to the recovered address @@ -390,7 +404,7 @@ contract Hub is Circles, IHubV2 { if (address(policy) != address(0) && treasuries[group] != msg.sender) { // if Circles are a group Circles and if the burner is not the associated treasury, // then the mint policy must approve the burn - require(policy.beforeBurnPolicy(msg.sender, group, _amount, ""), "Burn policy rejected burn."); + require(policy.beforeBurnPolicy(msg.sender, group, _amount, _data), "Burn policy rejected burn."); } _burn(msg.sender, _id, _amount); } diff --git a/src/hub/IHub.sol b/src/hub/IHub.sol index c2c8b19..e2307f4 100644 --- a/src/hub/IHub.sol +++ b/src/hub/IHub.sol @@ -6,5 +6,5 @@ import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; interface IHubV2 is IERC1155 { function avatars(address avatar) external view returns (address); function mintPolicies(address avatar) external view returns (address); - function burn(uint256 id, uint256 amount) external; + function burn(uint256 id, uint256 amount, bytes calldata data) external; } diff --git a/src/treasury/IStandardVault.sol b/src/treasury/IStandardVault.sol index 20b1dd8..74ab2ac 100644 --- a/src/treasury/IStandardVault.sol +++ b/src/treasury/IStandardVault.sol @@ -4,6 +4,5 @@ pragma solidity >=0.8.13; interface IStandardVault { function returnCollateral(address receiver, uint256[] calldata ids, uint256[] calldata values, bytes calldata data) external; - - function burnCollateral(uint256[] calldata ids, uint256[] calldata values) external; + function burnCollateral(uint256[] calldata ids, uint256[] calldata values, bytes calldata data) external; } diff --git a/src/treasury/standardTreasury.sol b/src/treasury/standardTreasury.sol index d8d569f..5c4c777 100644 --- a/src/treasury/standardTreasury.sol +++ b/src/treasury/standardTreasury.sol @@ -41,6 +41,9 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { // Modifiers + /** + * @notice Ensure the caller is the hub + */ modifier onlyHub() { require(msg.sender == address(hub), "Treasury: caller is not the hub"); _; @@ -48,6 +51,11 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { // Constructor + /** + * @notice Constructor to create a standard treasury + * @param _hub Address of the hub contract + * @param _mastercopyStandardVault Address of the mastercopy standard vault contract + */ constructor(IHubV2 _hub, address _mastercopyStandardVault) { require(address(_hub) != address(0), "Hub address cannot be 0"); require(_mastercopyStandardVault != address(0), "Mastercopy standard vault address cannot be 0"); @@ -101,13 +109,13 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { require(sum == _value, "Treasury: Invalid redemption values from policy"); // burn the group Circles - hub.burn(_id, _value); + hub.burn(_id, _value, _data); // return collateral Circles to the redeemer of group Circles vault.returnCollateral(_from, redemptionIds, redemptionValues, _data); // burn the collateral Circles from the vault - vault.burnCollateral(burnIds, burnValues); + vault.burnCollateral(burnIds, burnValues, _data); // return the ERC1155 selector for acceptance of the (redeemed) group Circles return this.onERC1155Received.selector; @@ -119,7 +127,7 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { */ function onERC1155BatchReceived( address, /*_operator*/ - address _from, + address, /*_from*/ uint256[] memory _ids, uint256[] memory _values, bytes memory _data @@ -138,12 +146,22 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { // Internal functions + /** + * @dev Validate the Circles id to group address + * @param _id Circles identifier + * @return group Address of the group + */ function _validateCirclesIdToGroup(uint256 _id) internal pure returns (address) { address group = address(uint160(_id)); require(uint256(uint160(group)) == _id, "Treasury: Invalid group Circles id"); return group; } + /** + * @dev Ensure the vault exists for the group, and if not deploy it + * @param _group Address of the group + * @return vault Address of the vault + */ function _ensureVault(address _group) internal returns (IStandardVault) { IStandardVault vault = vaults[_group]; if (address(vault) == address(0)) { @@ -154,6 +172,10 @@ contract standardTreasury is ERC165, IERC1155Receiver, ProxyFactory { } // todo: this could be done with deterministic deployment, but same comment, not worth it + /** + * @dev Deploy the vault + * @return vault Address of the vault + */ function _deployVault() internal returns (IStandardVault) { bytes memory vaultSetupData = abi.encodeWithSelector(STANDARD_VAULT_SETUP_CALLPREFIX, hub); IStandardVault vault = IStandardVault(address(_createProxy(mastercopyStandardVault, vaultSetupData))); diff --git a/src/treasury/standardVault.sol b/src/treasury/standardVault.sol index af5dd41..3495250 100644 --- a/src/treasury/standardVault.sol +++ b/src/treasury/standardVault.sol @@ -8,12 +8,21 @@ import "./IStandardVault.sol"; contract standardVault is ERC1155Holder, IStandardVault { // State variables + /** + * @notice Address of the standard treasury + */ address public standardTreasury; + /** + * @notice Address of the hub contract + */ IHubV2 public hub; // Modifiers + /** + * @notice Ensure the caller is the standard treasury + */ modifier onlyTreasury() { require(msg.sender == standardTreasury, "Vault: caller is not the treasury"); _; @@ -64,13 +73,17 @@ contract standardVault is ERC1155Holder, IStandardVault { * @notice Burn collateral from the vault can only ve called by the treasury * @param _ids Circles identifiers of the collateral * @param _values Values of the collateral to be burnt + * @param _data Optional data bytes passed to the hub and policy for burning */ - function burnCollateral(uint256[] calldata _ids, uint256[] calldata _values) external onlyTreasury { + function burnCollateral(uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) + external + onlyTreasury + { require(_ids.length == _values.length, "Vault: ids and values length mismatch"); // burn the collateral from the vault for (uint256 i = 0; i < _ids.length; i++) { - hub.burn(_ids[i], _values[i]); + hub.burn(_ids[i], _values[i], _data); } } }