Skip to content

Commit 38ca885

Browse files
Call Interval Enforcer
1 parent 0018e69 commit 38ca885

File tree

4 files changed

+170
-3
lines changed

4 files changed

+170
-3
lines changed

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// SPDX-License-Identifier: MIT AND Apache-2.0
2+
pragma solidity 0.8.23;
3+
4+
import { CaveatEnforcer } from "./CaveatEnforcer.sol";
5+
import { ModeCode } from "../utils/Types.sol";
6+
import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";
7+
8+
/**
9+
* @title CallIntervalEnforcer
10+
* @notice Enforces a minimum interval between consecutive calls for a given delegation.
11+
* @dev This enforcer restricts the frequency at which a delegation can be used, by requiring a minimum time interval between calls.
12+
* The interval is specified in the `_terms` parameter as a `uint256` encoded in 32 bytes.
13+
* Only operates in the default execution mode.
14+
*
15+
* - The `beforeHook` checks that the required interval has passed since the last call for the given delegation.
16+
* - The interval is enforced per (delegationManager, delegationHash) pair.
17+
* - The `lastCallExecution` mapping tracks the last execution timestamp for each delegation.
18+
*
19+
* Example usage:
20+
* - To allow a delegation to be used only once every 24 hours, set `_terms` to `uint256(86400)` encoded as 32 bytes.
21+
*/
22+
contract CallIntervalEnforcer is CaveatEnforcer {
23+
using ExecutionLib for bytes;
24+
25+
/// @notice Tracks the last execution timestamp for each (delegationManager, delegationHash) pair.
26+
/// @dev delegationManager => delegationHash => last execution timestamp
27+
mapping(address delegationManager => mapping(bytes32 delegationHash => uint256 lastCallExecution)) public lastCallExecution;
28+
29+
/**
30+
* @notice Checks that the minimum interval between calls has passed before allowing execution.
31+
* @dev Reverts if the interval has not elapsed since the last call for this delegation.
32+
* The msg.sender is expected to be the delegation manager address.
33+
* @param _terms Encoded as 32 bytes, representing the minimum interval in seconds between calls.
34+
* @param _mode The execution mode. Must be the default execution mode.
35+
* @param _delegationHash The hash of the delegation being enforced.
36+
*/
37+
function beforeHook(
38+
bytes calldata _terms,
39+
bytes calldata,
40+
ModeCode _mode,
41+
bytes calldata,
42+
bytes32 _delegationHash,
43+
address,
44+
address
45+
)
46+
public
47+
override
48+
onlyDefaultExecutionMode(_mode)
49+
{
50+
(uint256 callInterval_) = getTermsInfo(_terms);
51+
52+
uint256 lastCallExecutedAt_ = lastCallExecution[msg.sender][_delegationHash];
53+
54+
if (callInterval_ > 0) {
55+
require((block.timestamp - lastCallExecutedAt_) > callInterval_, "CallIntervalEnforcer:early-delegation");
56+
}
57+
lastCallExecution[msg.sender][_delegationHash] = block.timestamp;
58+
}
59+
60+
/**
61+
* @notice Decodes the interval from the `_terms` parameter.
62+
* @dev Expects `_terms` to be exactly 32 bytes, representing a uint256 interval in seconds.
63+
* @param _terms The encoded interval.
64+
* @return interval_ The minimum interval in seconds between calls.
65+
*/
66+
function getTermsInfo(bytes calldata _terms) public pure returns (uint256 interval_) {
67+
require(_terms.length == 32, "CallIntervalEnforcer:invalid-terms-length");
68+
interval_ = uint256(bytes32(_terms));
69+
}
70+
}

src/utils/Types.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// SPDX-License-Identifier: MIT AND Apache-2.0
22
pragma solidity 0.8.23;
33

4-
import { PackedUserOperation } from "@account-abstraction/interfaces/PackedUserOperation.sol";
5-
import { Execution } from "@erc7579/interfaces/IERC7579Account.sol";
6-
import { ModeCode, CallType, ExecType, ModeSelector, ModePayload } from "@erc7579/lib/ModeLib.sol";
4+
import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol";
5+
import {Execution} from "@erc7579/interfaces/IERC7579Account.sol";
6+
import {ModeCode, CallType, ExecType, ModeSelector, ModePayload} from "@erc7579/lib/ModeLib.sol";
77

