Skip to content

Commit 9aa578d

Browse files
committed
Rename total balance change enforcers to multi operation balance enforcers
1 parent 9e7df65 commit 9aa578d

11 files changed

+195
-191
lines changed

documents/CaveatEnforcers.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,26 +83,26 @@ Balance Change Enforcers are ideal for:
8383
2. **No Aggregation**: Each enforcer operates independently and doesn't consider other enforcers in the chain
8484
3. **Delegation Chain Issues**: In complex delegation chains, the same balance change might satisfy multiple enforcers, potentially bypassing intended security measures
8585

86-
**When to Use Regular vs Total Balance Enforcers**:
86+
**When to Use Regular vs Multi Operation Balance Enforcers**:
8787
- Use **Regular Balance Enforcers** for simple, single-enforcer scenarios
88-
- Use **Total Balance Enforcers** when multiple enforcers might track the same recipient in a delegation chain
88+
- Use **Multi Operation Balance Enforcers** when multiple enforcers might track the same recipient in a delegation chain
8989

9090

91-
### Total Balance Change Enforcers
91+
### Multi Operation Balance Enforcers
9292

9393
This includes:
94-
- `ERC20TotalBalanceChangeEnforcer`
95-
- `ERC721TotalBalanceChangeEnforcer`
96-
- `ERC1155TotalBalanceChangeEnforcer`
97-
- `NativeTokenTotalBalanceChangeEnforcer`
94+
- `ERC20MultiOperationBalanceEnforcer`
95+
- `ERC721MultiOperationBalanceEnforcer`
96+
- `ERC1155MultiOperationBalanceEnforcer`
97+
- `NativeTokenMultiOperationBalanceEnforcer`
9898

9999
Use these when multiple total-balance constraints may apply to the same recipient and token within a single redemption, and you need a single, coherent end-of-redemption check.
100100

101101
#### Key differences from Regular Balance Change Enforcers
102102

103103
**Regular Balance Change Enforcers** (e.g., `NativeBalanceChangeEnforcer`) check deltas around one execution using `beforeHook`/`afterHook`. Because multiple enforcers watching the same recipient can be satisfied by the same balance movement, they are best for independent, per-delegation constraints.
104104

105-
**Total Balance Change Enforcers** are designed for coordinated multi-step flows and now behave as follows:
105+
**Multi Operation Balance Enforcers** are designed for coordinated multi-step flows and now behave as follows:
106106

107107
1. **Redemption-wide tracking**: Balance is tracked from the first `beforeAllHook` to the last `afterAllHook` for a given state key. The state key is defined by the recipient; for token-based variants it also includes the token address, and for ERC1155 it additionally includes the token ID. The state is scoped to the current `DelegationManager`. Any balance changes caused between those points (including by other enforcers, even those that modify state in `afterAllHook`, such as `NativeTokenPaymentEnforcer` even tho mixing with `NativeTokenPaymentEnforcer` is not recomended) are included in the final check.
108108
2. **Initialization rule**: The first enforcer that starts tracking must be created by the account whose balance is being constrained. In other words, for the first `beforeAllHook` on a state key, the delegator must equal the recipient. If this is not true, the enforcer reverts.
@@ -127,7 +127,7 @@ Use these when multiple total-balance constraints may apply to the same recipien
127127
- Alice → Bob: “Treasury can lose max 100 ETH”
128128
- Bob → Dave: “Treasury can lose max 50 ETH” (stricter)
129129
- With Regular enforcers, each delegation enforces its own limit, so the effective cap is 50 ETH.
130-
- **Coordinated multi-operation transactions (one complex flow with multiple steps)**: Use Total Balance Change Enforcers. Example: a swap + fee + settlement that together must result in a minimum net profit for a recipient, or must not exceed a net loss cap across the whole flow. The owner may aggregate multiple requirements; redelegations can only make them stricter.
130+
- **Coordinated multi-operation transactions (one complex flow with multiple steps)**: Use Multi Operation Balance Enforcers. Example: a swap + fee + settlement that together must result in a minimum net profit for a recipient, or must not exceed a net loss cap across the whole flow. The owner may aggregate multiple requirements; redelegations can only make them stricter.
131131

