Skip to content

Commit 07c9780

Browse files
jxomlegion2002actions-userhowydev
authored
feat: add spend limit enabled flag (#372)
* feat: add spend limit enabled flag * fix: move disabled flag to the end of GuardedExecutorKey storage, so that it doesn't affect user storage space after contract upgrade * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * chore: add upgrade test * ? --------- Co-authored-by: Tanishk Goyal <[email protected]> Co-authored-by: GitHub Action <[email protected]> Co-authored-by: howy <[email protected]>
1 parent 475da0f commit 07c9780

File tree

7 files changed

+404
-16
lines changed

7 files changed

+404
-16
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@
1717
path = lib/solady
1818
url = https://github.com/vectorized/solady
1919
branch = main
20+
[submodule "lib/account_v0_5_5"]
21+
path = lib/account_v0_5_5
22+
url = https://github.com/ithacaxyz/account

lib/account_v0_5_5

Submodule account_v0_5_5 added at 35bda33

snapshots/BenchmarkTest.json

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
"testERC20Transfer_ERC4337MinimalAccount": "171906",
1717
"testERC20Transfer_ERC4337MinimalAccount_AppSponsor": "168917",
1818
"testERC20Transfer_ERC4337MinimalAccount_ERC20SelfPay": "217488",
19-
"testERC20Transfer_IthacaAccount": "129918",
20-
"testERC20Transfer_IthacaAccountWithSpendLimits": "190787",
21-
"testERC20Transfer_IthacaAccount_AppSponsor": "141350",
22-
"testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "163827",
23-
"testERC20Transfer_IthacaAccount_ERC20SelfPay": "147559",
19+
"testERC20Transfer_IthacaAccount": "129986",
20+
"testERC20Transfer_IthacaAccountWithSpendLimits": "193499",
21+
"testERC20Transfer_IthacaAccount_AppSponsor": "141385",
22+
"testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "163850",
23+
"testERC20Transfer_IthacaAccount_ERC20SelfPay": "147639",
2424
"testERC20Transfer_Safe4337": "197515",
2525
"testERC20Transfer_Safe4337_AppSponsor": "191679",
2626
"testERC20Transfer_Safe4337_ERC20SelfPay": "238658",
@@ -29,25 +29,25 @@
2929
"testERC20Transfer_ZerodevKernel_ERC20SelfPay": "252683",
3030
"testERC20Transfer_batch100_AlchemyModularAccount": "10104466",
3131
"testERC20Transfer_batch100_AlchemyModularAccount_ERC20SelfPay": "11635798",
32-
"testERC20Transfer_batch100_IthacaAccount": "7695808",
33-
"testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8394864",
34-
"testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "8244232",
35-
"testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7540412",
32+
"testERC20Transfer_batch100_IthacaAccount": "7702476",
33+
"testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8397224",
34+
"testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "8246556",
35+
"testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7547236",
3636
"testERC20Transfer_batch100_ZerodevKernel": "12626718",
3737
"testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "14176437",
3838
"testNativeTransfer_AlchemyModularAccount": "180829",
3939
"testNativeTransfer_CoinbaseSmartWallet": "178916",
40-
"testNativeTransfer_IthacaAccount": "131320",
41-
"testNativeTransfer_IthacaAccount_AppSponsor": "142783",
42-
"testNativeTransfer_IthacaAccount_ERC20SelfPay": "156273",
40+
"testNativeTransfer_IthacaAccount": "131388",
41+
"testNativeTransfer_IthacaAccount_AppSponsor": "142818",
42+
"testNativeTransfer_IthacaAccount_ERC20SelfPay": "156341",
4343
"testNativeTransfer_Safe4337": "198595",
4444
"testNativeTransfer_ZerodevKernel": "208635",
4545
"testUniswapV2Swap_AlchemyModularAccount": "238767",
4646
"testUniswapV2Swap_CoinbaseSmartWallet": "237571",
4747
"testUniswapV2Swap_ERC4337MinimalAccount": "231242",
48-
"testUniswapV2Swap_IthacaAccount": "189228",
49-
"testUniswapV2Swap_IthacaAccount_AppSponsor": "200648",
50-
"testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "211681",
48+
"testUniswapV2Swap_IthacaAccount": "189296",
49+
"testUniswapV2Swap_IthacaAccount_AppSponsor": "200671",
50+
"testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "211749",
5151
"testUniswapV2Swap_Safe4337": "257453",
5252
"testUniswapV2Swap_ZerodevKernel": "266487"
5353
}

