Skip to content

Commit

Permalink
Merge pull request #27 from gnosis/erc-6551
Browse files Browse the repository at this point in the history
EIP-6551 compat
  • Loading branch information
jfschwarz authored Oct 13, 2023
2 parents dd81ff4 + 39ef6c7 commit e9cc900
Show file tree
Hide file tree
Showing 80 changed files with 6,309 additions and 3,777 deletions.
53 changes: 41 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
5 changes: 5 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
91 changes: 0 additions & 91 deletions contracts/ERC1155Mech.sol

This file was deleted.

48 changes: 48 additions & 0 deletions contracts/ERC1155ThresholdMech.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
80 changes: 80 additions & 0 deletions contracts/ERC1155TokenboundMech.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
29 changes: 29 additions & 0 deletions contracts/ERC20ThresholdMech.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
41 changes: 0 additions & 41 deletions contracts/ERC721Mech.sol

This file was deleted.

Loading

1 comment on commit e9cc900

@vercel
Copy link

@vercel vercel bot commented on e9cc900 Oct 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

mech – ./

mech-gnosis-guild.vercel.app
mech-git-main-gnosis-guild.vercel.app
mech-omega.vercel.app

Please sign in to comment.