diff --git a/README.md b/README.md index ba3f2ca..571058e 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,44 @@ Smart account with programmable ownership -#### Transferrable ownership +#### Token-bound ownership -- [ERC721Mech.sol](contracts/ERC721Mech.sol): allow the holder of a designated ERC-721 NFT to sign transactions on behalf of the Mech +- [ERC721TokenboundMech.sol](contracts/ERC721TokenboundMech.sol): allow the holder of a designated ERC-721 NFT to operate the Mech +- [ERC1155TokenboundMech.sol](contracts/ERC721TokenboundMech.sol): allow the holder of a designated ERC-1155 NFT to operate the Mech #### Threshold ownership -- [ERC1155Mech.sol](contracts/ERC1155Mech.sol): allow holders of a minimum balance of ERC-1155 tokens to sign transactions on behalf of the Mech +- [ERC20ThresholdMech.sol](contracts/ERC20ThresholdMech.sol): allow holders of a minimum balance of an ERC-20 token to operate the Mech +- [ERC1155ThresholdMech.sol](contracts/ERC1155ThresholdMech.sol): allow holders of a minimum balances of designated ERC-1155 tokens to operate the Mech #### Programmable ownership - [ZodiacMech.sol](contracts/ZodiacMech.sol): allow enabled [zodiac](https://github.com/gnosis/zodiac) modules to sign transactions on behalf of the Mech - [Mech.sol](contracts/base/Mech.sol): implement custom ownership terms by extending this abstract contract +![mech hierarchy](docs/mech-hierarchy.png) + +## Mech interface + +Mech implements the [EIP-4337](https://eips.ethereum.org/EIPS/eip-4337) account interface, [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271), and the following functions: + +### `isOperator(address signer)` + +Returns true if `signer` is allowed to operate the Mech. +Sub classes implement this function for defining the specific operator criteria. + +### `execute(address to, uint256 value, bytes data, uint8 operation)` + +Allows the operator to make the Mech execute a transaction. + +- `operation: 0` for a regular call +- `operation: 1` for a delegate call + +### `execute(address to, uint256 value, bytes data, uint8 operation, uint256 txGas)` + +Allows the operator to make the Mech execute a transaction restricting the gas amount made available to the direct execution of the internal meta transaction. +Any remaining transaction gas must only be spent for surrounding checks of the operator criteria. + ## Contribute The repo is structured as a monorepo with `mech-contracts` as the container package exporting the contract sources and artifacts. @@ -67,11 +92,11 @@ Integration tests are run on a mainnet fork and cover the interaction of mech co ## How it works -### EIP-4337 account +### EIP-4337 account abstraction -Mechs implement the EIP-4337 [Account](contracts/base/Account.sol) interface meaning they allow bundlers to execute account-abstracted user operations from the Mech's address. +Mech implements the EIP-4337 [Account](contracts/base/Account.sol) interface meaning they allow bundlers to execute account-abstracted user operations from the Mech's address. For this purpose the EIP-4337 entry point contract first calls the Mech's `validateUserOp()` function for checking if a user operation has a valid signature by the mech operator. -The entry point then calls the `exec` function, or any other function using the `onlyOperator` modifier, to trigger execution. +The entry point then calls the `execute` function, or any other function using the `onlyOperator` modifier, to trigger execution. ### EIP-1271 signatures @@ -95,18 +120,22 @@ An EIP-1271 signature will be considered valid if it meets the following conditi ### Deterministic deployment -The idea for the ERC721 and ERC1155 mechs is that the mech instance for the designated tokens is deployed to a deterministic address. +The idea for the token-bound versions of mech is that the mech instance for a designated token is deployed to an address that can be deterministically derived from the token contract address and token ID. This enables counterfactually funding the mech account (own token to unlock treasure) or granting access for it (use token as key card). -The deterministic deployment is implemented via Zodiac's [ModuleProxyFactory](https://github.com/gnosis/zodiac/blob/master/contracts/factory/ModuleProxyFactory.sol), through which each mech instance is deployed as an ERC-1167 minimal proxy. -### Immutable storage +### EIP-6551 token-bound account + +The token-bound versions of Mech adopts the [EIP-6551](https://eips.ethereum.org/EIPS/eip-6551) standard. +This means that these kinds of mechs are deployed through the official 6551 account registry, so they are deployed to their canonical addresses and can be detected by compatible tools. + +### EIP-1167 minimal proxies with context The holder of the token gains full control over the mech account and can write to its storage without any restrictions via delegate calls. Since tokens are transferrable this is problematic, as a past owner could mess with storage to change the mech's behavior in ways that future owners wouldn't expect. -That's why the ERC721 and ERC1155 versions of the mech avoid using storage but hard-code their configuration in bytecode. +That's why the ERC721 and ERC1155 versions of mech avoid using storage but instead solely rely on the immutable data in their own bytecode. -To achieve this, Mech sub contracts can extend [ImmutableStorage](contracts/base/ImmutableStorage.sol) which allows writing data to the bytecode at a deterministic address once. -Note that using Solidity's `immutable` keyword is not an option for proxy contracts, since immutable fields can only be written to from the constructor which won't be invoked for proxy instances. +To achieve this, mechs are deployed through a version of a EIP-1167 proxy factory that allows appending arbitrary bytes to the minimal proxy bytecode. +The same mechanism is implemented by the 6551 account registry. ### Migrate a Safe to a ZodiacMech diff --git a/TODO.md b/TODO.md index 80ce2c1..59ead04 100644 --- a/TODO.md +++ b/TODO.md @@ -5,3 +5,8 @@ - check what the Safe indexer requires to pick up ZodiacMechs - more requirements listed here: https://www.notion.so/Simpler-safe-mastercopy-dd8cf22626794b4aade600e1aa16da0e - ZodiacMech should allow updating the ERC4337 entrypoint + +## SDK + +- get rid of zodiac dep +- migrate to viem diff --git a/contracts/ERC1155Mech.sol b/contracts/ERC1155Mech.sol deleted file mode 100644 index a265247..0000000 --- a/contracts/ERC1155Mech.sol +++ /dev/null @@ -1,91 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0 -pragma solidity ^0.8.12; - -import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; -import "./base/Mech.sol"; -import "./base/ImmutableStorage.sol"; - -/** - * @dev A Mech that is operated by the holder of a defined set of minimum ERC1155 token balances - */ -contract ERC1155Mech is Mech, ImmutableStorage { - /// @param _token Address of the token contract - /// @param _tokenIds The token IDs - /// @param _minBalances The minimum balances required for each token ID - constructor( - address _token, - uint256[] memory _tokenIds, - uint256[] memory _minBalances, - uint256 _minTotalBalance - ) { - bytes memory initParams = abi.encode( - _token, - _tokenIds, - _minBalances, - _minTotalBalance - ); - setUp(initParams); - } - - function setUp(bytes memory initParams) public override { - require(readImmutable().length == 0, "Already initialized"); - (, uint256[] memory _tokenIds, uint256[] memory _minBalances, ) = abi - .decode(initParams, (address, uint256[], uint256[], uint256)); - require(_tokenIds.length > 0, "No token IDs provided"); - require(_tokenIds.length == _minBalances.length, "Length mismatch"); - - writeImmutable(initParams); - } - - function token() public view returns (IERC1155) { - address _token = abi.decode(readImmutable(), (address)); - return IERC1155(_token); - } - - function tokenIds(uint256 index) public view returns (uint256) { - (, uint256[] memory _tokenIds) = abi.decode( - readImmutable(), - (address, uint256[]) - ); - return _tokenIds[index]; - } - - function minBalances(uint256 index) public view returns (uint256) { - (, , uint256[] memory _minBalances) = abi.decode( - readImmutable(), - (address, uint256[], uint256[]) - ); - return _minBalances[index]; - } - - function minTotalBalance() public view returns (uint256) { - (, , , uint256 _minTotalBalance) = abi.decode( - readImmutable(), - (address, uint256[], uint256[], uint256) - ); - return _minTotalBalance; - } - - function isOperator(address signer) public view override returns (bool) { - ( - address _token, - uint256[] memory _tokenIds, - uint256[] memory _minBalances, - uint256 _minTotalBalance - ) = abi.decode( - readImmutable(), - (address, uint256[], uint256[], uint256) - ); - - uint256 balanceSum = 0; - for (uint256 i = 0; i < _tokenIds.length; i++) { - uint256 balance = IERC1155(_token).balanceOf(signer, _tokenIds[i]); - if (balance < _minBalances[i]) { - return false; - } - balanceSum += balance; - } - - return balanceSum >= _minTotalBalance; - } -} diff --git a/contracts/ERC1155ThresholdMech.sol b/contracts/ERC1155ThresholdMech.sol new file mode 100644 index 0000000..daff205 --- /dev/null +++ b/contracts/ERC1155ThresholdMech.sol @@ -0,0 +1,48 @@ +//SPDX-License-Identifier: LGPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "./base/ThresholdMech.sol"; +import "./libraries/MinimalProxyStore.sol"; + +/** + * @dev A Mech that is operated by any holder of a defined set of minimum ERC1155 token balances + */ +contract ERC1155ThresholdMech is ThresholdMech { + function threshold() + public + view + returns ( + address token, + uint256[] memory tokenIds, + uint256[] memory minBalances, + uint256 minTotalBalance + ) + { + return + abi.decode( + MinimalProxyStore.getContext(address(this)), + (address, uint256[], uint256[], uint256) + ); + } + + function isOperator(address signer) public view override returns (bool) { + ( + address token, + uint256[] memory tokenIds, + uint256[] memory minBalances, + uint256 minTotalBalance + ) = this.threshold(); + + uint256 balanceSum = 0; + for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 balance = IERC1155(token).balanceOf(signer, tokenIds[i]); + if (balance < minBalances[i]) { + return false; + } + balanceSum += balance; + } + + return balanceSum >= minTotalBalance; + } +} diff --git a/contracts/ERC1155TokenboundMech.sol b/contracts/ERC1155TokenboundMech.sol new file mode 100644 index 0000000..e76cda1 --- /dev/null +++ b/contracts/ERC1155TokenboundMech.sol @@ -0,0 +1,80 @@ +//SPDX-License-Identifier: LGPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "./base/TokenboundMech.sol"; + +/** + * @dev A Mech that is operated by the holder of a designated ERC1155 token + */ +contract ERC1155TokenboundMech is TokenboundMech { + function isOperator(address signer) public view override returns (bool) { + (uint256 chainId, address tokenContract, uint256 tokenId) = this + .token(); + if (chainId != block.chainid) return false; + return IERC1155(tokenContract).balanceOf(signer, tokenId) > 0; + } + + function onERC1155Received( + address, + address from, + uint256 receivedTokenId, + uint256, + bytes calldata + ) external view override returns (bytes4) { + ( + uint256 chainId, + address boundTokenContract, + uint256 boundTokenId + ) = this.token(); + + if ( + chainId == block.chainid && + msg.sender == boundTokenContract && + receivedTokenId == boundTokenId + ) { + // We block the transfer only if the sender has no balance left after the transfer. + // Note: ERC-1155 prescribes that balances are updated BEFORE the call to onERC1155Received. + if ( + IERC1155(boundTokenContract).balanceOf(from, boundTokenId) == 0 + ) { + revert OwnershipCycle(); + } + } + + return 0xf23a6e61; + } + + function onERC1155BatchReceived( + address, + address from, + uint256[] calldata ids, + uint256[] calldata, + bytes calldata + ) external view override returns (bytes4) { + ( + uint256 chainId, + address boundTokenContract, + uint256 boundTokenId + ) = this.token(); + + if (chainId == block.chainid && msg.sender == boundTokenContract) { + // We block the transfer only if the sender has no balance left after the transfer. + // Note: ERC-1155 prescribes that balances are updated BEFORE the call to onERC1155BatchReceived. + for (uint256 i = 0; i < ids.length; i++) { + if (ids[i] == boundTokenId) { + if ( + IERC1155(boundTokenContract).balanceOf( + from, + boundTokenId + ) == 0 + ) { + revert OwnershipCycle(); + } + } + } + } + + return 0xbc197c81; + } +} diff --git a/contracts/ERC20ThresholdMech.sol b/contracts/ERC20ThresholdMech.sol new file mode 100644 index 0000000..4e91150 --- /dev/null +++ b/contracts/ERC20ThresholdMech.sol @@ -0,0 +1,29 @@ +//SPDX-License-Identifier: LGPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./base/ThresholdMech.sol"; +import "./libraries/MinimalProxyStore.sol"; + +/** + * @dev A Mech that is operated by any holder of a minimum ERC20 token balance + */ +contract ERC20ThresholdMech is ThresholdMech { + function threshold() + public + view + returns (address token, uint256 minBalance) + { + return + abi.decode( + MinimalProxyStore.getContext(address(this)), + (address, uint256) + ); + } + + function isOperator(address signer) public view override returns (bool) { + (address token, uint256 minBalance) = this.threshold(); + + return IERC20(token).balanceOf(signer) >= minBalance; + } +} diff --git a/contracts/ERC721Mech.sol b/contracts/ERC721Mech.sol deleted file mode 100644 index 5b76b79..0000000 --- a/contracts/ERC721Mech.sol +++ /dev/null @@ -1,41 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0 -pragma solidity ^0.8.12; - -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import "./base/Mech.sol"; -import "./base/ImmutableStorage.sol"; - -/** - * @dev A Mech that is operated by the holder of an ERC721 non-fungible token - */ -contract ERC721Mech is Mech, ImmutableStorage { - /// @param _token Address of the token contract - /// @param _tokenId The token ID - constructor(address _token, uint256 _tokenId) { - bytes memory initParams = abi.encode(_token, _tokenId); - setUp(initParams); - } - - function setUp(bytes memory initParams) public override { - require(readImmutable().length == 0, "Already initialized"); - writeImmutable(initParams); - } - - function token() public view returns (IERC721) { - address _token = abi.decode(readImmutable(), (address)); - return IERC721(_token); - } - - function tokenId() public view returns (uint256) { - (, uint256 _tokenId) = abi.decode(readImmutable(), (address, uint256)); - return _tokenId; - } - - function isOperator(address signer) public view override returns (bool) { - (address _token, uint256 _tokenId) = abi.decode( - readImmutable(), - (address, uint256) - ); - return IERC721(_token).ownerOf(_tokenId) == signer; - } -} diff --git a/contracts/ERC721TokenboundMech.sol b/contracts/ERC721TokenboundMech.sol new file mode 100644 index 0000000..f9f9f86 --- /dev/null +++ b/contracts/ERC721TokenboundMech.sol @@ -0,0 +1,43 @@ +//SPDX-License-Identifier: LGPL-3.0 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +import "./base/TokenboundMech.sol"; + +/** + * @dev A Mech that is operated by the holder of an ERC721 non-fungible token + */ +contract ERC721TokenboundMech is TokenboundMech { + function isOperator(address signer) public view override returns (bool) { + (uint256 chainId, address tokenContract, uint256 tokenId) = this + .token(); + if (chainId != block.chainid) return false; + return + IERC721(tokenContract).ownerOf(tokenId) == signer && + signer != address(0); + } + + function onERC721Received( + address, + address, + uint256 receivedTokenId, + bytes calldata + ) external view override returns (bytes4) { + ( + uint256 chainId, + address boundTokenContract, + uint256 boundTokenId + ) = this.token(); + + if ( + chainId == block.chainid && + msg.sender == boundTokenContract && + receivedTokenId == boundTokenId + ) { + revert OwnershipCycle(); + } + + return 0x150b7a02; + } +} diff --git a/contracts/MechFactory.sol b/contracts/MechFactory.sol new file mode 100644 index 0000000..ad013db --- /dev/null +++ b/contracts/MechFactory.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0; + +import "./libraries/MinimalProxyStore.sol"; + +contract MechFactory { + event MechCreated( + address indexed proxy, + address indexed mastercopy, + bytes context + ); + + /// `target` can not be zero. + error ZeroAddress(address target); + + /// `target` has no code deployed. + error TargetHasNoCode(address target); + + /// `target` is already taken. + error TakenAddress(address target); + + /// @notice Initialization failed. + error FailedInitialization(); + + function createProxy( + address target, + bytes memory context, + bytes32 salt + ) internal returns (address result) { + if (address(target) == address(0)) revert ZeroAddress(target); + if (address(target).code.length == 0) revert TargetHasNoCode(target); + + address proxy = MinimalProxyStore.cloneDeterministic( + target, + context, + salt + ); + + emit MechCreated(proxy, target, context); + + return proxy; + } + + function deployMech( + address mastercopy, + bytes memory context, + bytes memory initialCall, + bytes32 salt + ) public returns (address proxy) { + proxy = createProxy(mastercopy, context, salt); + + if (initialCall.length > 0) { + (bool success, ) = proxy.call(initialCall); + if (!success) revert FailedInitialization(); + } + } +} diff --git a/contracts/ZodiacMech.sol b/contracts/ZodiacMech.sol index a053bd2..21429aa 100644 --- a/contracts/ZodiacMech.sol +++ b/contracts/ZodiacMech.sol @@ -31,7 +31,7 @@ contract ZodiacMech is SafeStorage, Mech, IAvatar { } /// @dev This function can be called whenever no modules are enabled, meaning anyone could come and call setUp() then. We keep this behavior to not brick the mech in that case. - function setUp(bytes memory initParams) public override { + function setUp(bytes memory initParams) public { require( modules[address(SENTINEL_MODULES)] == address(0), "Already initialized" @@ -46,18 +46,6 @@ contract ZodiacMech is SafeStorage, Mech, IAvatar { } } - function nonce() public view override returns (uint256) { - // Here we use the nonce variable of the SafeStorage contract rather than that of the Mech contract. - // This is for keeping the nonce sequence of Safes that got upgraded to ZodiacMechs. - return safeNonce; - } - - function _validateAndUpdateNonce( - UserOperation calldata userOp - ) internal override { - require(safeNonce++ == userOp.nonce, "Invalid nonce"); - } - function isOperator(address signer) public view override returns (bool) { return isModuleEnabled(signer); } @@ -74,7 +62,7 @@ contract ZodiacMech is SafeStorage, Mech, IAvatar { bytes calldata data, Enum.Operation operation ) public onlyOperator returns (bool success) { - (success, ) = _exec(to, value, data, operation, gasleft()); + (success, ) = _exec(to, value, data, uint8(operation), gasleft()); } /// @dev Passes a transaction to the avatar, expects return data. @@ -89,7 +77,7 @@ contract ZodiacMech is SafeStorage, Mech, IAvatar { bytes calldata data, Enum.Operation operation ) public onlyOperator returns (bool success, bytes memory returnData) { - return _exec(to, value, data, operation, gasleft()); + return _exec(to, value, data, uint8(operation), gasleft()); } /// @dev Disables a module on the modifier. diff --git a/contracts/base/Account.sol b/contracts/base/Account.sol index 2bfd107..31f84a2 100644 --- a/contracts/base/Account.sol +++ b/contracts/base/Account.sol @@ -14,22 +14,22 @@ abstract contract Account is BaseAccount { // return value in case of signature validation success, with no time-range. uint256 private constant SIG_VALIDATION_SUCCEEDED = 0; - uint256 private _nonce = 0; - /** - * @dev Hard-code the ERC4337 entry point contract address for gas efficiency + * @dev Hard-code the ERC4337 entry point contract address so it cannot be changed by anyone */ IEntryPoint private constant _entryPoint = - IEntryPoint(0x0576a174D229E3cFA37253523E645A78A0C91B57); + IEntryPoint(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789); /// @inheritdoc BaseAccount - function nonce() public view virtual override returns (uint256) { - return _nonce; + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; } /// @inheritdoc BaseAccount - function entryPoint() public view virtual override returns (IEntryPoint) { - return _entryPoint; + function _validateNonce(uint256 nonce) internal view virtual override { + // First 192 bits are the sequence key, remaining 64 bits are the sequence number + // We only use the nonce sequence with key 0, so no out-of-order nonce is possible. + require(nonce < type(uint64).max, "Invalid nonce"); } /** @@ -59,15 +59,4 @@ abstract contract Account is BaseAccount { bytes32 hash, bytes memory signature ) public view virtual returns (bytes4 magicValue); - - /** - * @dev Validate the current nonce matches the UserOperation nonce, then increment nonce to prevent replay of this user operation. - * Called only if initCode is empty (since "nonce" field is used as "salt" on account creation) - * @param userOp The user operation to validate. - */ - function _validateAndUpdateNonce( - UserOperation calldata userOp - ) internal virtual override { - require(_nonce++ == userOp.nonce, "Invalid nonce"); - } } diff --git a/contracts/base/ImmutableStorage.sol b/contracts/base/ImmutableStorage.sol deleted file mode 100644 index aee95a2..0000000 --- a/contracts/base/ImmutableStorage.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.12; - -import "hardhat/console.sol"; -import "../libraries/WriteOnce.sol"; - -contract ImmutableStorage { - /** - * @return The address the data is written to - */ - function storageLocation() internal view returns (address) { - // calculates the address of the contract created through the first create() call made by this contract - // see: https://ethereum.stackexchange.com/a/761 - return - address( - uint160( - uint256( - keccak256( - abi.encodePacked( - bytes1(0xd6), - bytes1(0x94), - address(this), - bytes1(0x01) // contracts start with nonce 1 (EIP-161) - ) - ) - ) - ) - ); - } - - /** - * Stores `data` and validates that it's written to the expected storage location - * @param data to be written - */ - function writeImmutable(bytes memory data) internal { - bytes memory initCode = WriteOnce.creationCodeFor(data); - address createdAt; - - // Deploy contract using create - assembly { - createdAt := create(0, add(initCode, 32), mload(initCode)) - } - - require(createdAt == storageLocation(), "Write failed"); - } - - /** - * Reads the code at the storage location as data - * @return data stored at the storage location - */ - function readImmutable() internal view returns (bytes memory) { - return WriteOnce.valueStoredAt(storageLocation()); - } -} diff --git a/contracts/base/Mech.sol b/contracts/base/Mech.sol index e6c2726..9f2391c 100644 --- a/contracts/base/Mech.sol +++ b/contracts/base/Mech.sol @@ -2,7 +2,12 @@ pragma solidity ^0.8.12; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import "@account-abstraction/contracts/interfaces/IAccount.sol"; import "./Receiver.sol"; import "./Account.sol"; @@ -93,14 +98,16 @@ abstract contract Mech is IMech, Account, Receiver { function _exec( address to, uint256 value, - bytes memory data, - Enum.Operation operation, + bytes calldata data, + uint8 operation, uint256 txGas ) internal returns (bool success, bytes memory returnData) { - if (operation == Enum.Operation.DelegateCall) { + if (operation == 0) { + (success, returnData) = to.call{gas: txGas, value: value}(data); + } else if (operation == 1) { (success, returnData) = to.delegatecall{gas: txGas}(data); } else { - (success, returnData) = to.call{gas: txGas, value: value}(data); + revert("Invalid operation"); } } @@ -111,13 +118,13 @@ abstract contract Mech is IMech, Account, Receiver { /// @param operation Operation type of transaction. /// @param txGas Gas to send for executing the meta transaction, if 0 all left will be sent /// @return returnData Return data of the call - function exec( + function execute( address to, uint256 value, - bytes memory data, - Enum.Operation operation, + bytes calldata data, + uint8 operation, uint256 txGas - ) public onlyOperator returns (bytes memory returnData) { + ) public payable onlyOperator returns (bytes memory returnData) { bool success; (success, returnData) = _exec( to, @@ -135,6 +142,29 @@ abstract contract Mech is IMech, Account, Receiver { } } + /// @dev Allows the mech operator to execute arbitrary transactions + /// @param to Destination address of transaction. + /// @param value Ether value of transaction. + /// @param data Data payload of transaction. + /// @param operation Operation type of transaction. + /// @return returnData Return data of the call + function execute( + address to, + uint256 value, + bytes calldata data, + uint8 operation + ) external payable onlyOperator returns (bytes memory returnData) { + bool success; + (success, returnData) = _exec(to, value, data, operation, gasleft()); + + if (!success) { + // solhint-disable-next-line no-inline-assembly + assembly { + revert(add(returnData, 0x20), mload(returnData)) + } + } + } + /** * @dev Divides bytes signature into `uint8 v, bytes32 r, bytes32 s`. * @param signature The signature bytes @@ -152,4 +182,18 @@ abstract contract Mech is IMech, Account, Receiver { v := byte(0, mload(add(signature, 0x60))) } } + + /// @dev Returns true if a given interfaceId is supported by this account. This method can be + /// extended by an override + function supportsInterface( + bytes4 interfaceId + ) public pure virtual returns (bool) { + return + interfaceId == type(IMech).interfaceId || + interfaceId == type(IERC165).interfaceId || + interfaceId == type(IAccount).interfaceId || + interfaceId == type(IERC1271).interfaceId || + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC6551Executable).interfaceId; + } } diff --git a/contracts/base/Receiver.sol b/contracts/base/Receiver.sol index fdd349a..b4c183b 100644 --- a/contracts/base/Receiver.sol +++ b/contracts/base/Receiver.sol @@ -13,7 +13,7 @@ contract Receiver is ERC777TokensRecipient, ERC721TokenReceiver { - receive() external payable {} + receive() external payable virtual {} function onERC1155Received( address, @@ -21,7 +21,7 @@ contract Receiver is uint256, uint256, bytes calldata - ) external pure override returns (bytes4) { + ) external view virtual override returns (bytes4) { return 0xf23a6e61; } @@ -31,7 +31,7 @@ contract Receiver is uint256[] calldata, uint256[] calldata, bytes calldata - ) external pure override returns (bytes4) { + ) external view virtual override returns (bytes4) { return 0xbc197c81; } @@ -40,7 +40,7 @@ contract Receiver is address, uint256, bytes calldata - ) external pure override returns (bytes4) { + ) external view virtual override returns (bytes4) { return 0x150b7a02; } diff --git a/contracts/base/ThresholdMech.sol b/contracts/base/ThresholdMech.sol new file mode 100644 index 0000000..b34d5fb --- /dev/null +++ b/contracts/base/ThresholdMech.sol @@ -0,0 +1,11 @@ +//SPDX-License-Identifier: LGPL-3.0 +pragma solidity ^0.8.12; + +import "./Mech.sol"; + +/** + * @dev A Mech that is operated by anyone hitting the configured threshold + */ +abstract contract ThresholdMech is Mech { + +} diff --git a/contracts/base/TokenboundMech.sol b/contracts/base/TokenboundMech.sol new file mode 100644 index 0000000..d57a1a4 --- /dev/null +++ b/contracts/base/TokenboundMech.sol @@ -0,0 +1,53 @@ +//SPDX-License-Identifier: LGPL-3.0 +pragma solidity ^0.8.12; + +import "@erc6551/reference/src/interfaces/IERC6551Account.sol"; +import "@erc6551/reference/src/lib/ERC6551AccountLib.sol"; + +import "./Mech.sol"; + +/** + * @dev A Mech that is operated by the holder of a designated token, implements the ERC6551 standard and is deployed through the ERC6551 registry + */ +abstract contract TokenboundMech is Mech, IERC6551Account { + error OwnershipCycle(); + + /// @dev Returns the current account nonce + function state() external view returns (uint256) { + return entryPoint().getNonce(address(this), 0); + } + + function token() + external + view + returns (uint256 chainId, address tokenContract, uint256 tokenId) + { + return ERC6551AccountLib.token(); + } + + receive() external payable override(Receiver, IERC6551Account) {} + + /** + * @dev Returns a magic value indicating whether a given signer is authorized to act on behalf + * of the account + * @param signer The address to check signing authorization for + * @return magicValue Magic value indicating whether the signer is valid + */ + function isValidSigner( + address signer, + bytes calldata + ) external view returns (bytes4 magicValue) { + return + isOperator(signer) + ? IERC6551Account.isValidSigner.selector + : bytes4(0); + } + + function supportsInterface( + bytes4 interfaceId + ) public pure override returns (bool) { + return + super.supportsInterface(interfaceId) || + interfaceId == type(IERC6551Account).interfaceId; + } +} diff --git a/contracts/interfaces/IFactoryFriendly.sol b/contracts/interfaces/IFactoryFriendly.sol deleted file mode 100644 index ef6ced1..0000000 --- a/contracts/interfaces/IFactoryFriendly.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.12; - -/// @dev Interface for contracts that can be used as master copies for minimal proxies deployed through Zodiac's ModuleProxyFactory -interface IFactoryFriendly { - function setUp(bytes memory initializeParams) external; -} diff --git a/contracts/interfaces/IMech.sol b/contracts/interfaces/IMech.sol index 4570f6d..38ab3dc 100644 --- a/contracts/interfaces/IMech.sol +++ b/contracts/interfaces/IMech.sol @@ -2,24 +2,41 @@ pragma solidity ^0.8.12; import "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol"; +import "@erc6551/reference/src/interfaces/IERC6551Executable.sol"; import "@account-abstraction/contracts/interfaces/IAccount.sol"; -import "./IFactoryFriendly.sol"; +interface IMech is IAccount, IERC1271, IERC6551Executable { + /** + * @dev Return if the passed address is authorized to sign on behalf of the mech, must be implemented by the child contract + * @param signer The address to check + */ + function isOperator(address signer) external view returns (bool); -interface IMech is IAccount, IERC1271, IFactoryFriendly { /// @dev Executes either a delegatecall or a call with provided parameters /// @param to Destination address. /// @param value Ether value. /// @param data Data payload. /// @param operation Operation type. + /// @return returnData bytes The return data of the call + function execute( + address to, + uint256 value, + bytes calldata data, + uint8 operation + ) external payable returns (bytes memory returnData); + + /// @dev Executes either a delegatecall or a call with provided parameters, with a specified gas limit for the meta transaction + /// @param to Destination address. + /// @param value Ether value. + /// @param data Data payload. + /// @param operation Operation type. /// @param txGas Gas to send for executing the meta transaction, if 0 all left will be sent /// @return returnData bytes The return data of the call - function exec( + function execute( address to, uint256 value, - bytes memory data, - Enum.Operation operation, + bytes calldata data, + uint8 operation, uint256 txGas - ) external returns (bytes memory returnData); + ) external payable returns (bytes memory returnData); } diff --git a/contracts/libraries/MinimalProxyStore.sol b/contracts/libraries/MinimalProxyStore.sol new file mode 100644 index 0000000..7a59bcc --- /dev/null +++ b/contracts/libraries/MinimalProxyStore.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/utils/Create2.sol"; + +/** + * @title A library for deploying EIP-1167 minimal proxy contracts with embedded constant data + * @author Jayden Windle (jaydenwindle) + */ +library MinimalProxyStore { + error CreateError(); + + /** + * @dev Returns bytecode for a minmal proxy with additional context data appended to it + * + * @param implementation the implementation this proxy will delegate to + * @param context the data to be appended to the proxy + * @return the generated bytecode + */ + function getBytecode( + address implementation, + bytes memory context + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + hex"3d61", // RETURNDATASIZE, PUSH2 + uint16(0x2d + context.length + 1), // size of minimal proxy (45 bytes) + size of context + stop byte + hex"8060", // DUP1, PUSH1 + uint8(0x0a + 1), // default offset (0x0a) + 1 byte because we increased size from uint8 to uint16 + hex"3d3981f3363d3d373d3d3d363d73", // standard EIP1167 implementation + implementation, // implementation address + hex"5af43d82803e903d91602b57fd5bf3", // standard EIP1167 implementation + hex"00", // stop byte (prevents context from executing as code) + context // appended context data + ); + } + + /** + * @dev Fetches the context data stored in a deployed proxy + * + * @param instance the proxy to query context data for + * @return context the queried context data + */ + function getContext( + address instance + ) internal view returns (bytes memory context) { + uint256 instanceCodeLength = instance.code.length; + uint256 start = 46; + uint256 size = instanceCodeLength - start; + + assembly { + // allocate output byte array - this could also be done without assembly + // by using o_code = new bytes(size) + context := mload(0x40) + // new "memory end" including padding + mstore( + 0x40, + add(context, and(add(add(size, 0x20), 0x1f), not(0x1f))) + ) + // store length in memory + mstore(context, size) + // actually retrieve the code, this needs assembly + extcodecopy(instance, add(context, 0x20), start, size) + } + } + + /** + * @dev Deploys and returns the address of a clone with stored context data that mimics the behaviour of `implementation`. + * + * This function uses the create opcode, which should never revert. + * + * @param implementation the implementation to delegate to + * @param context context data to be stored in the proxy + * @return instance the address of the deployed proxy + */ + function clone( + address implementation, + bytes memory context + ) internal returns (address instance) { + // Generate bytecode for proxy + bytes memory code = getBytecode(implementation, context); + + // Deploy contract using create + assembly { + instance := create(0, add(code, 32), mload(code)) + } + + // If address is zero, deployment failed + if (instance == address(0)) revert CreateError(); + } + + /** + * @dev Deploys and returns the address of a clone with stored context data that mimics the behaviour of `implementation`. + * + * This function uses the create2 opcode and a `salt` to deterministically deploy + * the clone. Using the same `implementation` and `salt` multiple time will revert, since + * the clones cannot be deployed twice at the same address. + * + * @param implementation the implementation to delegate to + * @param context context data to be stored in the proxy + * @return instance the address of the deployed proxy + */ + function cloneDeterministic( + address implementation, + bytes memory context, + bytes32 salt + ) internal returns (address instance) { + bytes memory code = getBytecode(implementation, context); + + // Deploy contract using create2 + assembly { + instance := create2(0, add(code, 32), mload(code), salt) + } + + // If address is zero, deployment failed + if (instance == address(0)) revert CreateError(); + } + + /** + * @dev Computes the address of a clone deployed using {MinimalProxyStore-cloneDeterministic}. + */ + function predictDeterministicAddress( + address implementation, + bytes memory context, + bytes32 salt, + address deployer + ) internal pure returns (address predicted) { + bytes memory code = getBytecode(implementation, context); + + return Create2.computeAddress(salt, keccak256(code), deployer); + } + + /** + * @dev Computes the address of a clone deployed using {MinimalProxyStore-cloneDeterministic}. + */ + function predictDeterministicAddress( + address implementation, + bytes memory context, + bytes32 salt + ) internal view returns (address predicted) { + return + predictDeterministicAddress( + implementation, + context, + salt, + address(this) + ); + } +} diff --git a/contracts/libraries/WriteOnce.sol b/contracts/libraries/WriteOnce.sol deleted file mode 100644 index 88b37b9..0000000 --- a/contracts/libraries/WriteOnce.sol +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: MIT - -// Forked from https://github.com/0xsequence/sstore2/blob/master/contracts/utils/Bytecode.sol -// MIT License -// Copyright (c) [2018] [Ismael Ramos Silvan] -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -pragma solidity ^0.8.12; - -library WriteOnce { - /** - * @notice Generate a creation code that results on a contract with `00${_value}` as bytecode - * @param data The value to store in the bytecode - * @return creationCode (constructor) for new contract - */ - function creationCodeFor( - bytes memory data - ) internal pure returns (bytes memory) { - /* - 0x00 0x63 0x63XXXXXX PUSH4 _value.length size - 0x01 0x80 0x80 DUP1 size size - 0x02 0x60 0x600e PUSH1 14 14 size size - 0x03 0x60 0x6000 PUSH1 00 0 14 size size - 0x04 0x39 0x39 CODECOPY size - 0x05 0x60 0x6000 PUSH1 00 0 size - 0x06 0xf3 0xf3 RETURN - - */ - - // Prepend 00 so the created contract can't be called - return - abi.encodePacked( - hex"63", - uint32(data.length + 1), - hex"80_60_0E_60_00_39_60_00_F3", - hex"00", - data - ); - } - - /** - * @notice Returns the size of the code on a given address - * @param _addr Address that may or may not contain code - * @return size of the code on the given `_addr` - */ - function codeSize(address _addr) internal view returns (uint256 size) { - assembly { - size := extcodesize(_addr) - } - } - - /** - * @notice Returns the value stored in the bytecode of the given address - * @param _addr Address that may or may not contain code - * @return _value The value stored in the bytecode of the given address - * - * Forked from: https://gist.github.com/KardanovIR/fe98661df9338c842b4a30306d507fbd - **/ - function valueStoredAt( - address _addr - ) internal view returns (bytes memory _value) { - uint256 size = codeSize(_addr); - if (size <= 1) return bytes(""); - size--; // remove 00 byte we prepend when writing - - unchecked { - assembly { - // allocate output byte array - this could also be done without assembly - // by using o_code = new bytes(size) - _value := mload(0x40) - // new "memory end" including padding - mstore( - 0x40, - add(_value, and(add(add(size, 0x20), 0x1f), not(0x1f))) - ) - // store length in memory - mstore(_value, size) - // actually retrieve the code, this needs assembly - // start at offset 1 to skip over 00 byte we prepend when writing - extcodecopy(_addr, add(_value, 0x20), 1, size) - } - } - } -} diff --git a/contracts/test/ERC1155Token.sol b/contracts/test/ERC1155Token.sol index d286716..0e1bb7a 100644 --- a/contracts/test/ERC1155Token.sol +++ b/contracts/test/ERC1155Token.sol @@ -8,7 +8,7 @@ contract ERC1155Token is ERC1155("ERC1155Token") { address recipient, uint256 id, uint256 amount, - bytes memory data + bytes calldata data ) public { _mint(recipient, id, amount, data); } diff --git a/contracts/test/ERC6551Registry.sol b/contracts/test/ERC6551Registry.sol new file mode 100644 index 0000000..d62021f --- /dev/null +++ b/contracts/test/ERC6551Registry.sol @@ -0,0 +1,4 @@ +//SPDX-License-Identifier: LGPL-3.0 +pragma solidity ^0.8.12; + +import "@erc6551/reference/src/ERC6551Registry.sol"; diff --git a/contracts/test/ImmutableStorageTest.sol b/contracts/test/ImmutableStorageTest.sol deleted file mode 100644 index 02fd38f..0000000 --- a/contracts/test/ImmutableStorageTest.sol +++ /dev/null @@ -1,14 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0 -pragma solidity ^0.8.12; - -import "../base/ImmutableStorage.sol"; - -contract ImmutableStorageTest is ImmutableStorage { - function read() public view returns (bytes memory data) { - return readImmutable(); - } - - function write(bytes memory data) public { - writeImmutable(data); - } -} diff --git a/deploy/00_deploy_ERC6551Registry.ts b/deploy/00_deploy_ERC6551Registry.ts new file mode 100644 index 0000000..185fcd3 --- /dev/null +++ b/deploy/00_deploy_ERC6551Registry.ts @@ -0,0 +1,77 @@ +import { DeployFunction } from "hardhat-deploy/types" +import { + createWalletClient, + custom as customTransport, + getCreate2Address, + publicActions, +} from "viem" +import * as chains from "viem/chains" + +import { deployMastercopy } from "../sdk" +import { + DEFAULT_SALT, + ERC2470_SINGLETON_FACTORY_ADDRESS, +} from "../sdk/src/constants" + +const deployERC6551Registry: DeployFunction = async (hre) => { + const [signer] = await hre.ethers.getSigners() + const deployer = await hre.ethers.provider.getSigner(signer.address) + const network = await hre.ethers.provider.getNetwork() + const chain = Object.values(chains).find( + (chain) => chain.id === Number(network.chainId) + ) + console.log(`Using chain ${chain?.name} (${chain?.id})`) + + const deployerClient = createWalletClient({ + account: deployer.address as `0x${string}`, + transport: customTransport({ + async request({ method, params }) { + return deployer.provider.send(method, params) + }, + }), + chain, + }) + const ERC6551Registry = await hre.ethers.getContractFactory("ERC6551Registry") + const bytecode = ERC6551Registry.bytecode as `0x${string}` + + // TODO: use Nick's factory 0x4e59b44847b379578588920ca78fbf26c0b4956c and 6551 salt rather than ERC2470 + const expectedAddress = getCreate2Address({ + bytecode, + from: ERC2470_SINGLETON_FACTORY_ADDRESS, + salt: DEFAULT_SALT, + }) + + if ( + await deployerClient + .extend(publicActions) + .getBytecode({ address: expectedAddress }) + ) { + console.log(` ✔ Contract is already deployed at ${expectedAddress}`) + } else { + await deployMastercopy(deployerClient, bytecode) + console.log(` ✔ Contract deployed at ${expectedAddress}`) + } + + try { + await hre.run("verify:verify", { + address: expectedAddress, + constructorArguments: [], + }) + } catch (e) { + if ( + e instanceof Error && + e.stack && + (e.stack.indexOf("Reason: Already Verified") > -1 || + e.stack.indexOf("Contract source code already verified") > -1) + ) { + console.log(" ✔ Contract is already verified") + } else { + console.log( + " ✘ Verifying the contract failed. This is probably because Etherscan is still indexing the contract. Try running this same command again in a few seconds." + ) + throw e + } + } +} + +export default deployERC6551Registry diff --git a/deploy/00_deploy_mastercopy_ERC721.ts b/deploy/00_deploy_mastercopy_ERC721.ts deleted file mode 100644 index 047a138..0000000 --- a/deploy/00_deploy_mastercopy_ERC721.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { DeployFunction } from "hardhat-deploy/types" - -import { - calculateERC721MechMastercopyAddress, - deployERC721MechMastercopy, - ERC721_MASTERCOPY_INIT_DATA, -} from "../sdk" - -const deployMastercopyERC721: DeployFunction = async (hre) => { - const [signer] = await hre.ethers.getSigners() - const deployer = hre.ethers.provider.getSigner(signer.address) - - await deployERC721MechMastercopy(deployer) - const address = calculateERC721MechMastercopyAddress() - - try { - await hre.run("verify:verify", { - address, - constructorArguments: ERC721_MASTERCOPY_INIT_DATA, - }) - } catch (e) { - if ( - e instanceof Error && - e.stack && - (e.stack.indexOf("Reason: Already Verified") > -1 || - e.stack.indexOf("Contract source code already verified") > -1) - ) { - console.log(" ✔ Mastercopy is already verified") - } else { - console.log( - " ✘ Verifying the mastercopy failed. This is probably because Etherscan is still indexing the contract. Try running this same command again in a few seconds." - ) - throw e - } - } -} - -deployMastercopyERC721.tags = ["ERC721Mech"] - -export default deployMastercopyERC721 diff --git a/deploy/01_deploy_mastercopy_ERC1155.ts b/deploy/01_deploy_mastercopy_ERC1155.ts deleted file mode 100644 index 6c7dfdf..0000000 --- a/deploy/01_deploy_mastercopy_ERC1155.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { DeployFunction } from "hardhat-deploy/types" - -import { - calculateERC1155MechMastercopyAddress, - deployERC1155MechMastercopy, - ERC1155_MASTERCOPY_INIT_DATA, -} from "../sdk" - -const deployMastercopyERC1155: DeployFunction = async (hre) => { - const [signer] = await hre.ethers.getSigners() - const deployer = hre.ethers.provider.getSigner(signer.address) - - await deployERC1155MechMastercopy(deployer) - const address = calculateERC1155MechMastercopyAddress() - - try { - await hre.run("verify:verify", { - address, - constructorArguments: ERC1155_MASTERCOPY_INIT_DATA, - }) - } catch (e) { - if ( - e instanceof Error && - e.stack && - (e.stack.indexOf("Reason: Already Verified") > -1 || - e.stack.indexOf("Contract source code already verified") > -1) - ) { - console.log(" ✔ Mastercopy is already verified") - } else { - console.log( - " ✘ Verifying the mastercopy failed. This is probably because Etherscan is still indexing the contract. Try running this same command again in a few seconds." - ) - throw e - } - } -} - -deployMastercopyERC1155.tags = ["ERC1155Mech"] - -export default deployMastercopyERC1155 diff --git a/deploy/01_deploy_mastercopy_ERC721Tokenbound.ts b/deploy/01_deploy_mastercopy_ERC721Tokenbound.ts new file mode 100644 index 0000000..1be0e8c --- /dev/null +++ b/deploy/01_deploy_mastercopy_ERC721Tokenbound.ts @@ -0,0 +1,66 @@ +import { DeployFunction } from "hardhat-deploy/types" +import { + createWalletClient, + custom as customTransport, + publicActions, +} from "viem" +import * as chains from "viem/chains" + +import { + calculateERC721TokenboundMechMastercopyAddress, + deployERC721TokenboundMechMastercopy, +} from "../sdk" + +const deployMastercopyERC721Tokenbound: DeployFunction = async (hre) => { + const [signer] = await hre.ethers.getSigners() + const deployer = await hre.ethers.provider.getSigner(signer.address) + const network = await hre.ethers.provider.getNetwork() + const chain = Object.values(chains).find( + (chain) => chain.id === Number(network.chainId) + ) + console.log(`Using chain ${chain?.name} (${chain?.id})`) + + const deployerClient = createWalletClient({ + account: deployer.address as `0x${string}`, + transport: customTransport({ + async request({ method, params }) { + return deployer.provider.send(method, params) + }, + }), + chain, + }) + + const address = calculateERC721TokenboundMechMastercopyAddress() + + if (await deployerClient.extend(publicActions).getBytecode({ address })) { + console.log(` ✔ Contract is already deployed at ${address}`) + } else { + await deployERC721TokenboundMechMastercopy(deployerClient) + console.log(` ✔ Contract deployed at ${address}`) + } + + try { + await hre.run("verify:verify", { + address, + constructorArguments: [], + }) + } catch (e) { + if ( + e instanceof Error && + e.stack && + (e.stack.indexOf("Reason: Already Verified") > -1 || + e.stack.indexOf("Contract source code already verified") > -1) + ) { + console.log(" ✔ Mastercopy is already verified") + } else { + console.log( + " ✘ Verifying the mastercopy failed. This is probably because Etherscan is still indexing the contract. Try running this same command again in a few seconds." + ) + throw e + } + } +} + +deployMastercopyERC721Tokenbound.tags = ["ERC721TokenboundMech"] + +export default deployMastercopyERC721Tokenbound diff --git a/deploy/02_deploy_mastercopy_ERC1155Tokenbound.ts b/deploy/02_deploy_mastercopy_ERC1155Tokenbound.ts new file mode 100644 index 0000000..dc31aaf --- /dev/null +++ b/deploy/02_deploy_mastercopy_ERC1155Tokenbound.ts @@ -0,0 +1,66 @@ +import { DeployFunction } from "hardhat-deploy/types" +import { + createWalletClient, + custom as customTransport, + publicActions, +} from "viem" +import * as chains from "viem/chains" + +import { + calculateERC1155TokenboundMechMastercopyAddress, + deployERC1155TokenboundMechMastercopy, +} from "../sdk/build/cjs/sdk/src" + +const deployMastercopyERC1155Tokenbound: DeployFunction = async (hre) => { + const [signer] = await hre.ethers.getSigners() + const deployer = await hre.ethers.provider.getSigner(signer.address) + const network = await hre.ethers.provider.getNetwork() + const chain = Object.values(chains).find( + (chain) => chain.id === Number(network.chainId) + ) + console.log(`Using chain ${chain?.name} (${chain?.id})`) + + const deployerClient = createWalletClient({ + account: deployer.address as `0x${string}`, + transport: customTransport({ + async request({ method, params }) { + return deployer.provider.send(method, params) + }, + }), + chain, + }) + + const address = calculateERC1155TokenboundMechMastercopyAddress() + + if (await deployerClient.extend(publicActions).getBytecode({ address })) { + console.log(` ✔ Contract is already deployed at ${address}`) + } else { + await await deployERC1155TokenboundMechMastercopy(deployerClient) + console.log(` ✔ Contract deployed at ${address}`) + } + + try { + await hre.run("verify:verify", { + address, + constructorArguments: [], + }) + } catch (e) { + if ( + e instanceof Error && + e.stack && + (e.stack.indexOf("Reason: Already Verified") > -1 || + e.stack.indexOf("Contract source code already verified") > -1) + ) { + console.log(" ✔ Mastercopy is already verified") + } else { + console.log( + " ✘ Verifying the mastercopy failed. This is probably because Etherscan is still indexing the contract. Try running this same command again in a few seconds." + ) + throw e + } + } +} + +deployMastercopyERC1155Tokenbound.tags = ["ERC1155TokenboundMech"] + +export default deployMastercopyERC1155Tokenbound diff --git a/deploy/02_deploy_mastercopy_Zodiac.ts b/deploy/03_deploy_mastercopy_Zodiac.ts similarity index 56% rename from deploy/02_deploy_mastercopy_Zodiac.ts rename to deploy/03_deploy_mastercopy_Zodiac.ts index c98cba0..6591dac 100644 --- a/deploy/02_deploy_mastercopy_Zodiac.ts +++ b/deploy/03_deploy_mastercopy_Zodiac.ts @@ -1,25 +1,41 @@ import { DeployFunction } from "hardhat-deploy/types" +import { createWalletClient, custom as customTransport } from "viem" +import * as chains from "viem/chains" import { calculateZodiacMechMastercopyAddress, deployZodiacMechMastercopy, - ZODIAC_MASTERCOPY_INIT_DATA, -} from "../sdk" +} from "../sdk/build/cjs/sdk/src" const deployMastercopyZodiac: DeployFunction = async (hre) => { // TODO disabled for now return const [signer] = await hre.ethers.getSigners() - const deployer = hre.ethers.provider.getSigner(signer.address) + const deployer = await hre.ethers.provider.getSigner(signer.address) + const network = await hre.ethers.provider.getNetwork() + const chain = Object.values(chains).find( + (chain) => chain.id === Number(network.chainId) + ) + console.log(`Using chain ${chain?.name} (${chain?.id})`) - await deployZodiacMechMastercopy(deployer) + const deployerClient = createWalletClient({ + account: deployer.address as `0x${string}`, + transport: customTransport({ + async request({ method, params }) { + return deployer.provider.send(method, params) + }, + }), + chain, + }) + + await deployZodiacMechMastercopy(deployerClient) const address = calculateZodiacMechMastercopyAddress() try { await hre.run("verify:verify", { address, - constructorArguments: ZODIAC_MASTERCOPY_INIT_DATA, + constructorArguments: [], }) } catch (e) { if ( diff --git a/docs/mech-hierarchy.excalidraw b/docs/mech-hierarchy.excalidraw new file mode 100644 index 0000000..1336476 --- /dev/null +++ b/docs/mech-hierarchy.excalidraw @@ -0,0 +1,1269 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "type": "rectangle", + "version": 70, + "versionNonce": 1498118490, + "isDeleted": false, + "id": "Qd9BZ5uJod3PiAHYKKnvK", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1158.5, + "y": 289.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 167, + "height": 83, + "seed": 1414607686, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "N1C--aI5OeMY2meQvHngJ" + }, + { + "id": "Ac4x7mSQSeiBzk-erYTMF", + "type": "arrow" + } + ], + "updated": 1687249969036, + "link": null, + "locked": false + }, + { + "id": "N1C--aI5OeMY2meQvHngJ", + "type": "text", + "x": 1173.4599914550781, + "y": 318.5, + "width": 137.08001708984375, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 2064977222, + "version": 45, + "versionNonce": 2006428870, + "isDeleted": false, + "boundElements": null, + "updated": 1687249880558, + "link": null, + "locked": false, + "text": "4337 Account", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 18, + "containerId": "Qd9BZ5uJod3PiAHYKKnvK", + "originalText": "4337 Account", + "lineHeight": 1.25, + "isFrameName": false + }, + { + "type": "rectangle", + "version": 229, + "versionNonce": 1895193286, + "isDeleted": false, + "id": "Kt9ecuU0T6YziwMxORpi2", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 726.25, + "y": 289.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 167, + "height": 83, + "seed": 397358470, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "JvF1O6TcKXZfoMCAecrFJ" + }, + { + "id": "G1IDUtHnT88HeC00m-AXa", + "type": "arrow" + }, + { + "id": "3xF5i-0zFL2jLfHOzUX1P", + "type": "arrow" + }, + { + "id": "eposn4gkhhG1veHDC9XFh", + "type": "arrow" + }, + { + "id": "Ac4x7mSQSeiBzk-erYTMF", + "type": "arrow" + } + ], + "updated": 1687249965251, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 208, + "versionNonce": 580383578, + "isDeleted": false, + "id": "JvF1O6TcKXZfoMCAecrFJ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 786.6599998474121, + "y": 318.5, + "strokeColor": "#343a40", + "backgroundColor": "transparent", + "width": 46.18000030517578, + "height": 25, + "seed": 1484428486, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249952985, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Mech", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Kt9ecuU0T6YziwMxORpi2", + "originalText": "Mech", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 357, + "versionNonce": 1887120858, + "isDeleted": false, + "id": "EP0vhCSYGfHck97YH-xoL", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 511.5, + "y": 488, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 167, + "height": 83, + "seed": 1966364762, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "kGJ6q15_dBJ7QIs5U15SN" + }, + { + "id": "eposn4gkhhG1veHDC9XFh", + "type": "arrow" + }, + { + "id": "qDjCrIWFuavGMz4gqjl3v", + "type": "arrow" + }, + { + "id": "wEyfkGDmhk1mtJ_8R8RzR", + "type": "arrow" + } + ], + "updated": 1687249977937, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 352, + "versionNonce": 1446753094, + "isDeleted": false, + "id": "kGJ6q15_dBJ7QIs5U15SN", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 548.0200004577637, + "y": 517, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 93.95999908447266, + "height": 25, + "seed": 1350319386, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249880559, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Threshold", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "EP0vhCSYGfHck97YH-xoL", + "originalText": "Threshold", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 346, + "versionNonce": 1249555654, + "isDeleted": false, + "id": "ySuuzFZh2ir5U4fU09Ov6", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 941.5, + "y": 488, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 167, + "height": 83, + "seed": 1584732954, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "jS0Yx4OABjmV3IpNHK2uD" + }, + { + "id": "3xF5i-0zFL2jLfHOzUX1P", + "type": "arrow" + }, + { + "id": "x8h2ozCdnWr9fPeldYKMP", + "type": "arrow" + }, + { + "id": "ujPJ-Hr9B2-vmWhgspcOl", + "type": "arrow" + } + ], + "updated": 1687249977937, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 353, + "versionNonce": 338778758, + "isDeleted": false, + "id": "jS0Yx4OABjmV3IpNHK2uD", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 969.7500038146973, + "y": 517, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 110.49999237060547, + "height": 25, + "seed": 1108781018, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249880559, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Tokenbound", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ySuuzFZh2ir5U4fU09Ov6", + "originalText": "Tokenbound", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 187, + "versionNonce": 927752858, + "isDeleted": false, + "id": "JzQHCBVbil3RJ6YGCYDLa", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1158.5, + "y": 488, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 167, + "height": 83, + "seed": 1668926362, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "kaaqWYn0_SHG_UKvpPic_" + } + ], + "updated": 1687249977937, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 165, + "versionNonce": 728766918, + "isDeleted": false, + "id": "kaaqWYn0_SHG_UKvpPic_", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1177.389991760254, + "y": 517, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 129.2200164794922, + "height": 25, + "seed": 338774106, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249880559, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "6551 Account", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "JzQHCBVbil3RJ6YGCYDLa", + "originalText": "6551 Account", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 511, + "versionNonce": 378260890, + "isDeleted": false, + "id": "fkYBlyRqtorTL1ENJpcZQ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 726.25, + "y": 488, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 167, + "height": 83, + "seed": 1593457434, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "kay7uRInPleLcf4mqUAvC" + }, + { + "id": "qDjCrIWFuavGMz4gqjl3v", + "type": "arrow" + } + ], + "updated": 1687249880559, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 520, + "versionNonce": 1125445894, + "isDeleted": false, + "id": "kay7uRInPleLcf4mqUAvC", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 776.6699981689453, + "y": 517, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 66.16000366210938, + "height": 25, + "seed": 888653786, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249880559, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Zodiac", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "fkYBlyRqtorTL1ENJpcZQ", + "originalText": "Zodiac", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "id": "G1IDUtHnT88HeC00m-AXa", + "type": "arrow", + "x": 809.6633959850747, + "y": 489, + "width": 0, + "height": 115.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1363388250, + "version": 512, + "versionNonce": 1650615514, + "isDeleted": false, + "boundElements": null, + "updated": 1687249957032, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -115.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": { + "elementId": "Kt9ecuU0T6YziwMxORpi2", + "focus": 0.001037173831441224, + "gap": 1 + }, + "startArrowhead": null, + "endArrowhead": "triangle" + }, + { + "type": "arrow", + "version": 1213, + "versionNonce": 815861318, + "isDeleted": false, + "id": "3xF5i-0zFL2jLfHOzUX1P", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1025.9133959850747, + "y": 487, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 214.91339598507466, + "height": 111, + "seed": 762674970, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249975182, + "link": "", + "locked": false, + "startBinding": { + "elementId": "ySuuzFZh2ir5U4fU09Ov6", + "focus": 0.010938874072750394, + "gap": 1 + }, + "endBinding": { + "elementId": "Kt9ecuU0T6YziwMxORpi2", + "focus": -0.014970059880239521, + "gap": 3.5 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "triangle", + "points": [ + [ + 0, + 0 + ], + [ + 0, + -47 + ], + [ + -214.91339598507466, + -50 + ], + [ + -214.91339598507466, + -111 + ] + ] + }, + { + "type": "arrow", + "version": 1281, + "versionNonce": 168689562, + "isDeleted": false, + "id": "eposn4gkhhG1veHDC9XFh", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 594.4575924337465, + "y": 486.98224264141163, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 214.91339598507466, + "height": 111.5477285690605, + "seed": 1804348186, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249975181, + "link": "", + "locked": false, + "startBinding": { + "elementId": "EP0vhCSYGfHck97YH-xoL", + "focus": -0.006495898997047583, + "gap": 1.0177573585883692 + }, + "endBinding": { + "elementId": "Kt9ecuU0T6YziwMxORpi2", + "focus": 0.004539060852440905, + "gap": 2.934514072351135 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "triangle", + "points": [ + [ + 0, + 0 + ], + [ + 0, + -47.547728569060496 + ], + [ + 214.91339598507466, + -50.547728569060496 + ], + [ + 214.91339598507466, + -111.5477285690605 + ] + ] + }, + { + "type": "arrow", + "version": 645, + "versionNonce": 1281259098, + "isDeleted": false, + "id": "Ac4x7mSQSeiBzk-erYTMF", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 895, + "y": 331.2405029751235, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 262, + "height": 0, + "seed": 2100566234, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1687249957033, + "link": null, + "locked": false, + "startBinding": { + "elementId": "Kt9ecuU0T6YziwMxORpi2", + "focus": 0.005795252412614232, + "gap": 1.75 + }, + "endBinding": { + "elementId": "Qd9BZ5uJod3PiAHYKKnvK", + "focus": -0.005795252412614232, + "gap": 1.5 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "triangle", + "points": [ + [ + 0, + 0 + ], + [ + 262, + 0 + ] + ] + }, + { + "type": "arrow", + "version": 799, + "versionNonce": 150950426, + "isDeleted": false, + "id": "Jzb_M87c0iZIujMrhS8DM", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1107.9999997615814, + "y": 529.3070919532981, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 52, + "height": 0, + "seed": 1265998214, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1687249880559, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "triangle", + "points": [ + [ + 0, + 0 + ], + [ + 52, + 0 + ] + ] + }, + { + "type": "rectangle", + "version": 705, + "versionNonce": 115995782, + "isDeleted": false, + "id": "EXgclQrPXRWS3yBwcMY_P", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 411.55555531713696, + "y": 656.0833339691162, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 167, + "height": 83, + "seed": 861009414, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "2ZILoPx5A87qAY0sIXFL1" + }, + { + "id": "qDjCrIWFuavGMz4gqjl3v", + "type": "arrow" + } + ], + "updated": 1687249880559, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 710, + "versionNonce": 742522586, + "isDeleted": false, + "id": "2ZILoPx5A87qAY0sIXFL1", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 461.09555241796704, + "y": 685.0833339691162, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 67.92000579833984, + "height": 25, + "seed": 1659979078, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249880559, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "ERC20", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "EXgclQrPXRWS3yBwcMY_P", + "originalText": "ERC20", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 779, + "versionNonce": 805663686, + "isDeleted": false, + "id": "tmkH2xuhmAF0sXW0jY4qH", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 610.0370367986184, + "y": 656.0833339691162, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 167, + "height": 83, + "seed": 910749914, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "p6s3JKky5qS-knm7KSY0D" + }, + { + "id": "wEyfkGDmhk1mtJ_8R8RzR", + "type": "arrow" + } + ], + "updated": 1687249880559, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 788, + "versionNonce": 1410405274, + "isDeleted": false, + "id": "p6s3JKky5qS-knm7KSY0D", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 655.7970351201516, + "y": 685.0833339691162, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 75.4800033569336, + "height": 25, + "seed": 610911642, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249880559, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "ERC1155", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "tmkH2xuhmAF0sXW0jY4qH", + "originalText": "ERC1155", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 955, + "versionNonce": 1300664582, + "isDeleted": false, + "id": "2c1lVUWxftqhZNv88TyC5", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 846.5185182800999, + "y": 656.0833339691162, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 167, + "height": 83, + "seed": 222976282, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "jqbDIWQm_D5YnqOm9Z6ZR" + }, + { + "id": "x8h2ozCdnWr9fPeldYKMP", + "type": "arrow" + } + ], + "updated": 1687249895419, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 965, + "versionNonce": 1556170138, + "isDeleted": false, + "id": "jqbDIWQm_D5YnqOm9Z6ZR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 894.8485201111546, + "y": 685.0833339691162, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 70.33999633789062, + "height": 25, + "seed": 529802714, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249989739, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "ERC721", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "2c1lVUWxftqhZNv88TyC5", + "originalText": "ERC721", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 893, + "versionNonce": 2087618438, + "isDeleted": false, + "id": "cjqOxNQpQ_zpGwBphnsTk", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1044.9999997615814, + "y": 656.0833339691162, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 167, + "height": 83, + "seed": 143022746, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "T9S8DdITVkWI8ie7JIS9V" + }, + { + "id": "ujPJ-Hr9B2-vmWhgspcOl", + "type": "arrow" + } + ], + "updated": 1687249895419, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 902, + "versionNonce": 2076210458, + "isDeleted": false, + "id": "T9S8DdITVkWI8ie7JIS9V", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1090.7599980831146, + "y": 685.0833339691162, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 75.4800033569336, + "height": 25, + "seed": 651425626, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249880559, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "ERC1155", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "cjqOxNQpQ_zpGwBphnsTk", + "originalText": "ERC1155", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 1461, + "versionNonce": 1703337734, + "isDeleted": false, + "id": "qDjCrIWFuavGMz4gqjl3v", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 494.17816011326846, + "y": 653.3865026444197, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 100.90899630558232, + "height": 81.38650264441969, + "seed": 1570652614, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249975181, + "link": "", + "locked": false, + "startBinding": { + "elementId": "EXgclQrPXRWS3yBwcMY_P", + "focus": -0.010507726992437157, + "gap": 2.696831324696518 + }, + "endBinding": { + "elementId": "EP0vhCSYGfHck97YH-xoL", + "focus": -0.014733526463525423, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "triangle", + "points": [ + [ + 0, + 0 + ], + [ + 0, + -41.547728569060496 + ], + [ + 99.82183964831296, + -41.547728569060496 + ], + [ + 100.90899630558232, + -81.38650264441969 + ] + ] + }, + { + "type": "arrow", + "version": 1564, + "versionNonce": 1109326938, + "isDeleted": false, + "id": "wEyfkGDmhk1mtJ_8R8RzR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 696.1042085606559, + "y": 654.2045002406462, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 100.91339598507477, + "height": 81.54772856906061, + "seed": 1612632218, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249975182, + "link": "", + "locked": false, + "startBinding": { + "elementId": "tmkH2xuhmAF0sXW0jY4qH", + "focus": 0.030744572000449822, + "gap": 1.8788337284699992 + }, + "endBinding": { + "elementId": "EP0vhCSYGfHck97YH-xoL", + "focus": 0.011660871820403415, + "gap": 1.6567716715856022 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "triangle", + "points": [ + [ + 0, + 0 + ], + [ + 0, + -41.54772856906055 + ], + [ + -99.82183964831309, + -41.54772856906055 + ], + [ + -100.91339598507477, + -81.54772856906061 + ] + ] + }, + { + "type": "arrow", + "version": 1530, + "versionNonce": 1886544154, + "isDeleted": false, + "id": "x8h2ozCdnWr9fPeldYKMP", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 930.7731009751593, + "y": 655.2220637400633, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 100.91339598507454, + "height": 81.5477285690605, + "seed": 1805001414, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249975182, + "link": "", + "locked": false, + "startBinding": { + "elementId": "2c1lVUWxftqhZNv88TyC5", + "focus": 0.009036918503705534, + "gap": 1 + }, + "endBinding": { + "elementId": "ySuuzFZh2ir5U4fU09Ov6", + "focus": -0.09324984264576391, + "gap": 2.674335171002781 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "triangle", + "points": [ + [ + 0, + 0 + ], + [ + 0, + -41.547728569060496 + ], + [ + 99.82183964831296, + -41.547728569060496 + ], + [ + 100.91339598507454, + -81.5477285690605 + ] + ] + }, + { + "type": "arrow", + "version": 1633, + "versionNonce": 1328764294, + "isDeleted": false, + "id": "ujPJ-Hr9B2-vmWhgspcOl", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1132.6991494225467, + "y": 656.0400613362899, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 100.91339598507466, + "height": 81.54772856906061, + "seed": 696487878, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1687249975182, + "link": "", + "locked": false, + "startBinding": { + "elementId": "cjqOxNQpQ_zpGwBphnsTk", + "focus": 0.05028921749659047, + "gap": 1 + }, + "endBinding": { + "elementId": "ySuuzFZh2ir5U4fU09Ov6", + "focus": -0.06567172735635796, + "gap": 3.4923327672293 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "triangle", + "points": [ + [ + 0, + 0 + ], + [ + 0, + -41.54772856906055 + ], + [ + -99.82183964831309, + -41.54772856906055 + ], + [ + -100.91339598507466, + -81.54772856906061 + ] + ] + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/docs/mech-hierarchy.png b/docs/mech-hierarchy.png new file mode 100644 index 0000000..1540a9c Binary files /dev/null and b/docs/mech-hierarchy.png differ diff --git a/docs/mech-hierarchy.svg b/docs/mech-hierarchy.svg new file mode 100644 index 0000000..8aa97a6 --- /dev/null +++ b/docs/mech-hierarchy.svg @@ -0,0 +1,17 @@ + + + + + + + + 4337 AccountMechThresholdTokenbound6551 AccountZodiacERC20ERC1155ERC721ERC1155 \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 41f7703..e0a953a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,30 +3,30 @@ "version": "1.0.0", "private": true, "devDependencies": { - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "@walletconnect/client": "^1.8.0", - "@walletconnect/core": "^2.7.3", - "@walletconnect/utils": "^1.8.0", - "@walletconnect/web3wallet": "^1.7.1", - "@web3modal/ethereum": "^2.7.0", - "@web3modal/react": "^2.7.0", + "0xsequence": "^1.2.6", + "@types/react": "^18.2.21", + "@types/react-dom": "^18.2.7", + "@walletconnect/core": "^2.10.2", + "@walletconnect/utils": "^2.10.2", + "@walletconnect/web3wallet": "^1.9.2", + "@web3modal/ethereum": "^2.7.1", + "@web3modal/react": "^2.7.1", "@web3modal/standalone": "^2.4.3", "buffer": "^6.0.3", "clsx": "^1.2.1", "copy-to-clipboard": "^3.3.3", "cryptocurrency-icons": "^0.18.1", "ethereum-blockies-base64": "^1.0.2", - "ethers": "^5.7.2", + "ethers": "^6.8.0", "mech-sdk": "workspace:^", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.8.0", + "react-router-dom": "^6.15.0", "react-scripts": "5.0.1", - "typescript": "^4.9.5", - "typescript-plugin-css-modules": "^4.1.1", - "viem": "^0.3.24", - "wagmi": "^1.0.4" + "typescript": "^5.2.2", + "typescript-plugin-css-modules": "^5.0.1", + "viem": "^1.16.4", + "wagmi": "^1.4.3" }, "scripts": { "start": "react-scripts start", diff --git a/frontend/src/chains.ts b/frontend/src/chains.ts index c0f3fc1..db37a33 100644 --- a/frontend/src/chains.ts +++ b/frontend/src/chains.ts @@ -1,3 +1,4 @@ +import { SequenceIndexerServices } from "@0xsequence/indexer" import { mainnet, goerli, @@ -23,3 +24,11 @@ export const CHAINS = { export type ChainId = keyof typeof CHAINS export const DEFAULT_CHAIN = CHAINS[5] + +export const SEQUENCER_ENDPOINTS: Record = { + 1: SequenceIndexerServices.MAINNET, + 5: SequenceIndexerServices.GOERLI, + 100: SequenceIndexerServices.GNOSIS, + 137: SequenceIndexerServices.POLYGON, + 80001: SequenceIndexerServices.POLYGON_MUMBAI, +} diff --git a/frontend/src/components/Connect/index.tsx b/frontend/src/components/Connect/index.tsx index 9dbd00d..27352e6 100644 --- a/frontend/src/components/Connect/index.tsx +++ b/frontend/src/components/Connect/index.tsx @@ -1,7 +1,5 @@ import React, { useState } from "react" -import useWalletConnect, { - SessionWithMetadata, -} from "../../hooks/useWalletConnect" +import useWalletConnect, { Session } from "../../hooks/useWalletConnect" import Spinner from "../Spinner" import classes from "./Connect.module.css" @@ -14,7 +12,7 @@ const MechConnect: React.FC = () => { const disconnectAll = () => { sessions.forEach((session) => { - disconnect(session.legacy ? session.uri : session.topic) + disconnect(session.topic) }) } @@ -77,7 +75,7 @@ const MechConnect: React.FC = () => { export default MechConnect const SessionItem: React.FC<{ - session: SessionWithMetadata + session: Session disconnect: (uriOrTopic: string) => void }> = ({ session, disconnect }) => { const icon = session.metadata?.icons[0] || walletConnectLogo @@ -100,7 +98,7 @@ const SessionItem: React.FC<{ - )} + {isLoading && } ) } diff --git a/frontend/src/components/NFTGridItem/index.tsx b/frontend/src/components/NFTGridItem/index.tsx index 6a1df48..4e4229f 100644 --- a/frontend/src/components/NFTGridItem/index.tsx +++ b/frontend/src/components/NFTGridItem/index.tsx @@ -1,3 +1,4 @@ +import { TokenBalance } from "@0xsequence/indexer" import { useState } from "react" import copy from "copy-to-clipboard" import clsx from "clsx" @@ -7,47 +8,49 @@ import classes from "./NFTItem.module.css" import Button from "../Button" import { shortenAddress } from "../../utils/shortenAddress" import Spinner from "../Spinner" -import { MechNFT } from "../../hooks/useNFTsByOwner" import ChainIcon from "../ChainIcon" import { calculateMechAddress } from "../../utils/calculateMechAddress" import { CHAINS, ChainId } from "../../chains" import { useDeployMech } from "../../hooks/useDeployMech" interface Props { - nftData: MechNFT + tokenBalance: TokenBalance } -const NFTGridItem: React.FC = ({ nftData }) => { +const NFTGridItem: React.FC = ({ tokenBalance }) => { const [imageError, setImageError] = useState(false) - const chain = CHAINS[parseInt(nftData.blockchain.shortChainID) as ChainId] + const chain = CHAINS[tokenBalance.chainId as ChainId] - const mechAddress = calculateMechAddress(nftData) - const { deploy, deployPending, deployed } = useDeployMech(nftData) + const mechAddress = calculateMechAddress(tokenBalance) + const { deploy, deployPending, deployed } = useDeployMech(tokenBalance) + + const name = + tokenBalance.tokenMetadata?.name || tokenBalance.contractInfo?.name return (

- {nftData.nft.title || nftData.nft.contractTitle || "..."} + {name || "..."}

- {nftData.nft.tokenID.length < 5 && ( -

{nftData.nft.tokenID || "..."}

+ {tokenBalance.tokenID.length < 5 && ( +

{tokenBalance.tokenID || "..."}

)}
- {(!nftData.nft.previews || imageError) && ( + {(imageError || !tokenBalance.tokenMetadata?.image) && (
)} - {!imageError && nftData.nft.previews && ( + {!imageError && tokenBalance.tokenMetadata?.image && (
{nftData.nft.contractTitle} setImageError(true)} /> @@ -68,7 +71,7 @@ const NFTGridItem: React.FC = ({ nftData }) => {
{deployed ? (