88
/**
99
* @title EIP712Domain
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// SPDX-License-Identifier: MIT AND Apache-2.0
2+
pragma solidity 0.8.23;
3+
4+
import "forge-std/Test.sol";
5+
import { CallIntervalEnforcer } from "../../src/enforcers/CallIntervalEnforcer.sol";
6+
import { ModeCode } from "../../src/utils/Types.sol";
7+
8+
contract CallIntervalEnforcerTest is Test {
9+
CallIntervalEnforcer public enforcer;
10+
address public delegationManager = address(0x1234);
11+
bytes32 public delegationHash = keccak256("test-delegation");
12+
uint256 public interval = 1 hours;
13+
bytes public terms;
14+
15+
ModeCode public defaultMode = ModeCode.wrap(bytes32(0));
16+
17+
function setUp() public {
18+
vm.warp(10000);
19+
enforcer = new CallIntervalEnforcer();
20+
terms = abi.encodePacked(interval);
21+
}
22+
23+
function testRevertOnInvalidTermsLength() public {
24+
bytes memory invalidTerms = new bytes(31);
25+
vm.expectRevert("CallIntervalEnforcer:invalid-terms-length");
26+
enforcer.getTermsInfo(invalidTerms);
27+
}
28+
29+
function testFirstCallSucceeds() public {
30+
vm.prank(delegationManager);
31+
enforcer.beforeHook(terms, "", defaultMode, "", delegationHash, address(0), address(0));
32+
uint256 lastCall = enforcer.lastCallExecution(delegationManager, delegationHash);
33+
assertEq(lastCall, block.timestamp);
34+
}
35+
36+
function testRevertIfIntervalNotElapsed() public {
37+
// First call
38+
vm.prank(delegationManager);
39+
enforcer.beforeHook(terms, "", defaultMode, "", delegationHash, address(0), address(0));
40+
// Second call immediately
41+
vm.prank(delegationManager);
42+
vm.expectRevert("CallIntervalEnforcer:early-delegation");
43+
enforcer.beforeHook(terms, "", defaultMode, "", delegationHash, address(0), address(0));
44+
}
45+
46+
function testCallSucceedsAfterInterval() public {
47+
// First call
48+
vm.prank(delegationManager);
49+
enforcer.beforeHook(terms, "", defaultMode, "", delegationHash, address(0), address(0));
50+
// Warp time forward by interval + 1
51+
vm.warp(block.timestamp + interval + 1);
52+
// Second call
53+
vm.prank(delegationManager);
54+
enforcer.beforeHook(terms, "", defaultMode, "", delegationHash, address(0), address(0));
55+
uint256 lastCall = enforcer.lastCallExecution(delegationManager, delegationHash);
56+
assertEq(lastCall, block.timestamp);
57+
}
58+
59+
function testZeroIntervalAllowsImmediateCalls() public {
60+
bytes memory zeroTerms = abi.encodePacked(uint256(0));
61+
// First call
62+
vm.prank(delegationManager);
63+
enforcer.beforeHook(zeroTerms, "", defaultMode, "", delegationHash, address(0), address(0));
64+
// Second call immediately
65+
vm.prank(delegationManager);
66+
enforcer.beforeHook(zeroTerms, "", defaultMode, "", delegationHash, address(0), address(0));
67+
// Should not revert
68+
}
69+
70+
function testDifferentDelegationHashesTrackedIndependently() public {
71+
bytes32 hash1 = keccak256("hash1");
72+
bytes32 hash2 = keccak256("hash2");
73+
vm.prank(delegationManager);
74+
enforcer.beforeHook(terms, "", defaultMode, "", hash1, address(0), address(0));
75+
vm.prank(delegationManager);
76+
enforcer.beforeHook(terms, "", defaultMode, "", hash2, address(0), address(0));
77+
assertEq(enforcer.lastCallExecution(delegationManager, hash1), block.timestamp);
78+
assertEq(enforcer.lastCallExecution(delegationManager, hash2), block.timestamp);
79+
}
80+
81+
function testDifferentManagersTrackedIndependently() public {
82+
address manager1 = address(0x1111);
83+
address manager2 = address(0x2222);
84+
vm.prank(manager1);
85+
enforcer.beforeHook(terms, "", defaultMode, "", delegationHash, address(0), address(0));
86+
vm.prank(manager2);
87+
enforcer.beforeHook(terms, "", defaultMode, "", delegationHash, address(0), address(0));
88+
assertEq(enforcer.lastCallExecution(manager1, delegationHash), block.timestamp);
89+
assertEq(enforcer.lastCallExecution(manager2, delegationHash), block.timestamp);
90+
}
91+
}

0 commit comments

Comments
 (0)