Skip to content

Commit 6d2b529

Browse files
committed
Add ScheduledTxModule with EIP-712 support and related tests
- Implement ScheduledTxModule contract for scheduling transactions with time-based execution. - Introduce ISafe interface for interacting with Safe contracts. - Add MockERC20 and TestSafeBase for testing purposes. - Configure submodules for safe-smart-account and openzeppelin-contracts. - Update remappings for new dependencies.
1 parent 96b33e1 commit 6d2b529

File tree

10 files changed

+523
-4
lines changed

10 files changed

+523
-4
lines changed

.gitmodules

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
[submodule "lib/forge-std"]
22
path = lib/forge-std
33
url = https://github.com/foundry-rs/forge-std
4+
[submodule "lib/safe-smart-account"]
5+
path = lib/safe-smart-account
6+
url = https://github.com/safe-fndn/safe-smart-account
7+
branch = v1.4.1
8+
9+
[submodule "lib/openzeppelin-contracts"]
10+
path = lib/openzeppelin-contracts
11+
url = https://github.com/OpenZeppelin/openzeppelin-contracts

foundry.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,12 @@
22
src = "src"
33
out = "out"
44
libs = ["lib"]
5+
optimizer = true
6+
optimizer_runs = 200
7+
solc_version = "0.8.29"
8+
9+
# Safe v1.4.1 contracts were compiled without via-ir
10+
# Enabling via-ir causes stack-too-deep errors in Safe's execTransaction
11+
# via_ir = false (default)
512

613
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

lib/openzeppelin-contracts

Submodule openzeppelin-contracts added at fcbae53

lib/safe-smart-account

Submodule safe-smart-account added at bf943f8

remappings.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
safe-smart-account/=lib/safe-smart-account/contracts/
2+
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/

src/ScheduledTxModule.sol

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,103 @@
11
// SPDX-License-Identifier: UNLICENSED
22
pragma solidity ^0.8.13;
33

4-
contract ScheduledTxModule {}
4+
import {ISafe} from "./interfaces/ISafe.sol";
5+
6+
/**
7+
* @title ScheduledTxModule
8+
* @notice A Safe module that allows scheduling transactions with time-based execution windows
9+
* @dev Uses EIP-712 signatures to authorize scheduled transactions that can only be executed within specified time windows
10+
*/
11+
contract ScheduledTxModule {
12+
/**
13+
* @notice EIP-712 type hash for scheduled transaction permits
14+
* @dev Used to generate the struct hash for signature verification
15+
*/
16+
bytes32 public immutable PERMIT_TYPEHASH = keccak256(
17+
"ScheduledTxModule(address to,uint256 value,bytes data,uint256 nonce,uint64 executeAfter,uint64 deadline)"
18+
);
19+
20+
/**
21+
* @notice Tracks which nonces have been used for each Safe
22+
* @dev Mapping from Safe address to nonce to execution status
23+
*/
24+
mapping(address safe => mapping(uint256 nonce => bool used)) public nonces;
25+
26+
/**
27+
* @notice Thrown when attempting to execute a transaction after its deadline
28+
*/
29+
error TransactionExpired();
30+
31+
/**
32+
* @notice Thrown when attempting to reuse a nonce that has already been executed
33+
*/
34+
error AlreadyExecuted();
35+
36+
/**
37+
* @notice Thrown when attempting to execute a transaction before its executeAfter timestamp
38+
*/
39+
error TooEarly();
40+
41+
/**
42+
* @notice Thrown when the module transaction execution fails
43+
*/
44+
error ModuleTxFailed();
45+
46+
/**
47+
* @notice Generates the EIP-712 domain separator for this contract
48+
* @dev The domain separator includes the contract name, version, chain ID, and address
49+
* @return The EIP-712 domain separator hash
50+
*/
51+
function getDomainSeparator() private view returns (bytes32) {
52+
return keccak256(
53+
abi.encode(
54+
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
55+
keccak256(bytes("ScheduledTxModule")),
56+
keccak256(bytes("1")),
57+
block.chainid,
58+
address(this)
59+
)
60+
);
61+
}
62+
63+
/**
64+
* @notice Executes a scheduled transaction on behalf of a Safe
65+
* @dev Validates timing constraints, signature, and marks nonce as used before execution
66+
* @param safe The address of the Safe contract that will execute the transaction
67+
* @param to The destination address for the transaction
68+
* @param value The amount of ETH to send with the transaction (in wei)
69+
* @param data The calldata to send with the transaction
70+
* @param nonce A unique identifier for this scheduled transaction (prevents replay attacks)
71+
* @param executeAfter The earliest timestamp when this transaction can be executed
72+
* @param deadline The latest timestamp when this transaction can be executed
73+
* @param signatures EIP-712 signature(s) from Safe owner(s) authorizing this transaction
74+
* @custom:reverts TooEarly if current timestamp is before executeAfter
75+
* @custom:reverts TransactionExpired if current timestamp is after deadline
76+
* @custom:reverts AlreadyExecuted if this nonce has already been used for this Safe
77+
* @custom:reverts ModuleTxFailed if the Safe transaction execution fails
78+
*/
79+
function execute(
80+
address safe,
81+
address to,
82+
uint256 value,
83+
bytes memory data,
84+
uint256 nonce,
85+
uint64 executeAfter,
86+
uint64 deadline,
87+
bytes memory signatures
88+
) external {
89+
require(block.timestamp <= deadline, TransactionExpired());
90+
require(block.timestamp >= executeAfter, TooEarly());
91+
require(nonces[safe][nonce] == false, AlreadyExecuted());
92+
93+
nonces[safe][nonce] = true;
94+
95+
bytes32 signatureData = keccak256(abi.encode(PERMIT_TYPEHASH, to, value, data, nonce, executeAfter, deadline));
96+
97+
bytes32 hash = keccak256(abi.encodePacked("\x19\x01", getDomainSeparator(), signatureData));
98+
99+
ISafe(payable(safe)).checkSignatures(hash, abi.encodePacked(signatureData), signatures);
100+
101+
require(ISafe(payable(safe)).execTransactionFromModule(to, value, data, 0), ModuleTxFailed());
102+
}
103+
}

src/interfaces/ISafe.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
interface ISafe {
5+
function checkSignatures(bytes32 dataHash, bytes memory data, bytes memory signatures) external;
6+
function execTransactionFromModule(address to, uint256 value, bytes memory data, uint8 operation)
7+
external
8+
returns (bool success);
9+
}

0 commit comments

Comments
 (0)