132132
Why accumulation is appropriate in coordinated flows: the intention is to verify the final end state of the recipient after all steps complete, not to enforce separate independent limits. In contrast, for independent limits, prefer Regular enforcers so each delegation remains self-contained.
133133

script/DeployCaveatEnforcers.s.sol

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ import { RedeemerEnforcer } from "../src/enforcers/RedeemerEnforcer.sol";
3838
import { SpecificActionERC20TransferBatchEnforcer } from "../src/enforcers/SpecificActionERC20TransferBatchEnforcer.sol";
3939
import { TimestampEnforcer } from "../src/enforcers/TimestampEnforcer.sol";
4040
import { ValueLteEnforcer } from "../src/enforcers/ValueLteEnforcer.sol";
41-
import { ERC20TotalBalanceChangeEnforcer } from "../src/enforcers/ERC20TotalBalanceChangeEnforcer.sol";
42-
import { ERC721TotalBalanceChangeEnforcer } from "../src/enforcers/ERC721TotalBalanceChangeEnforcer.sol";
43-
import { ERC1155TotalBalanceChangeEnforcer } from "../src/enforcers/ERC1155TotalBalanceChangeEnforcer.sol";
44-
import { NativeTokenTotalBalanceChangeEnforcer } from "../src/enforcers/NativeTokenTotalBalanceChangeEnforcer.sol";
41+
import { ERC20MultiOperationBalanceEnforcer } from "../src/enforcers/ERC20MultiOperationBalanceEnforcer.sol";
42+
import { ERC721MultiOperationBalanceEnforcer } from "../src/enforcers/ERC721MultiOperationBalanceEnforcer.sol";
43+
import { ERC1155MultiOperationBalanceEnforcer } from "../src/enforcers/ERC1155MultiOperationBalanceEnforcer.sol";
44+
import { NativeTokenMultiOperationBalanceEnforcer } from "../src/enforcers/NativeTokenMultiOperationBalanceEnforcer.sol";
4545

