## Hats Wallets (tokenbound) from [Hats tokenbound github](https://github.com/Hats-Protocol/hats-wallet/tree/tokenbound) ```txt! Signing Messages Wearer(s) of the hat can sign messages on the hat's behalf. Other applications or contracts can verify that such signatures are valid by calling the isValidSignature() function, which takes the following arguments: _hash — the keccak256 hash of the signed message, which could be a transaction hash or a message hash _signature — the signature to verify The signature is considered valid if it is a... valid ECDSA signature by a wearer of the hat valid EIP-1271 signature by a wearer of the hat valid EIP-1271 signature by the HatsWallet itself This design follows Gnosis Mech's approach, and creates flexibility for recursive validation of nested signatures. See their docs for more details. ``` from https://github.com/Hats-Protocol/hats-wallet/blob/tokenbound/src/HatsWalletBase.sol ```solidity! // TODO need to rethink this flow for m-of-n hatswallet // if it's our own signature, we recursively check if it's valid if (!_isValidSigner(signingContract) && signingContract != address(this)) { return false; } return IERC1271(signingContract).isValidSignature(_hash, contractSignature) == IERC1271.isValidSignature.selector; } else { // This is an ECDSA signature if (_isValidSigner(ECDSA.recover(_hash, v, r, s))) { return true; } } ``` ```solidity! /// @inheritdoc ERC6551Account function _isValidSigner(address _signer, bytes memory /* context */ ) internal view override returns (bool) { return _isValidSigner(_signer); } /** * @dev Internal function to check if a given address is a valid signer for this HatsWallet. A signer is valid if they * are wearing the `hat` of this HatsWallet. * @param _signer The address to check */ function _isValidSigner(address _signer) internal view returns (bool) { return HATS().isWearerOfHat(_signer, hat()); ``` ## Gnosis mech [eip1271](https://github.com/gnosis/mech/tree/main#eip-1271-signatures) ```txt! Deterministic deployment 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). EIP-6551 token-bound account The token-bound versions of Mech adopts the 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 mech avoid using storage but instead solely rely on the immutable data in their own bytecode. 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. ``` ## Tokenbound https://docs.tokenbound.org/sdk/methods#isvalidsigner ### isValidSigner [](https://docs.tokenbound.org/sdk/methods#isvalidsigner) Checks if a tokenbound account has signing authorization. This determines whether the active `WalletClient` or `Signer` can be used to sign transactions on behalf of the TBA. **Returns** a Promise that resolves to true if the account is a valid signer, otherwise false NOTE: This method is not available to V2-based implementations ``` const isValidSigner = await tokenboundClient.isValidSigner({ account: ZORA721_TBA_ADDRESS, }) console.log('isValidSigner?', isValidSigner) ``` | Parameter | Description | Type | | --- | --- | --- | | **account** | The Tokenbound account address. | string | --- ### signMessage [](https://docs.tokenbound.org/sdk/methods#signmessage) Gets an [EIP-191](https://eips.ethereum.org/EIPS/eip-191) formatted signature for a message. **Returns** a Promise that resolves to a signed Hex string The message to be signed is typed as `UniversalSignableMessage` so that it can elegantly handle Ethers 5, Ethers 6, and viem's expected types for all signable formats. Check the types associated with signMessage for [viem](https://viem.sh/docs/actions/wallet/signMessage.html) , [Ethers 5](https://docs.ethers.org/v5/api/signer/#Signer-signMessage), and [Ethers 6](https://docs.ethers.org/v6/api/providers/#Signer-signMessage) as needed. ``` // Ethers 5 const arrayMessage: ArrayLike<number> = [72, 101, 108, 108, 111] // "Hello" in ASCII // Ethers 5 or Ethers 6 const uint8ArrayMessage: Uint8Array = new Uint8Array([72, 101, 108, 108, 111]) // "Hello" in ASCII ``` Note that this method is just for convenience. Since your EOA wallet is responsible for signing, messages can also be signed explicitly using your EOA wallet address in viem or Ethers. ``` const signedMessage = await tokenboundClient.signMessage({ message: "Ice cream so good", }) console.log(signedMessage) // Works in Ethers 5 or 6, throws in viem const signedUint8Message = await tokenboundClient.signMessage({ message: uint8ArrayMessage, }) console.log(signedUint8Message) // Works in viem const signedRawUint8Message = await tokenboundClient.signMessage({ message: {raw: uint8ArrayMessage}, }) console.log(signedUint8Message) ``` | Parameter | Description | Type | | --- | --- | --- | | **message** | The message to be signed. | UniversalSignableMessage | --- ### execute [](https://docs.tokenbound.org/sdk/methods#execute) Performs an arbitrary contract call against any contract. This means any onchain action you can perform with your EOA wallet can be done with your NFT's Tokenbound account. You can mint or transfer NFTs, approve contracts, make and vote on DAO proposals, and much more. **Note**: this method replaces the deprecated V2 method `executeCall`. **Returns** a hash of the transaction that executed a call using a Tokenbound account. ``` const executedCall = await tokenboundClient.execute({ account: "<account_address>", to: "<contract_address>", value: "<wei_value>", data: "<encoded_call_data>", }) console.log(executedCall) ``` | Parameter | Description | Type | | --- | --- | --- | | **account** | The Tokenbound account address. | string | | **to** | The contract address. | string | | **value** | The value to send, in wei. | bigint | | **data** (optional) | The ABI-encoded call data. | `0x{string}` | Here's a more robust example, where we see how to use your TBA to mint an NFT using Zora's [ERC721Drop contract](https://etherscan.io/address/0x7c74dfe39976dc395529c14e54a597809980e01c#code) by calling the contract's `purchase` function. ``` // Webb's First Deep Field (unlimited mint drop): // https://zora.co/collect/eth:0x28ee638f2fcb66b4106acab7efd225aeb2bd7e8d const zora721 = { abi: zora721DropABI, proxyContractAddress: getAddress( "0x28ee638f2fcb66b4106acab7efd225aeb2bd7e8d" ), mintPrice: BigInt(0), quantity: 2, tbaAddress: getAddress("0xc33f0A7FcD69Ba00b4e980463199CD38E30d0E5c"), } const encodedMintFunctionData = encodeFunctionData({ abi: zora721.abi, functionName: "purchase", args: [BigInt(zora721.quantity)], }) const mintToTBATxHash = await tokenboundClient.execute({ account: zora721.tbaAddress, to: zora721.proxyContractAddress, value: zora721.mintPrice * BigInt(zora721.quantity), data: encodedMintFunctionData, }) ``` ### Viem https://viem.sh/docs/actions/wallet/signMessage.html signMessage =========== Calculates an Ethereum-specific signature in [EIP-191 format](https://eips.ethereum.org/EIPS/eip-191): `keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))`. With the calculated signature, you can: - use [`verifyMessage`](https://viem.sh/docs/utilities/verifyMessage.html) to verify the signature, - use [`recoverMessageAddress`](https://viem.sh/docs/utilities/recoverMessageAddress.html) to recover the signing address from a signature. Usage ----- example.tsconfig.ts ts ``` import { account, walletClient } from './config' const signature = await walletClient.signMessage({ account, message: 'hello world', }) // "0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b1b" const signature = await walletClient.signMessage({ account, // Hex data representation of message. message: { raw: '0x68656c6c6f20776f726c64' }, }) // "0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b1b" ``` ### Account Hoisting If you do not wish to pass an `account` to every `signMessage`, you can also hoist the Account on the Wallet Client (see `config.ts`). [Learn more](https://viem.sh/docs/clients/wallet.html#withaccount). example.tsconfig.ts (JSON-RPC Account)config.ts (Local Account) ts ``` import { walletClient } from './config' const signature = await walletClient.signMessage({ message: 'hello world', }) // "0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b1b" ``` Returns ------- [`Hex`](https://viem.sh/docs/glossary/types.html#hex) The signed message. Parameters ---------- ### account - **Type:** `Account | Address` Account to use for signing. Accepts a [JSON-RPC Account](https://viem.sh/docs/clients/wallet.html#json-rpc-accounts) or [Local Account (Private Key, etc)](https://viem.sh/docs/clients/wallet.html#local-accounts-private-key-mnemonic-etc). ts ``` --- signTypedData Signs typed data and calculates an Ethereum-specific signature in https://eips.ethereum.org/EIPS/eip-712: sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message))) Usage example.ts data.ts config.ts ts import { account, walletClient } from './config' import { domain, types } from './data' const signature = await walletClient.signTypedData({ account, domain, types, primaryType: 'Mail', message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, contents: 'Hello, Bob!', }, }) Account Hoisting If you do not wish to pass an account to every signTypedData, you can also hoist the Account on the Wallet Client (see config.ts). Learn more. example.ts data.ts config.ts (JSON-RPC Account) config.ts (Local Account) ts import { walletClient } from './config' import { domain, types } from './data' const signature = await walletClient.signTypedData({ domain, types, primaryType: 'Mail', message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, contents: 'Hello, Bob!', }, }) Returns 0x${string} The signed data. Parameters account Type: Account | Address The Account to use for signing. Accepts a JSON-RPC Account or Local Account (Private Key, etc). ts const signature = await walletClient.signTypedData({ account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', domain: { name: 'Ether Mail', version: '1', chainId: 1, verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', }, types, primaryType: 'Mail', message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, contents: 'Hello, Bob!', }, }) domain Type: TypedDataDomain The typed data domain. ts const signature = await walletClient.signTypedData({ account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', domain: { name: 'Ether Mail', version: '1', chainId: 1, verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', }, types, primaryType: 'Mail', message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, contents: 'Hello, Bob!', }, }) types The type definitions for the typed data. ts const signature = await walletClient.signTypedData({ account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', domain, types: { Person: [ { name: 'name', type: 'string' }, { name: 'wallet', type: 'address' }, ], Mail: [ { name: 'from', type: 'Person' }, { name: 'to', type: 'Person' }, { name: 'contents', type: 'string' }, ], }, primaryType: 'Mail', message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, contents: 'Hello, Bob!', }, }) primaryType Type: Inferred string. The primary type to extract from types and use in value. ts const signature = await walletClient.signTypedData({ account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', domain, types: { Person: [ { name: 'name', type: 'string' }, { name: 'wallet', type: 'address' }, ], Mail: [ { name: 'from', type: 'Person' }, { name: 'to', type: 'Person' }, { name: 'contents', type: 'string' }, ], }, primaryType: 'Mail', message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, contents: 'Hello, Bob!', }, }) message Type: Inferred from types & primaryType. ts const signature = await walletClient.signTypedData({ account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', domain, types: { Person: [ { name: 'name', type: 'string' }, { name: 'wallet', type: 'address' }, ], Mail: [ { name: 'from', type: 'Person' }, { name: 'to', type: 'Person' }, { name: 'contents', type: 'string' }, ], }, primaryType: 'Mail', message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, contents: 'Hello, Bob!', }, }) const signature = await walletClient.signMessage({ account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', message: 'hello world', }) ``` ### message - **Type:** `string | { raw: Hex | ByteArray }` Message to sign. By default, viem signs the UTF-8 representation of the message. ts ``` const signature = await walletClient.signMessage({ account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', message: 'hello world', }) ``` To sign the data representation of the message, you can use the `raw` attribute. ts ``` const signature = await walletClient.signMessage({ account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', message: { raw: '0x68656c6c6f20776f726c64' }, }) ``` ## Tests **test NFT** https://tokenbound.org/assets/goerli/0x3bc1a0ad72417f2d411118085256fc53cbddd137/9516861173504175847517456325717929627793911981156822085829574417973248 **Test Tree** https://app.hatsprotocol.xyz/trees/5/353 TBA attached to Hat ID 350480129425236977933692273615846477432535368860960259044278607544320 0x03B8bbCb4E05C436b9d4e3B300eBfff2AC39Bb06 safe: gor:0xAeAee22030966Ab7BC06Ef3754018437b3911715 ![image](https://hackmd.io/_uploads/HyimopnVp.png)