Skip to content

Commit 71d1b05

Browse files
authored
feat: group subscriptions (#16)
Single entry point via: `redeem(bytes32 id, bytes calldata data)` Three redemption types: ### Groups Internal call: `_redeemGroup(bytes32 id)` External calls via multisend - `ERC1155.safeTransferFrom` (transfer subscribers tokens to the module) - `IHubV2.groupMint` (mint group tokens using subscriber CRC to the module) - `ERC1155.safeTransferFrom` (transfer group tokens to the subscriber) Redeeming occurs via this route if the recipient address is a group address. Group minting locks tokens from the `msg.sender` and mints group tokens to the `msg.sender`, so fairly sure three calls are required in a pull-based setup. ### Personal Trusted - Via `operateFlowMatrix` ### Personal Untrusted - Via `safeTransferFrom` ## Refactor Notes - Replaced use of `periods` with transient storage (`T_REDEEMABLE_AMOUNT`), set once in `redeem` using `LibTransient` - `_redeemGroup`, `_redeemTrusted`, and `_redeemUntrusted` now consume the redeemable amount from transient storage. - Explicit `clear` calls clean up the slot after redemption
1 parent 8e23ac2 commit 71d1b05

File tree

17 files changed

+517
-213
lines changed

17 files changed

+517
-213
lines changed

foundry.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
[profile.default]
44
auto_detect_solc = false
55
block_timestamp = 1_738_368_000 # Feb 1, 2025 at 00:00 GMT
6+
evm_version = "cancun"
67
fuzz = { runs = 1_000 }
78
gas_reports = ["*"]
89
optimizer = true

src/SubscriptionModule.sol

Lines changed: 169 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
pragma solidity ^0.8.28;
33

44
import { ISafe } from "src/interfaces/ISafe.sol";
5+
import { IHubV2 } from "src/interfaces/IHub.sol";
6+
import { IMultiSend } from "src/interfaces/IMultiSend.sol";
57
import { CirclesLib } from "src/libs/CirclesLib.sol";
68
import { Errors } from "src/libs/Errors.sol";
7-
import { Subscription } from "src/libs/Types.sol";
9+
import { Subscription, Category } from "src/libs/Types.sol";
810
import { SubscriptionLib } from "src/libs/SubscriptionLib.sol";
911

1012
import { ERC1155 } from "@circles/src/circles/ERC1155.sol";
11-
import { IHubV2 } from "@circles/src/hub/IHub.sol";
1213
import { TypeDefinitions } from "@circles/src/hub/TypeDefinitions.sol";
1314
import { Enum } from "@safe-smart-account/contracts/common/Enum.sol";
1415
import { EnumerableSetLib } from "@solady/src/utils/EnumerableSetLib.sol";
16+
import { LibTransient } from "@solady/src/utils/LibTransient.sol";
1517

1618
contract SubscriptionModule {
1719
/*//////////////////////////////////////////////////////////////
@@ -26,6 +28,8 @@ contract SubscriptionModule {
2628

2729
using CirclesLib for TypeDefinitions.Stream[];
2830

31+
using LibTransient for LibTransient.TUint256;
32+
2933
/*//////////////////////////////////////////////////////////////
3034
STATE VARIABLES
3135
//////////////////////////////////////////////////////////////*/
@@ -35,11 +39,13 @@ contract SubscriptionModule {
3539

3640
address public constant HUB = 0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8;
3741

38-
mapping(bytes32 id => address safe) public safeFromId;
42+
address public constant MULTISEND = 0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526;
43+
44+
bytes32 internal constant T_REDEEMABLE_AMOUNT = 0x70bfbb43a5ce660914e09d1b48fcc488982d5981137b973eac35b0592a414e90;
3945

40-
mapping(address safe => mapping(bytes32 id => Subscription subscription)) internal _subscriptions;
46+
mapping(bytes32 id => Subscription subscription) internal _subscriptions;
4147

42-
mapping(address safe => EnumerableSetLib.Bytes32Set) internal ids;
48+
mapping(address subscriber => EnumerableSetLib.Bytes32Set) internal ids;
4349

4450
/*//////////////////////////////////////////////////////////////
4551
EVENTS
@@ -50,18 +56,11 @@ contract SubscriptionModule {
5056
address indexed subscriber,
5157
address indexed recipient,
5258
uint256 amount,
53-
uint256 lastRedeemed,
54-
uint256 frequency,
55-
bool requireTrusted
59+
uint256 nextRedeemAt,
60+
Category category
5661
);
5762

58-
event Redeemed(
59-
bytes32 indexed id,
60-
address indexed subscriber,
61-
address indexed recipient,
62-
uint256 lastRedeemed,
63-
bool requireTrusted
64-
);
63+
event Redeemed(bytes32 indexed id, address indexed subscriber, address indexed recipient, uint256 nextRedeemAt);
6564

6665
event RecipientUpdated(bytes32 indexed id, address indexed oldRecipient, address indexed newRecipient);
6766

@@ -75,7 +74,7 @@ contract SubscriptionModule {
7574
address recipient,
7675
uint256 amount,
7776
uint256 frequency,
78-
bool requireTrusted
77+
Category category
7978
)
8079
external
8180
returns (bytes32 id)
@@ -87,75 +86,40 @@ contract SubscriptionModule {
8786
amount: amount,
8887
lastRedeemed: block.timestamp - frequency,
8988
frequency: frequency,
90-
requireTrusted: requireTrusted
89+
category: category
9190
});
9291
id = sub.compute();
93-
_subscribe(msg.sender, id, sub);
94-
emit SubscriptionCreated(
95-
id, msg.sender, recipient, amount, block.timestamp - frequency, frequency, requireTrusted
96-
);
97-
}
98-
99-
function redeem(
100-
bytes32 id,
101-
address[] calldata flowVertices,
102-
TypeDefinitions.FlowEdge[] calldata flow,
103-
TypeDefinitions.Stream[] calldata streams,
104-
bytes calldata packedCoordinates,
105-
uint256 sourceCoordinate
106-
)
107-
external
108-
{
109-
(address safe, Subscription memory sub) = _loadSubscription(id);
110-
111-
uint256 periods = _requireRedeemablePeriods(sub);
112-
113-
require(flowVertices[sourceCoordinate] == sub.subscriber, Errors.InvalidSubscriber());
114-
115-
require(streams.checkSource(sourceCoordinate), Errors.InvalidStreamSource());
116-
117-
require(streams.checkRecipients(sub.recipient, flowVertices, packedCoordinates), Errors.InvalidRecipient());
118-
119-
require(flow.extractAmount() == periods * sub.amount, Errors.InvalidAmount());
120-
121-
_applyRedemption(safe, id, sub, periods);
122-
123-
require(
124-
ISafe(safe).execTransactionFromModule(
125-
HUB,
126-
0,
127-
abi.encodeCall(IHubV2.operateFlowMatrix, (flowVertices, flow, streams, packedCoordinates)),
128-
Enum.Operation.Call
129-
),
130-
Errors.ExecutionFailed()
131-
);
132-
133-
emit Redeemed(id, safe, sub.recipient, sub.lastRedeemed, sub.requireTrusted);
92+
_subscribe(id, sub);
93+
emit SubscriptionCreated(id, msg.sender, recipient, amount, block.timestamp, category);
13494
}
13595

136-
function redeemUntrusted(bytes32 id) external {
137-
(address safe, Subscription memory sub) = _loadSubscription(id);
138-
139-
require(!sub.requireTrusted, Errors.TrustedPathOnly());
96+
function redeem(bytes32 id, bytes calldata data) external {
97+
Subscription memory sub = _subscriptions[id];
98+
require(sub.subscriber != address(0), Errors.IdentifierNonexistent());
14099

141-
uint256 periods = _requireRedeemablePeriods(sub);
142-
143-
_applyRedemption(safe, id, sub, periods);
144-
145-
require(
146-
ISafe(safe).execTransactionFromModule(
147-
HUB,
148-
0,
149-
abi.encodeCall(
150-
ERC1155.safeTransferFrom,
151-
(sub.subscriber, sub.recipient, uint256(uint160(sub.subscriber)), periods * sub.amount, "")
152-
),
153-
Enum.Operation.Call
154-
),
155-
Errors.ExecutionFailed()
156-
);
100+
uint256 periods = (block.timestamp - sub.lastRedeemed) / sub.frequency;
101+
require(periods >= 1, Errors.NotRedeemable());
157102

158-
emit Redeemed(id, safe, sub.recipient, sub.lastRedeemed, sub.requireTrusted);
103+
LibTransient.tUint256(T_REDEEMABLE_AMOUNT).set(periods * sub.amount);
104+
sub.lastRedeemed += periods * sub.frequency;
105+
_subscriptions[id] = sub;
106+
107+
if (sub.category == Category.group) {
108+
_redeemGroup(id, sub);
109+
} else if (sub.category == Category.trusted) {
110+
(
111+
address[] memory flowVertices,
112+
TypeDefinitions.FlowEdge[] memory flow,
113+
TypeDefinitions.Stream[] memory streams,
114+
bytes memory packedCoordinates,
115+
uint256 sourceCoordinate
116+
) = abi.decode(data, (address[], TypeDefinitions.FlowEdge[], TypeDefinitions.Stream[], bytes, uint256));
117+
_redeemTrusted(id, sub, flowVertices, flow, streams, packedCoordinates, sourceCoordinate);
118+
} else if (sub.category == Category.untrusted) {
119+
_redeemUntrusted(id, sub);
120+
} else {
121+
revert Errors.InvalidCategory();
122+
}
159123
}
160124

161125
function unsubscribe(bytes32 id) external {
@@ -169,8 +133,7 @@ contract SubscriptionModule {
169133
}
170134

171135
function updateRecipient(bytes32 id, address newRecipient) external {
172-
address safe = safeFromId[id];
173-
Subscription storage sub = _subscriptions[safe][id];
136+
Subscription storage sub = _subscriptions[id];
174137
require(sub.recipient == msg.sender, Errors.OnlyRecipient());
175138
sub.recipient = newRecipient;
176139
emit RecipientUpdated(id, msg.sender, newRecipient);
@@ -181,62 +144,152 @@ contract SubscriptionModule {
181144
//////////////////////////////////////////////////////////////*/
182145

183146
function getSubscription(bytes32 id) external view returns (Subscription memory) {
184-
return _subscriptions[safeFromId[id]][id];
147+
return _subscriptions[id];
185148
}
186149

187-
function getSubscriptionIds(address safe) external view returns (bytes32[] memory) {
188-
return ids[safe].values();
150+
function getSubscriptionIds(address subscriber) external view returns (bytes32[] memory) {
151+
return ids[subscriber].values();
189152
}
190153

191154
function isValidOrRedeemable(bytes32 id) public view returns (uint256) {
192-
Subscription memory subscription = _subscriptions[safeFromId[id]][id];
193-
return (block.timestamp - subscription.lastRedeemed) / subscription.frequency * subscription.amount;
194-
}
195-
196-
function isTrustedRequired(bytes32 id) external view returns (bool) {
197-
return _subscriptions[safeFromId[id]][id].requireTrusted;
155+
if (_subscriptions[id].subscriber == address(0)) return 0;
156+
Subscription memory sub = _subscriptions[id];
157+
return (block.timestamp - sub.lastRedeemed) / sub.frequency * sub.amount;
198158
}
199159

200160
/*//////////////////////////////////////////////////////////////
201161
INTERNAL NON-CONSTANT FUNCTIONS
202162
//////////////////////////////////////////////////////////////*/
203163

204-
function _subscribe(address subscriber, bytes32 id, Subscription memory subscription) internal {
205-
require(!_exists(id), Errors.IdentifierExists());
206-
_subscriptions[subscriber][id] = subscription;
207-
safeFromId[id] = subscriber;
208-
ids[subscriber].add(id);
164+
function _subscribe(bytes32 id, Subscription memory sub) internal {
165+
require(_subscriptions[id].subscriber == address(0), Errors.IdentifierExists());
166+
_subscriptions[id] = sub;
167+
ids[sub.subscriber].add(id);
209168
}
210169

211-
function _unsubscribe(address subscriber, bytes32 id) internal {
212-
require(_exists(id), Errors.IdentifierNonexistent());
213-
delete _subscriptions[subscriber][id];
214-
delete safeFromId[id];
215-
ids[subscriber].remove(id);
216-
emit Unsubscribed(id, subscriber);
170+
function _unsubscribe(address caller, bytes32 id) internal {
171+
Subscription memory sub = _subscriptions[id];
172+
require(sub.subscriber == caller, Errors.OnlySubscriber());
173+
delete _subscriptions[id];
174+
ids[sub.subscriber].remove(id);
175+
emit Unsubscribed(id, sub.subscriber);
217176
}
218177

219-
/*//////////////////////////////////////////////////////////////
220-
INTERNAL CONSTANT FUNCTIONS
221-
//////////////////////////////////////////////////////////////*/
178+
function _redeemGroup(bytes32 id, Subscription memory sub) internal {
179+
address[] memory collateralAvatars = new address[](1);
180+
collateralAvatars[0] = sub.subscriber;
181+
182+
uint256[] memory amounts = new uint256[](1);
183+
amounts[0] = sub.amount;
184+
185+
/**
186+
* Steps required to handle group minting for the subscriber
187+
* - pull subscriber CRC tokens
188+
* - mint group (= recipient) tokens to this module using subscriber CRC collateral
189+
* - empty data for now, @todo check group mint policies for requirements
190+
* - transfer minted group tokens to the subscriber
191+
*/
192+
bytes memory call0 = abi.encodeCall(
193+
ERC1155.safeTransferFrom,
194+
(
195+
sub.subscriber,
196+
address(this),
197+
_toTokenId(sub.subscriber),
198+
LibTransient.tUint256(T_REDEEMABLE_AMOUNT).get(),
199+
""
200+
)
201+
);
202+
/// @todo empty data for now -- checkout mint policies of existing groups for data requirements
203+
bytes memory call1 = abi.encodeCall(IHubV2.groupMint, (sub.recipient, collateralAvatars, amounts, ""));
204+
bytes memory call2 = abi.encodeCall(
205+
ERC1155.safeTransferFrom,
206+
(
207+
address(this),
208+
sub.subscriber,
209+
_toTokenId(sub.recipient),
210+
ERC1155(HUB).balanceOf(address(this), _toTokenId(sub.recipient)),
211+
""
212+
)
213+
);
214+
bytes memory transactions = bytes.concat(
215+
abi.encodePacked(uint8(0), HUB, uint256(0), call0.length, call0),
216+
abi.encodePacked(uint8(0), HUB, uint256(0), call1.length, call1),
217+
abi.encodePacked(uint8(0), HUB, uint256(0), call2.length, call2)
218+
);
219+
220+
require(
221+
ISafe(sub.subscriber).execTransactionFromModule(
222+
MULTISEND, 0, abi.encodeCall(IMultiSend.multiSend, (transactions)), Enum.Operation.DelegateCall
223+
),
224+
Errors.ExecutionFailed()
225+
);
222226

223-
function _exists(bytes32 id) internal view returns (bool) {
224-
return safeFromId[id] != address(0);
227+
emit Redeemed(id, sub.subscriber, sub.recipient, sub.lastRedeemed + sub.frequency);
228+
LibTransient.tUint256(T_REDEEMABLE_AMOUNT).clear();
225229
}
226230

227-
function _loadSubscription(bytes32 id) internal view returns (address safe, Subscription memory sub) {
228-
require(_exists(id), Errors.IdentifierNonexistent());
229-
safe = safeFromId[id];
230-
sub = _subscriptions[safe][id];
231+
function _redeemTrusted(
232+
bytes32 id,
233+
Subscription memory sub,
234+
address[] memory flowVertices,
235+
TypeDefinitions.FlowEdge[] memory flow,
236+
TypeDefinitions.Stream[] memory streams,
237+
bytes memory packedCoordinates,
238+
uint256 sourceCoordinate
239+
)
240+
internal
241+
{
242+
require(flowVertices[sourceCoordinate] == sub.subscriber, Errors.InvalidSubscriber());
243+
244+
require(streams.checkSource(sourceCoordinate), Errors.InvalidStreamSource());
245+
246+
require(streams.checkRecipients(sub.recipient, flowVertices, packedCoordinates), Errors.InvalidRecipient());
247+
248+
require(flow.extractAmount() == LibTransient.tUint256(T_REDEEMABLE_AMOUNT).get(), Errors.InvalidAmount());
249+
250+
require(
251+
ISafe(sub.subscriber).execTransactionFromModule(
252+
HUB,
253+
0,
254+
abi.encodeCall(IHubV2.operateFlowMatrix, (flowVertices, flow, streams, packedCoordinates)),
255+
Enum.Operation.Call
256+
),
257+
Errors.ExecutionFailed()
258+
);
259+
260+
emit Redeemed(id, sub.subscriber, sub.recipient, sub.lastRedeemed + sub.frequency);
261+
LibTransient.tUint256(T_REDEEMABLE_AMOUNT).clear();
231262
}
232263

233-
function _requireRedeemablePeriods(Subscription memory sub) internal view returns (uint256 periods) {
234-
periods = (block.timestamp - sub.lastRedeemed) / sub.frequency;
235-
require(periods >= 1, Errors.NotRedeemable());
264+
function _redeemUntrusted(bytes32 id, Subscription memory sub) internal {
265+
require(
266+
ISafe(sub.subscriber).execTransactionFromModule(
267+
HUB,
268+
0,
269+
abi.encodeCall(
270+
ERC1155.safeTransferFrom,
271+
(
272+
sub.subscriber,
273+
sub.recipient,
274+
_toTokenId(sub.subscriber),
275+
LibTransient.tUint256(T_REDEEMABLE_AMOUNT).get(),
276+
""
277+
)
278+
),
279+
Enum.Operation.Call
280+
),
281+
Errors.ExecutionFailed()
282+
);
283+
284+
emit Redeemed(id, sub.subscriber, sub.recipient, sub.lastRedeemed + sub.frequency);
285+
LibTransient.tUint256(T_REDEEMABLE_AMOUNT).clear();
236286
}
237287

238-
function _applyRedemption(address safe, bytes32 id, Subscription memory sub, uint256 periods) internal {
239-
sub.lastRedeemed += periods * sub.frequency;
240-
_subscriptions[safe][id] = sub;
288+
/*//////////////////////////////////////////////////////////////
289+
INTERNAL CONSTANT FUNCTIONS
290+
//////////////////////////////////////////////////////////////*/
291+
292+
function _toTokenId(address _avatar) internal pure returns (uint256) {
293+
return uint256(uint160(_avatar));
241294
}
242295
}

0 commit comments

Comments
 (0)