src/GuardedExecutor.sol

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ abstract contract GuardedExecutor is ERC7821 {
120120
/// @dev Emitted when a spend limit is removed.
121121
event SpendLimitRemoved(bytes32 keyHash, address token, SpendPeriod period);
122122

123+
/// @dev Emitted when spend limits are enabled or disabled for a key.
124+
event SpendLimitsEnabledSet(bytes32 keyHash, bool enabled);
125+
123126
////////////////////////////////////////////////////////////////////////
124127
// Constants
125128
////////////////////////////////////////////////////////////////////////
@@ -181,6 +184,8 @@ abstract contract GuardedExecutor is ERC7821 {
181184
SpendStorage spends;
182185
/// @dev Mapping of 3rd-party checkers for determining if an address can execute a function.
183186
EnumerableMapLib.AddressToAddressMap callCheckers;
187+
/// @dev Whether spend limits are disabled for this key. Defaults to false (limits enabled).
188+
bool spendLimitsDisabled;
184189
}
185190

186191
/// @dev Returns the storage pointer.
@@ -229,6 +234,11 @@ abstract contract GuardedExecutor is ERC7821 {
229234
return ERC7821._execute(calls, keyHash);
230235
}
231236

237+
// If spend limits are disabled for this key, execute without spend checks
238+
if (!spendLimitsEnabled(keyHash)) {
239+
return ERC7821._execute(calls, keyHash);
240+
}
241+
232242
SpendStorage storage spends = _getGuardedExecutorKeyStorage(keyHash).spends;
233243
_ExecuteTemps memory t;
234244

@@ -459,6 +469,21 @@ abstract contract GuardedExecutor is ERC7821 {
459469
emit SpendLimitRemoved(keyHash, token, period);
460470
}
461471

472+
/// @dev Sets whether spend limits are enabled or disabled for a specific key.
473+
/// By default, spend limits are enabled for all keys.
474+
function setSpendLimitsEnabled(bytes32 keyHash, bool enabled)
475+
public
476+
virtual
477+
onlyThis
478+
checkKeyHashIsNonZero(keyHash)
479+
{
480+
if (_isSuperAdmin(keyHash)) revert SuperAdminCanSpendAnything();
481+
482+
_getGuardedExecutorKeyStorage(keyHash).spendLimitsDisabled = !enabled;
483+
484+
emit SpendLimitsEnabledSet(keyHash, enabled);
485+
}
486+
462487
////////////////////////////////////////////////////////////////////////
463488
// Public View Functions
464489
////////////////////////////////////////////////////////////////////////
@@ -589,6 +614,13 @@ abstract contract GuardedExecutor is ERC7821 {
589614
}
590615
}
591616

617+
/// @dev Returns whether spend limits are enabled for a specific key.
618+
/// Defaults to true if never set.
619+
function spendLimitsEnabled(bytes32 keyHash) public view virtual returns (bool) {
620+
// disabled defaults to false, so !false = true (enabled by default)
621+
return !_getGuardedExecutorKeyStorage(keyHash).spendLimitsDisabled;
622+
}
623+
592624
/// @dev Rounds the unix timestamp down to the period.
593625
function startOfSpendPeriod(uint256 unixTimestamp, SpendPeriod period)
594626
public

src/IthacaAccount.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,6 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor {
763763
returns (string memory name, string memory version)
764764
{
765765
name = "IthacaAccount";
766-
version = "0.5.8";
766+
version = "0.5.9";
767767
}
768768
}

test/GuardedExecutor.t.sol

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,127 @@ contract GuardedExecutorTest is BaseTest {
654654
}
655655
}
656656

