# Building Applications with Safe Modules

This tutorial demonstrates how to:
- Create a Safe Module
- Enable a module on a Safe account
- Execute transactions through the module

You will build a `TokenWithdrawModule` that enables beneficiaries to withdraw ERC20 tokens from a Safe account using off-chain signatures from Safe owners.

## Prerequisites

- Experience with [Solidity](https://docs.soliditylang.org/en/latest/) and [Hardhat](https://hardhat.org)
- [Node.js](https://nodejs.org/en/download/package-manager) and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed

### Implementation Details

The module gives a third party an allowance via a signature for the token that can freely be used by that third party. In the module this third party is represented as the "beneficiary" in the EIP-712 struct (mentioned in the PERMIT_TYPEHASH). To use this allowance the third party / "beneficiary" calls the `tokenTransfer` method and specifies which token to use and who should receive it.
The Safe owners grant the permission for token transfer to the third party by signing the EIP-712 struct without requiring Safe owners to execute any on-chain transaction.

### Limitations
- Each beneficiary has a sequential nonce, requiring withdrawals to be processed in order
- The module is bound to a specific token and Safe address at deployment

<Callout type="warning" emoji="⚠️">
Important Notice: The smart contract code provided in this tutorial is intended solely for educational purposes and serves only as an illustrative example. This example code has not undergone any security audits or formal verification processes. Safe does not guarantee the reliability, security, or correctness of this example code. Before deploying any smart contract code in a production environment, developers must conduct a thorough security audit and ensure rigorous testing procedures have been performed.
</Callout>

## Project Setup

Create a new project directory and initialize npm:

```bash
mkdir safe-module-tutorial && cd safe-module-tutorial
npm init -y
```

You can choose all default values.

### Configure Dependencies

Add overrides in `package.json` so that there are no peer dependency related issues.

```json
{
  // ... existing content ...
  "overrides": {
    "@safe-global/safe-contracts": {
      "ethers": "^6.13.5"
    }
  }
}
```

Install the required dependencies:

```bash
npm add -D hardhat @safe-global/safe-contracts @openzeppelin/contracts hardhat-dependency-compiler
```

### Initialize hardhat project

```bash
npx hardhat init
```

Select `Create a TypeScript project` and leave the default values for the rest of the prompts.

Now, try compiling the contracts to ensure everything is set up correctly.

```bash
npx hardhat compile
```

### Update hardhat.config.ts

When compiling Safe contracts with solidity 0.8.x the bytecode size exceeds the limit of 24KB. To overcome this, set `allowUnlimitedContractSize` to `true` in the hardhat config.
In practise with production networks, use the officially deployed Safe contracts.
Also, add `dependencyCompiler` to import `SafeProxyFactory` contract.

```typescript
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "hardhat-dependency-compiler";

const config: HardhatUserConfig = {
  solidity: "0.8.28",
  networks: {
    hardhat: {
      allowUnlimitedContractSize: true, // Required for Safe contracts
    },
  },
  dependencyCompiler: {
    paths: [
      "@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol",
    ],
  },
};

export default config;

```

## Create a new Solidity contract

Delete the default `contracts/Lock.sol` and test file `test/Lock.ts` and create a new Solidity contract `TokenWithdrawModule.sol` in the `contracts` directory.

### Step 1. Create empty contract

```solidity
// SPDX-License-Identifier: LGPL-3.0
pragma solidity ^0.8.0;
// Imports will be added here

contract TokenWithdrawModule {
  // State variables will be added here

  // Constructor will be added here

  // Functions will be added here
}
```

Explanation:
- **SPDX License Identifier**: Specifies the license type.
- **`pragma solidity ^0.8.0`**: Defines the Solidity compiler version.
- **`contract TokenWithdrawModule`**: Declares the contract name.

### Step 2: Import required dependencies

```solidity
import "@safe-global/safe-contracts/contracts/common/Enum.sol";
import "@safe-global/safe-contracts/contracts/Safe.sol";
```

Explanation:
- **`Enum.sol`**: Provides Enum `Operation` which can have values like `Call` or `DelegateCall`. This will be used further in the contract when a module calls a Safe account where the module specifies the operation type.
- **`Safe.sol`**: Includes the Safe contract interface to interact with Safe accounts.

### Step 3: Define state variables

Declare the necessary state variables for the contract.

```solidity
bytes32 public immutable PERMIT_TYPEHASH =
    keccak256(
      "TokenWithdrawModule(uint256 amount,address beneficiary,uint256 nonce,uint256 deadline)"
    );
address public immutable safeAddress;
address public immutable tokenAddress;
mapping(address => uint256) public nonces;
```

Explanation:
- **`PERMIT_TYPEHASH`**: Used to construct the signature hash for the token transfer. 
- **`safeAddress`**: Stores the Safe contract address.
- **`tokenAddress`**: Stores the ERC20 token contract address.
- **`nonces`**: Tracks unique nonce to prevent replay attacks.

### Step 4: Create the Constructor

Define a constructor to initialize the Safe and token contract addresses.

```solidity
constructor(address _tokenAddress, address _safeAddress) {
    tokenAddress = _tokenAddress;
    safeAddress = _safeAddress;
}
```

- Initializes `tokenAddress` and `safeAddress` with provided values during deployment. Thus, in this module the token and Safe addresses are fixed.

### Step 5: Implement the `getDomainSeparator` function

Add a helper function to compute the EIP-712 domain separator.

```solidity
  function getDomainSeparator() private view returns (bytes32) {
      return keccak256(
          abi.encode(
              keccak256(
                  "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
              ),
              keccak256(bytes("TokenWithdrawModule")),
              keccak256(bytes("1")),
              block.chainid,
              address(this)
          )
      );
  }
```

Explanation:
- Computes the `EIP712Domain` separator for the current chain and contract.
- Ensures compatibility with the EIP-712 standard for off-chain signing.
- Using a Domain separator ensures that the signature is valid for specific contracts in context and the chain. Thus, preventing replay attacks.

### Step 6: Implement the `tokenTransfer` function

Add a function to handle token transfers from the Safe.

```solidity
function tokenTransfer(
    uint amount,
    address receiver,
    uint256 deadline,
    bytes memory signatures
) public {
    require(deadline >= block.timestamp, "expired deadline");

    bytes32 signatureData = keccak256(
        abi.encode(
            PERMIT_TYPEHASH,
            amount,
            msg.sender,
            nonces[msg.sender]++,
            deadline
        )
    );

    bytes32 hash = keccak256(
        abi.encodePacked("\x19\x01", getDomainSeparator(), signatureData)
    );

    Safe(payable(safeAddress)).checkSignatures(
        hash,
        abi.encodePacked(signatureData),
        signatures
    );

    bytes memory data = abi.encodeWithSignature(
        "transfer(address,uint256)",
        receiver,
        amount
    );

    // Calling `execTransactionFromModule` with the transaction data to execute the token transfer through the Safe account.
    require(
        Safe(payable(safeAddress)).execTransactionFromModule(
            tokenAddress,
            0,
            data,
            Enum.Operation.Call
        ),
        "Could not execute token transfer"
    );
}
```

Explanation:
1. **Parameter Validation**:
   - Ensure the `deadline` is valid.
   - Construct `signatureData` with the provided details and `PERMIT_TYPEHASH`.
2. **Hash Calculation**:
   - Compute the hash using the `EIP712` format to ensure signature consistency.
3. **Signature Verification**:
   - Call `checkSignatures` on the Safe to verify the signatures provided match the owners of the Safe.
4. **Transaction Execution**:
   - A module can use `execTransactionFromModule` or `execTransactionFromModuleReturnData` function to execute transactions through a Safe account on which the module is enabled.
   - Encode the transfer call using `abi.encodeWithSignature`.
   - Use `execTransactionFromModule` to execute the token transfer via the Safe.
   - Ensure execution succeeds, otherwise revert.

### Final contract code

Here is the complete code for reference with comments:

```solidity
// SPDX-License-Identifier: LGPL-3.0
pragma solidity ^0.8.0;
import "@safe-global/safe-contracts/contracts/common/Enum.sol";
import "@safe-global/safe-contracts/contracts/Safe.sol";

/**
 * @title TokenWithdrawModule
 * @dev This contract implements a Safe module that enables a user with a valid signature to
 *      transfer ERC20 tokens from a Safe contract to a specified receiver.
 */
contract TokenWithdrawModule {
    bytes32 public immutable PERMIT_TYPEHASH =
        keccak256(
            "TokenWithdrawModule(uint256 amount,address beneficiary,uint256 nonce,uint256 deadline)"
        );
    address public immutable safeAddress;
    address public immutable tokenAddress;
    mapping(address => uint256) public nonces;

    /**
     * @dev Constructor function for the contract
     * @param _tokenAddress address of the ERC20 token contract
     * @param _safeAddress address of the Safe contract
     */
    constructor(address _tokenAddress, address _safeAddress) {
        tokenAddress = _tokenAddress;
        safeAddress = _safeAddress;
    }

    /**
     * @dev Generates the EIP-712 domain separator for the contract.
     *
     * @return The EIP-712 domain separator.
     */
    function getDomainSeparator() private view returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    keccak256(
                        "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
                    ),
                    keccak256(bytes("TokenWithdrawModule")),
                    keccak256(bytes("1")),
                    block.chainid,
                    address(this)
                )
            );
    }

    /**
     * @dev Transfers the specified amount of tokens to a receiver address. The msg.sender must hold a valid signature.
     *      The msg.sender address must be used as the `beneficiary` parameter in the EIP-712 structured data for
     *      signature generation. However, msg.sender can specify a different `receiver` address to receive the tokens
     *      when withdrawing the tokens.
     * @param amount amount of tokens to be transferred
     * @param receiver address to which the tokens will be transferred
     * @param deadline deadline for the validity of the signature
     * @param signatures signatures of the Safe owner(s)
     */
    function tokenTransfer(
        uint amount,
        address receiver,
        uint256 deadline,
        bytes memory signatures
    ) public {
        require(deadline >= block.timestamp, "expired deadline");

        bytes32 signatureData = keccak256(
            abi.encode(
                PERMIT_TYPEHASH,
                amount,
                msg.sender,
                nonces[msg.sender]++,
                deadline
            )
        );

        bytes32 hash = keccak256(
            abi.encodePacked("\x19\x01", getDomainSeparator(), signatureData)
        );

        Safe(payable(safeAddress)).checkSignatures(
            hash,
            abi.encodePacked(signatureData),
            signatures
        );

        bytes memory data = abi.encodeWithSignature(
            "transfer(address,uint256)",
            receiver,
            amount
        );

        require(
            Safe(payable(safeAddress)).execTransactionFromModule(
                tokenAddress,
                0,
                data,
                Enum.Operation.Call
            ),
            "Could not execute token transfer"
        );
    }
}
```

### Create TestToken.sol contract

Create a new file in the `contracts` directory named `TestToken.sol` and add the following code:

```solidity
// SPDX-License-Identifier: LGPL-3.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract TestToken is ERC20, Ownable {
    constructor(
        string memory _name,
        string memory _symbol
    ) ERC20(_name, _symbol) Ownable(msg.sender){}

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}
```

## Testing the contract

### Step 1: Create test/utils/utils.ts file

Create a new file named `utils.ts` in the `test/utils` directory and include the code below.

```typescript
import { ethers } from "hardhat";
import { Signer, AddressLike, BigNumberish, ZeroAddress } from "ethers";
import { Safe } from "../../typechain-types";

/**
 * Executes a transaction on the Safe contract.
 * @param wallets - The signers of the transaction.
 * @param safe - The Safe contract instance.
 * @param to - The address to send the transaction to.
 * @param value - The value to send with the transaction.
 * @param data - The data to send with the transaction.
 * @param operation - The operation type (0 for call, 1 for delegate call).
 */
const execTransaction = async function (
  wallets: Signer[],
  safe: Safe,
  to: AddressLike,
  value: BigNumberish,
  data: string,
  operation: number,
): Promise<void> {
  // Get the current nonce of the Safe contract
  const nonce = await safe.nonce();

  // Get the transaction hash for the Safe transaction
  const transactionHash = await safe.getTransactionHash(
    to,
    value,
    data,
    operation,
    0,
    0,
    0,
    ZeroAddress,
    ZeroAddress,
    nonce
  );

  let signatureBytes = "0x";
  const bytesDataHash = ethers.getBytes(transactionHash);

  // Get the addresses of the signers
  const addresses = await Promise.all(wallets.map(wallet => wallet.getAddress()));
  // Sort the signers by their addresses
  const sorted = wallets.sort((a, b) => {
    const addressA = addresses[wallets.indexOf(a)];
    const addressB = addresses[wallets.indexOf(b)];
    return addressA.localeCompare(addressB, "en", { sensitivity: "base" });
  });

  // Sign the transaction hash with each signer
  for (let i = 0; i < sorted.length; i++) {
    const flatSig = (await sorted[i].signMessage(bytesDataHash))
      .replace(/1b$/, "1f")
      .replace(/1c$/, "20");
    signatureBytes += flatSig.slice(2);
  }

  // Execute the transaction on the Safe contract
  await safe.execTransaction(
    to,
    value,
    data,
    operation,
    0,
    0,
    0,
    ZeroAddress,
    ZeroAddress,
    signatureBytes
  );
};

export {
  execTransaction,
};
```

Explanation:
- This file contains utility function to execute transaction through the Safe account.

### Step 2: Start with a boilerplate test file

Create a new file named `TokenWithdrawModule.test.ts` and include the following basic structure that will be filled in later steps (ignore the warnings about unused imports):

```typescript
import { ethers } from "hardhat";
import { expect } from "chai";
import { Signer, TypedDataDomain, ZeroAddress } from "ethers";
import { Safe, TestToken, TokenWithdrawModule } from "../typechain-types";
import { execTransaction } from "./utils/utils";

describe("TokenWithdrawModule Tests", function () {
  // Define variables
  let deployer: Signer;
  let alice: Signer;
  let bob: Signer;
  let charlie: Signer;
  let masterCopy: any;
  let token: TestToken;
  let safe: Safe;
  let safeAddress: string;
  let chainId: bigint;
   
  // Before hook to setup the contracts
  before(async () => {
  });

  // Enable the module in the Safe
  const enableModule = async () => {
  }

  // Add your test cases here
  it("Should successfully transfer tokens to bob", async function () {
  });
});
```

### Step 3: Setup contracts and variables in before hook

```typescript
  // Setup signers and deploy contracts before running tests
  before(async () => {
    [deployer, alice, bob, charlie] = await ethers.getSigners();

    chainId = (await ethers.provider.getNetwork()).chainId;
    const safeFactory = await ethers.getContractFactory("Safe", deployer);
    masterCopy = await safeFactory.deploy();

    // Deploy a new token contract
    token = await (
      await ethers.getContractFactory("TestToken", deployer)
    ).deploy("test", "T");

    // Deploy a new SafeProxyFactory contract
    const proxyFactory = await (
      await ethers.getContractFactory("SafeProxyFactory", deployer)
    ).deploy();

    // Setup the Safe, Step 1, generate transaction data
    const safeData = masterCopy.interface.encodeFunctionData("setup", [
      [await alice.getAddress()],
      1,
      ZeroAddress,
      "0x",
      ZeroAddress,
      ZeroAddress,
      0,
      ZeroAddress,
      ]);

    // Read the safe address by executing the static call to createProxyWithNonce function
    safeAddress = await proxyFactory.createProxyWithNonce.staticCall(
      await masterCopy.getAddress(),
      safeData,
      0n
    );
    
    if (safeAddress === ZeroAddress) {
      throw new Error("Safe address not found");
    }

    // Setup the Safe, Step 2, execute the transaction
    await proxyFactory.createProxyWithNonce(
      await masterCopy.getAddress(),
      safeData,
      0n
    );

    safe = await ethers.getContractAt("Safe", safeAddress);

    // Mint tokens to the safe address
    await token
      .connect(deployer)
      .mint(safeAddress, BigInt(10) ** BigInt(18) * BigInt(100000));
  });

```

This step sets up the test environment by deploying and configuring the necessary contracts. Please note that:

- Alice is the only owner of the Safe and a threshold of 1 is set. Thus, only Alice's signature is required to execute transactions.
- We can receive the Safe address before deploying the Safe.

### Step 4: Deploy and enable module in `enableModule` function

```typescript
  // A Safe Module is a smart contract that is allowed to execute transactions on behalf of a Safe Smart Account.
  // This function deploys the TokenWithdrawModule contract and enables it in the Safe.
  const enableModule = async (): Promise<{
    tokenWithdrawModule: TokenWithdrawModule;
  }> => {
    // Deploy the TokenWithdrawModule contract and pass the token and safe address as arguments
    const tokenWithdrawModule = await (
      await ethers.getContractFactory("TokenWithdrawModule", deployer)
    ).deploy(token.target, safeAddress);

    // Enable the module in the safe, Step 1, generate transaction data
    const enableModuleData = masterCopy.interface.encodeFunctionData(
      "enableModule",
      [tokenWithdrawModule.target]
    );

    // Enable the module in the safe, Step 2, execute the transaction
    await execTransaction([alice], safe, safe.target, 0, enableModuleData, 0);

    // Verify that the module is enabled
    expect(await safe.isModuleEnabled.staticCall(tokenWithdrawModule.target)).to
      .be.true;

    return { tokenWithdrawModule };
  };
```

This step deploys the TokenWithdrawModule contract and enables it in the Safe.

Please note that:
- Alice as the owner of the Safe is required to enable the module.
- The module is enabled by calling the `enableModule` function on the Safe contract.
- The `enableModule` function is called with the address of the newly deployed module.
- ⚠️ Security Note: Only trusted and audited code should be enabled as a module, since modules have full access to the Safe's assets. A malicious module could drain all funds.

### Step 5: Add test case

```typescript

  // Test case to verify token transfer to bob
  it("Should successfully transfer tokens to bob", async function () {
    // Enable the module in the Safe
    const { tokenWithdrawModule } = await enableModule();

    const amount = 10000000000000000000n; // 10 * 10^18
    const deadline = 100000000000000n;
    const nonce = await tokenWithdrawModule.nonces(await bob.getAddress());

    // Our module expects a EIP-712 typed signature, so we need to define the EIP-712 domain, ...
    const domain: TypedDataDomain = {
      name: "TokenWithdrawModule",
      version: "1",
      chainId: chainId,
      verifyingContract: await tokenWithdrawModule.getAddress(),
    };

    // ... and EIP-712 types ...
    const types = {
      TokenWithdrawModule: [
        { name: "amount", type: "uint256" },
        { name: "beneficiary", type: "address" },
        { name: "nonce", type: "uint256" },
        { name: "deadline", type: "uint256" },
      ],
    };

    // ... and EIP-712 values ...
    const value = {
      amount: amount,
      beneficiary: await bob.getAddress(),
      nonce: nonce,
      deadline: deadline,
    };

    // ... and finally hash the data using EIP-712
    const digest = ethers.TypedDataEncoder.hash(domain, types, value);
    const bytesDataHash = ethers.getBytes(digest);
    let signatureBytes = "0x";

    // Alice signs the digest
    const flatSig = (await alice.signMessage(bytesDataHash))
      .replace(/1b$/, "1f")
      .replace(/1c$/, "20");
    signatureBytes += flatSig.slice(2);

    // We want to make sure that an invalid signer cannot call the module even with a valid signature
    // We test this before the valid transaction, because it would fail because of an invalid nonce otherwise
    await expect(
      tokenWithdrawModule
        .connect(charlie)
        .tokenTransfer(
          amount,
          await charlie.getAddress(),
          deadline,
          signatureBytes
        )
    ).to.be.revertedWith("GS026");

    // Now we use the signature to transfer via our module
    await tokenWithdrawModule
      .connect(bob)
      .tokenTransfer(amount, await bob.getAddress(), deadline, signatureBytes);

    // Verify the token balance of bob (should be 10000000000000000000)
    const balanceBob = await token.balanceOf.staticCall(await bob.getAddress());
    expect(balanceBob).to.be.equal(amount);

    // All done.
  });
```

This step tests the token transfer functionality of the module.

Note that:
- The module can execute transactions on behalf of the Safe by calling the `execTransactionFromModule` function.
- We added an security check to the module that checks if the signers of a Safe signed the typed EIP-712 data. A module without this check could be called by any address.

### Final test code

Here is the complete code for reference:

```typescript
// Import necessary libraries and types
import { ethers } from "hardhat";
import { expect } from "chai";
import { Signer, TypedDataDomain, ZeroAddress } from "ethers";
import {
  Safe,
  TestToken,
  TokenWithdrawModule,
} from "../typechain-types";
import { execTransaction } from "./utils/utils";

describe("TokenWithdrawModule Tests", function () {
  // Define variables
  let deployer: Signer;
  let alice: Signer;
  let bob: Signer;
  let charlie: Signer;
  let masterCopy: any;
  let token: TestToken;
  let safe: Safe;
  let safeAddress: string;
  let chainId: bigint;

  // Setup signers and deploy contracts before running tests
  before(async () => {
    [deployer, alice, bob, charlie] = await ethers.getSigners();

    chainId = (await ethers.provider.getNetwork()).chainId;
    const safeFactory = await ethers.getContractFactory("Safe", deployer);
    masterCopy = await safeFactory.deploy();

    // Deploy a new token contract
    token = await (
      await ethers.getContractFactory("TestToken", deployer)
    ).deploy("test", "T");

    // Deploy a new SafeProxyFactory contract
    const proxyFactory = await (
      await ethers.getContractFactory("SafeProxyFactory", deployer)
    ).deploy();

    // Setup the Safe, Step 1, generate transaction data
    const safeData = masterCopy.interface.encodeFunctionData("setup", [
      [await alice.getAddress()],
      1,
      ZeroAddress,
      "0x",
      ZeroAddress,
      ZeroAddress,
      0,
      ZeroAddress,
    ]);

    // Read the safe address by executing the static call to createProxyWithNonce function
    safeAddress = await proxyFactory.createProxyWithNonce.staticCall(
      await masterCopy.getAddress(),
      safeData,
      0n
    );

    if (safeAddress === ZeroAddress) {
      throw new Error("Safe address not found");
    }

    // Setup the Safe, Step 2, execute the transaction
    await proxyFactory.createProxyWithNonce(
      await masterCopy.getAddress(),
      safeData,
      0n
    );

    safe = await ethers.getContractAt("Safe", safeAddress);

    // Mint tokens to the safe address
    await token
      .connect(deployer)
      .mint(safeAddress, BigInt(10) ** BigInt(18) * BigInt(100000));
  });

  // A Safe Module is a smart contract that is allowed to execute transactions on behalf of a Safe Smart Account.
  // This function deploys the TokenWithdrawModule contract and enables it in the Safe.
  const enableModule = async (): Promise<{
    tokenWithdrawModule: TokenWithdrawModule;
  }> => {
    // Deploy the TokenWithdrawModule contract and pass the token and safe address as arguments
    const tokenWithdrawModule = await (
      await ethers.getContractFactory("TokenWithdrawModule", deployer)
    ).deploy(token.target, safeAddress);

    // Enable the module in the safe, Step 1, generate transaction data
    const enableModuleData = masterCopy.interface.encodeFunctionData(
      "enableModule",
      [tokenWithdrawModule.target]
    );

    // Enable the module in the safe, Step 2, execute the transaction
    await execTransaction([alice], safe, safe.target, 0, enableModuleData, 0);

    // Verify that the module is enabled
    expect(await safe.isModuleEnabled.staticCall(tokenWithdrawModule.target)).to
      .be.true;

    return { tokenWithdrawModule };
  };

  // Test case to verify token transfer to bob
  it("Should successfully transfer tokens to bob", async function () {
    // Enable the module in the Safe
    const { tokenWithdrawModule } = await enableModule();

    const amount = 10000000000000000000n; // 10 * 10^18
    const deadline = 100000000000000n;
    const nonce = await tokenWithdrawModule.nonces(await bob.getAddress());

    // Our module expects a EIP-712 typed signature, so we need to define the EIP-712 domain, ...
    const domain: TypedDataDomain = {
      name: "TokenWithdrawModule",
      version: "1",
      chainId: chainId,
      verifyingContract: await tokenWithdrawModule.getAddress(),
    };

    // ... and EIP-712 types ...
    const types = {
      TokenWithdrawModule: [
        { name: "amount", type: "uint256" },
        { name: "beneficiary", type: "address" },
        { name: "nonce", type: "uint256" },
        { name: "deadline", type: "uint256" },
      ],
    };

    // ... and EIP-712 values ...
    const value = {
      amount: amount,
      beneficiary: await bob.getAddress(),
      nonce: nonce,
      deadline: deadline,
    };

    // ... and finally hash the data using EIP-712
    const digest = ethers.TypedDataEncoder.hash(domain, types, value);
    const bytesDataHash = ethers.getBytes(digest);
    let signatureBytes = "0x";

    // Alice signs the digest
    const flatSig = (await alice.signMessage(bytesDataHash))
      .replace(/1b$/, "1f")
      .replace(/1c$/, "20");
    signatureBytes += flatSig.slice(2);

    // We want to make sure that an invalid signer cannot call the module even with a valid signature
    // We test this before the valid transaction, because it would fail because of an invalid nonce otherwise
    await expect(
      tokenWithdrawModule
        .connect(charlie)
        .tokenTransfer(
          amount,
          await charlie.getAddress(),
          deadline,
          signatureBytes
        )
    ).to.be.revertedWith("GS026");

    // Now we use the signature to transfer via our module
    await tokenWithdrawModule
      .connect(bob)
      .tokenTransfer(amount, await bob.getAddress(), deadline, signatureBytes);

    // Verify the token balance of bob (should be 10000000000000000000)
    const balanceBob = await token.balanceOf.staticCall(await bob.getAddress());
    expect(balanceBob).to.be.equal(amount);

    // All done.
  });
});
```

## Run the tests

```bash
npx hardhat test
```

Congratulations! You have successfully created, enabled and tested a Safe Module.

## Do more with Safe and Safe Modules

Did you encounter any difficulties? Let us know by opening [an issue](https://github.com/5afe/safe-module-tutorial-contracts/issues/new) or asking a question on [Stack Exchange](https://ethereum.stackexchange.com/questions/tagged/safe-core) with the `safe-core` tag.