4646
/**
4747
* @title DeployCaveatEnforcers
@@ -170,17 +170,17 @@ contract DeployCaveatEnforcers is Script {
170170
deployedAddress = address(new ValueLteEnforcer{ salt: salt }());
171171
console2.log("ValueLteEnforcer: %s", deployedAddress);
172172

173-
deployedAddress = address(new ERC20TotalBalanceChangeEnforcer{ salt: salt }());
174-
console2.log("ERC20TotalBalanceChangeEnforcer: %s", deployedAddress);
173+
deployedAddress = address(new ERC20MultiOperationBalanceEnforcer{ salt: salt }());
174+
console2.log("ERC20MultiOperationBalanceEnforcer: %s", deployedAddress);
175175

176-
deployedAddress = address(new ERC721TotalBalanceChangeEnforcer{ salt: salt }());
177-
console2.log("ERC721TotalBalanceChangeEnforcer: %s", deployedAddress);
176+
deployedAddress = address(new ERC721MultiOperationBalanceEnforcer{ salt: salt }());
177+
console2.log("ERC721MultiOperationBalanceEnforcer: %s", deployedAddress);
178178

179-
deployedAddress = address(new ERC1155TotalBalanceChangeEnforcer{ salt: salt }());
180-
console2.log("ERC1155TotalBalanceChangeEnforcer: %s", deployedAddress);
179+
deployedAddress = address(new ERC1155MultiOperationBalanceEnforcer{ salt: salt }());
180+
console2.log("ERC1155MultiOperationBalanceEnforcer: %s", deployedAddress);
181181

182-
deployedAddress = address(new NativeTokenTotalBalanceChangeEnforcer{ salt: salt }());
183-
console2.log("NativeTokenTotalBalanceChangeEnforcer: %s", deployedAddress);
182+
deployedAddress = address(new NativeTokenMultiOperationBalanceEnforcer{ salt: salt }());
183+
console2.log("NativeTokenMultiOperationBalanceEnforcer: %s", deployedAddress);
184184

185185
vm.stopBroadcast();
186186
}

script/verification/verify-enforcer-contracts.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ ENFORCERS=(
5353
"SpecificActionERC20TransferBatchEnforcer"
5454
"TimestampEnforcer"
5555
"ValueLteEnforcer"
56-
"ERC20TotalBalanceChangeEnforcer"
57-
"ERC721TotalBalanceChangeEnforcer"
58-
"ERC1155TotalBalanceChangeEnforcer"
59-
"NativeTokenTotalBalanceChangeEnforcer"
56+
"ERC20MultiOperationBalanceEnforcer"
57+
"ERC721MultiOperationBalanceEnforcer"
58+
"ERC1155MultiOperationBalanceEnforcer"
59+
"NativeTokenMultiOperationBalanceEnforcer"
6060
)
6161

6262
ADDRESSES=(

src/enforcers/ERC1155TotalBalanceChangeEnforcer.sol renamed to src/enforcers/ERC1155MultiOperationBalanceEnforcer.sol

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ import { CaveatEnforcer } from "./CaveatEnforcer.sol";
77
import { ModeCode } from "../utils/Types.sol";
88

99
/**
10-
* @title ERC1155TotalBalanceChangeEnforcer
10+
* @title ERC1155MultiOperationBalanceEnforcer
1111
* @notice Enforces that a recipient's ERC1155 token balance changes within expected limits across multiple delegations.
1212
* Tracks balance changes from the first beforeAllHook call to the last afterAllHook call within a redemption.
13-
*
13+
*
1414
* For balance increases: ensures the final balance is at least the initial balance plus the expected increase.
1515
* For balance decreases: ensures the final balance is at least the initial balance minus the expected decrease.
16-
*
17-
* @dev This enforcer operates in delegation chains where multiple delegations may affect the same recipient/token/tokenID combination.
18-
* State is shared between enforcers watching the same recipient/token/tokenID combination and is cleared after transaction execution.
19-
*
16+
*
17+
* @dev This enforcer operates in delegation chains where multiple delegations may affect the same recipient/token/tokenID
18+
* combination.
19+
* State is shared between enforcers watching the same recipient/token/tokenID combination and is cleared after transaction
20+
* execution.
21+
*
2022
* @dev Only operates in default execution mode (ModeCode 0).
21-
*
23+
*
2224
* @dev Security considerations:
2325
* - State is shared between enforcers watching the same recipient/token/tokenID combination
2426
* - Balance changes are tracked by comparing first beforeAll/last afterAll balances in batch delegations
@@ -28,7 +30,7 @@ import { ModeCode } from "../utils/Types.sol";
2830
* - Delegator must equal recipient for first delegation in a chain of delegations
2931
* - Only if delegator is equal to recipient do the amounts aggregate
3032
*/
31-
contract ERC1155TotalBalanceChangeEnforcer is CaveatEnforcer {
33+
contract ERC1155MultiOperationBalanceEnforcer is CaveatEnforcer {
3234
////////////////////////////// Events //////////////////////////////
3335

3436
event TrackedBalance(
@@ -107,13 +109,13 @@ contract ERC1155TotalBalanceChangeEnforcer is CaveatEnforcer {
107109
onlyDefaultExecutionMode(_mode)
108110
{
109111
TermsData memory terms_ = getTermsInfo(_terms);
110-
require(terms_.amount > 0, "ERC1155TotalBalanceChangeEnforcer:zero-expected-change-amount");
112+
require(terms_.amount > 0, "ERC1155MultiOperationBalanceEnforcer:zero-expected-change-amount");
111113
bytes32 hashKey_ = _getHashKey(msg.sender, terms_.token, terms_.recipient, terms_.tokenId);
112114
uint256 balance_ = IERC1155(terms_.token).balanceOf(terms_.recipient, terms_.tokenId);
113115
BalanceTracker memory balanceTracker_ = balanceTracker[hashKey_];
114116

115117
if (balanceTracker_.expectedIncrease == 0 && balanceTracker_.expectedDecrease == 0) {
116-
require(terms_.recipient == _delegator, "ERC1155TotalBalanceChangeEnforcer:invalid-delegator");
118+
require(terms_.recipient == _delegator, "ERC1155MultiOperationBalanceEnforcer:invalid-delegator");
117119
balanceTracker_.balanceBefore = balance_;
118120
emit TrackedBalance(msg.sender, terms_.recipient, terms_.token, terms_.tokenId, balance_);
119121
}
@@ -131,15 +133,15 @@ contract ERC1155TotalBalanceChangeEnforcer is CaveatEnforcer {
131133
// For decreases: new amount must be <= existing amount (more restrictive)
132134
require(
133135
terms_.amount <= balanceTracker_.expectedDecrease,
134-
"ERC1155TotalBalanceChangeEnforcer:decrease-must-be-more-restrictive"
136+
"ERC1155MultiOperationBalanceEnforcer:decrease-must-be-more-restrictive"
135137
);
136138
// Override instead of aggregate
137139
balanceTracker_.expectedDecrease = terms_.amount;
138140
} else {
139141
// For increases: new amount must be >= existing amount (more restrictive)
140142
require(
141143
terms_.amount >= balanceTracker_.expectedIncrease,
142-
"ERC1155TotalBalanceChangeEnforcer:increase-must-be-more-restrictive"
144+
"ERC1155MultiOperationBalanceEnforcer:increase-must-be-more-restrictive"
143145
);
144146
// Override instead of aggregate
145147
balanceTracker_.expectedIncrease = terms_.amount;
@@ -193,12 +195,13 @@ contract ERC1155TotalBalanceChangeEnforcer is CaveatEnforcer {
193195
expected_ = balanceTracker_.expectedIncrease - balanceTracker_.expectedDecrease;
194196
require(
195197
balance_ >= balanceTracker_.balanceBefore + expected_,
196-
"ERC1155TotalBalanceChangeEnforcer:insufficient-balance-increase"
198+
"ERC1155MultiOperationBalanceEnforcer:insufficient-balance-increase"
197199
);
198200
} else {
199201
expected_ = balanceTracker_.expectedDecrease - balanceTracker_.expectedIncrease;
200202
require(
201-
balance_ >= balanceTracker_.balanceBefore - expected_, "ERC1155TotalBalanceChangeEnforcer:exceeded-balance-decrease"
203+
balance_ >= balanceTracker_.balanceBefore - expected_,
204+
"ERC1155MultiOperationBalanceEnforcer:exceeded-balance-decrease"
202205
);
203206
}
204207

@@ -219,7 +222,7 @@ contract ERC1155TotalBalanceChangeEnforcer is CaveatEnforcer {
219222
* enforceDecrease)
220223
*/
221224
function getTermsInfo(bytes calldata _terms) public pure returns (TermsData memory) {
222-
require(_terms.length == 105, "ERC1155TotalBalanceChangeEnforcer:invalid-terms-length");
225+
require(_terms.length == 105, "ERC1155MultiOperationBalanceEnforcer:invalid-terms-length");
223226
return TermsData({
224227
enforceDecrease: _terms[0] != 0,
225228
token: address(bytes20(_terms[1:21])),

src/enforcers/ERC20TotalBalanceChangeEnforcer.sol renamed to src/enforcers/ERC20MultiOperationBalanceEnforcer.sol

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@ import { CaveatEnforcer } from "./CaveatEnforcer.sol";
66
import { ModeCode } from "../utils/Types.sol";
77

88
/**
9-
* @title ERC20TotalBalanceChangeEnforcer
9+
* @title ERC20MultiOperationBalanceEnforcer
1010
* @notice Enforces that a recipient's token balance changes within expected limits across multiple delegations.
1111
* Tracks balance changes from the first beforeAllHook call to the last afterAllHook call within a redemption.
12-
*
12+
*
1313
* For balance increases: ensures the final balance is at least the initial balance plus the expected increase.
1414
* For balance decreases: ensures the final balance is at least the initial balance minus the expected decrease.
15-
*
15+
*
1616
* @dev This enforcer operates in delegation chains where multiple delegations may affect the same recipient/token pair.
1717
* State is shared between enforcers watching the same recipient/token pair and is cleared after transaction execution.
18-
*
18+
*
1919
* @dev Only operates in default execution mode (ModeCode 0).
20-
*
20+
*
2121
* @dev Security considerations:
2222
* - State is shared between enforcers watching the same recipient/token pair
2323
* - Balance changes are tracked by comparing first beforeAll/last afterAll balances in batch delegations
@@ -27,7 +27,7 @@ import { ModeCode } from "../utils/Types.sol";
2727
* - Delegator must equal recipient for first delegation in a chain of delegations
2828
* - Only if delegator is equal to recipient do the amount aggragate
2929
*/
30-
contract ERC20TotalBalanceChangeEnforcer is CaveatEnforcer {
30+
contract ERC20MultiOperationBalanceEnforcer is CaveatEnforcer {
3131
////////////////////////////// Events //////////////////////////////
3232

3333
event TrackedBalance(address indexed delegationManager, address indexed recipient, address indexed token, uint256 balance);
@@ -88,14 +88,14 @@ contract ERC20TotalBalanceChangeEnforcer is CaveatEnforcer {
8888
onlyDefaultExecutionMode(_mode)
8989
{
9090
(bool enforceDecrease_, address token_, address recipient_, uint256 amount_) = getTermsInfo(_terms);
91-
require(amount_ > 0, "ERC20TotalBalanceChangeEnforcer:zero-expected-change-amount");
91+
require(amount_ > 0, "ERC20MultiOperationBalanceEnforcer:zero-expected-change-amount");
9292

9393
bytes32 hashKey_ = _getHashKey(msg.sender, token_, recipient_);
9494
BalanceTracker memory balanceTracker_ = balanceTracker[hashKey_];
9595

9696
uint256 currentBalance_ = IERC20(token_).balanceOf(recipient_);
9797
if (balanceTracker_.expectedDecrease == 0 && balanceTracker_.expectedIncrease == 0) {
98-
require(_delegator == recipient_, "ERC20TotalBalanceChangeEnforcer:invalid-delegator");
98+
require(_delegator == recipient_, "ERC20MultiOperationBalanceEnforcer:invalid-delegator");
9999
balanceTracker_.balanceBefore = currentBalance_;
100100
emit TrackedBalance(msg.sender, recipient_, token_, currentBalance_);
101101
}
@@ -114,15 +114,15 @@ contract ERC20TotalBalanceChangeEnforcer is CaveatEnforcer {
114114
// For decreases: new amount must be <= existing amount (more restrictive)
115115
require(
116116
amount_ <= balanceTracker_.expectedDecrease,
117-
"ERC20TotalBalanceChangeEnforcer:decrease-must-be-more-restrictive"
117+
"ERC20MultiOperationBalanceEnforcer:decrease-must-be-more-restrictive"
118118
);
119119
// Override instead of aggregate
120120
balanceTracker_.expectedDecrease = amount_;
121121
} else {
122122
// For increases: new amount must be >= existing amount (more restrictive)
123123
require(
124124
amount_ >= balanceTracker_.expectedIncrease,
125-
"ERC20TotalBalanceChangeEnforcer:increase-must-be-more-restrictive"
125+
"ERC20MultiOperationBalanceEnforcer:increase-must-be-more-restrictive"
126126
);
127127
// Override instead of aggregate
128128
balanceTracker_.expectedIncrease = amount_;
@@ -174,13 +174,13 @@ contract ERC20TotalBalanceChangeEnforcer is CaveatEnforcer {
174174
expected_ = balanceTracker_.expectedIncrease - balanceTracker_.expectedDecrease;
175175
require(
176176
currentBalance_ >= balanceTracker_.balanceBefore + expected_,
177-
"ERC20TotalBalanceChangeEnforcer:insufficient-balance-increase"
177+
"ERC20MultiOperationBalanceEnforcer:insufficient-balance-increase"
178178
);
179179
} else {
180180
expected_ = balanceTracker_.expectedDecrease - balanceTracker_.expectedIncrease;
181181
require(
182182
currentBalance_ >= balanceTracker_.balanceBefore - expected_,
183-
"ERC20TotalBalanceChangeEnforcer:exceeded-balance-decrease"
183+
"ERC20MultiOperationBalanceEnforcer:exceeded-balance-decrease"
184184
);
185185
}
186186

@@ -202,7 +202,7 @@ contract ERC20TotalBalanceChangeEnforcer is CaveatEnforcer {
202202
pure
203203
returns (bool enforceDecrease_, address token_, address recipient_, uint256 amount_)
204204
{
205-
require(_terms.length == 73, "ERC20TotalBalanceChangeEnforcer:invalid-terms-length");
205+
require(_terms.length == 73, "ERC20MultiOperationBalanceEnforcer:invalid-terms-length");
206206
enforceDecrease_ = _terms[0] != 0;
207207
token_ = address(bytes20(_terms[1:21]));
208208
recipient_ = address(bytes20(_terms[21:41]));

0 commit comments

Comments
 (0)