657+
function testSetSpendLimitsEnabled() public {
658+
Orchestrator.Intent memory u;
659+
DelegatedEOA memory d = _randomEIP7702DelegatedEOA();
660+
661+
u.eoa = d.eoa;
662+
u.combinedGas = 1000000;
663+
u.nonce = d.d.getNonce(0);
664+
665+
PassKey memory k = _randomSecp256k1PassKey();
666+
667+
address token = LibClone.clone(address(paymentToken));
668+
_mint(token, u.eoa, type(uint192).max);
669+
670+
// Test that spend limits are enabled by default for the key
671+
assertTrue(d.d.spendLimitsEnabled(k.keyHash), "Spend limits should be enabled by default");
672+
673+
// Authorize the key and set up spend limit
674+
{
675+
ERC7821.Call[] memory calls = new ERC7821.Call[](3);
676+
calls[0].data = abi.encodeWithSelector(IthacaAccount.authorize.selector, k.k);
677+
// Only allow the key to execute on the token, not on the account itself
678+
calls[1].data = abi.encodeWithSelector(
679+
GuardedExecutor.setCanExecute.selector, k.keyHash, token, _ANY_FN_SEL, true
680+
);
681+
calls[2] = _setSpendLimitCall(k, token, GuardedExecutor.SpendPeriod.Day, 1 ether);
682+
683+
u.executionData = abi.encode(calls);
684+
u.nonce = 0xc1d0 << 240;
685+
u.signature = _eoaSig(d.privateKey, u);
686+
687+
assertEq(oc.execute(abi.encode(u)), 0);
688+
}
689+
690+
// Test that non-EOA cannot call setSpendLimitsEnabled
691+
{
692+
ERC7821.Call[] memory calls = new ERC7821.Call[](1);
693+
calls[0].to = address(0);
694+
calls[0].data = abi.encodeWithSelector(
695+
GuardedExecutor.setSpendLimitsEnabled.selector, k.keyHash, false
696+
);
697+
698+
u.nonce = d.d.getNonce(0);
699+
u.executionData = abi.encode(calls);
700+
u.signature = _sig(k, u);
701+
702+
// This should fail because setSpendLimitsEnabled requires msg.sender == address(this)
703+
// and the key is not authorized to self-execute
704+
assertEq(
705+
oc.execute(abi.encode(u)),
706+
bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)"))
707+
);
708+
}
709+
710+
// Test that EOA can disable spend limits with event
711+
{
712+
ERC7821.Call[] memory calls = new ERC7821.Call[](1);
713+
calls[0].to = address(0);
714+
calls[0].data = abi.encodeWithSelector(
715+
GuardedExecutor.setSpendLimitsEnabled.selector, k.keyHash, false
716+
);
717+
718+
u.nonce = d.d.getNonce(0);
719+
u.executionData = abi.encode(calls);
720+
u.signature = _eoaSig(d.privateKey, u);
721+
722+
vm.expectEmit(true, true, true, true);
723+
emit GuardedExecutor.SpendLimitsEnabledSet(k.keyHash, false);
724+
725+
assertEq(oc.execute(abi.encode(u)), 0);
726+
assertFalse(d.d.spendLimitsEnabled(k.keyHash), "Spend limits should be disabled");
727+
}
728+
729+
// Test that when disabled, spend limits are not enforced (can spend beyond limit)
730+
{
731+
ERC7821.Call[] memory calls = new ERC7821.Call[](1);
732+
// Try to transfer 2 ether (exceeds the 1 ether daily limit)
733+
calls[0] = _transferCall2(token, address(0xb0b), 2 ether);
734+
735+
u.nonce = d.d.getNonce(0);
736+
u.executionData = abi.encode(calls);
737+
u.signature = _sig(k, u);
738+
739+
// Should succeed even though it exceeds the limit
740+
assertEq(oc.execute(abi.encode(u)), 0);
741+
assertEq(_balanceOf(token, address(0xb0b)), 2 ether);
742+
}
743+
744+
// Test that EOA can re-enable spend limits with event
745+
{
746+
ERC7821.Call[] memory calls = new ERC7821.Call[](1);
747+
calls[0].to = address(0);
748+
calls[0].data = abi.encodeWithSelector(
749+
GuardedExecutor.setSpendLimitsEnabled.selector, k.keyHash, true
750+
);
751+
752+
u.nonce = d.d.getNonce(0);
753+
u.executionData = abi.encode(calls);
754+
u.signature = _eoaSig(d.privateKey, u);
755+
756+
vm.expectEmit(true, true, true, true);
757+
emit GuardedExecutor.SpendLimitsEnabledSet(k.keyHash, true);
758+
759+
assertEq(oc.execute(abi.encode(u)), 0);
760+
assertTrue(d.d.spendLimitsEnabled(k.keyHash), "Spend limits should be enabled again");
761+
}
762+
763+
// Test that when re-enabled, spend limits are enforced again
764+
{
765+
ERC7821.Call[] memory calls = new ERC7821.Call[](1);
766+
// Try to transfer 2 ether again (exceeds the 1 ether daily limit)
767+
calls[0] = _transferCall2(token, address(0xb0b), 2 ether);
768+
769+
u.nonce = d.d.getNonce(0);
770+
u.executionData = abi.encode(calls);
771+
u.signature = _sig(k, u);
772+
773+
// Should fail now that limits are enforced
774+
assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("ExceededSpendLimit(address)")));
775+
}
776+
}
777+
657778
function testSpends(bytes32) public {
658779
Orchestrator.Intent memory u;
659780
DelegatedEOA memory d = _randomEIP7702DelegatedEOA();

0 commit comments

Comments
 (0)