Skip to content

Commit baa43c9

Browse files
committed
wip
time to test out boost creation and allocation
1 parent 5b1bd82 commit baa43c9

File tree

3 files changed

+363
-1
lines changed

3 files changed

+363
-1
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity ^0.8.24;
3+
4+
import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
5+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
6+
7+
import {ABudget} from "contracts/budgets/ABudget.sol";
8+
import {ACloneable} from "contracts/shared/ACloneable.sol";
9+
10+
/// @title Abstract Simple ABudget
11+
/// @notice A minimal budget implementation that simply holds and distributes tokens (ERC20-like and native)
12+
/// @dev This type of budget supports ETH, ERC20, and ERC1155 assets only
13+
abstract contract ATransparentBudget is ABudget, IERC1155Receiver {
14+
/// @inheritdoc ACloneable
15+
function supportsInterface(bytes4 interfaceId) public view virtual override(ABudget, IERC165) returns (bool) {
16+
return interfaceId == type(ATransparentBudget).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId
17+
|| interfaceId == type(IERC165).interfaceId || ABudget.supportsInterface(interfaceId);
18+
}
19+
20+
/// @inheritdoc ACloneable
21+
function getComponentInterface() public pure virtual override returns (bytes4) {
22+
return type(ATransparentBudget).interfaceId;
23+
}
24+
}
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity ^0.8.24;
3+
4+
import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
5+
import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
6+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
7+
import {DynamicArrayLib} from "@solady/utils/DynamicArrayLib.sol";
8+
import {LibTransient} from "@solady/utils/LibTransient.sol";
9+
10+
import {Ownable} from "@solady/auth/Ownable.sol";
11+
import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol";
12+
import {ReentrancyGuard} from "@solady/utils/ReentrancyGuard.sol";
13+
14+
import {BoostCore} from "contracts/BoostCore.sol";
15+
import {BoostError} from "contracts/shared/BoostError.sol";
16+
import {ABudget} from "contracts/budgets/ABudget.sol";
17+
import {ACloneable} from "contracts/shared/ACloneable.sol";
18+
import {ATransparentBudget} from "contracts/budgets/ATransparentBudget.sol";
19+
20+
/*
21+
TODO
22+
1. test deposit and boost creation logic
23+
2. implement clawback logic and tracking on deposits
24+
3. implement clawback auth
25+
4. implement permit2 support
26+
*/
27+
28+
/// @title Simple ABudget
29+
/// @notice A minimal budget implementation that simply holds and distributes tokens (ERC20-like and native)
30+
/// @dev This type of budget supports ETH, ERC20, and ERC1155 assets only
31+
contract TransparentBudget is ATransparentBudget, ReentrancyGuard {
32+
using SafeTransferLib for address;
33+
using DynamicArrayLib for *;
34+
using LibTransient for *;
35+
36+
/// @dev The total amount of each fungible asset distributed from the budget
37+
mapping(address => uint256) private _distributedFungible;
38+
39+
/// @dev The total amount of each ERC1155 asset and token ID distributed from the budget
40+
mapping(address => mapping(uint256 => uint256)) private _distributedERC1155;
41+
42+
/// @inheritdoc ABudget
43+
/// @notice This is unused. Call `createBoost` with a deposit payload
44+
function allocate(bytes calldata) external payable virtual override returns (bool) {
45+
revert BoostError.NotImplemented();
46+
}
47+
48+
function createBoost(bytes[] calldata _allocations, BoostCore core, bytes calldata _boostPayload)
49+
external
50+
payable
51+
nonReentrant
52+
{
53+
DynamicArrayLib.DynamicArray memory allocationKeys;
54+
allocationKeys.reserve(_allocations.length);
55+
56+
for (uint256 i = 0; i < _allocations.length; i++) {
57+
bytes32 key = _allocate(_allocations[i]);
58+
allocationKeys.set(i, key);
59+
}
60+
61+
core.createBoost(_boostPayload);
62+
63+
bytes32[] memory keys = allocationKeys.asBytes32Array();
64+
for (uint256 i = 0; i < keys.length; i++) {
65+
LibTransient.TUint256 storage p = LibTransient.tUint256(keys[i]);
66+
if (p.get() != 0) revert BoostError.Unauthorized();
67+
}
68+
}
69+
70+
/// @notice Allocates assets to be distributed in the boost
71+
/// @param data_ The packed data for the {Transfer} request
72+
/// @return key The key of the amount allocated
73+
/// @dev The caller must have already approved the contract to transfer the asset
74+
/// @dev If the asset transfer fails, the allocation will revert
75+
function _allocate(bytes calldata data_) internal virtual returns (bytes32 key) {
76+
Transfer memory request = abi.decode(data_, (Transfer));
77+
if (request.assetType == AssetType.ETH) {
78+
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
79+
80+
// Ensure the value received is equal to the `payload.amount`
81+
if (msg.value != payload.amount) {
82+
revert InvalidAllocation(request.asset, payload.amount);
83+
}
84+
(LibTransient.TUint256 storage p, bytes32 tKey) = getFungibleAmountAndKey(address(0));
85+
p.inc(payload.amount);
86+
key = tKey;
87+
} else if (request.assetType == AssetType.ERC20) {
88+
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
89+
90+
// Transfer `payload.amount` of the token to this contract
91+
request.asset.safeTransferFrom(request.target, address(this), payload.amount);
92+
if (request.asset.balanceOf(address(this)) < payload.amount) {
93+
revert InvalidAllocation(request.asset, payload.amount);
94+
}
95+
key = bytes32(uint256(uint160(request.asset)));
96+
(LibTransient.TUint256 storage p, bytes32 tKey) = getFungibleAmountAndKey(request.asset);
97+
p.inc(payload.amount);
98+
key = tKey;
99+
} else if (request.assetType == AssetType.ERC1155) {
100+
ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload));
101+
102+
// Transfer `payload.amount` of `payload.tokenId` to this contract
103+
IERC1155(request.asset).safeTransferFrom(
104+
request.target, address(this), payload.tokenId, payload.amount, payload.data
105+
);
106+
if (IERC1155(request.asset).balanceOf(address(this), payload.tokenId) < payload.amount) {
107+
revert InvalidAllocation(request.asset, payload.amount);
108+
}
109+
(LibTransient.TUint256 storage p, bytes32 tKey) = getERC1155AmountAndKey(request.asset, payload.amount);
110+
p.inc(payload.amount);
111+
key = tKey;
112+
} else {
113+
// Unsupported asset type
114+
revert BoostError.NotImplemented();
115+
}
116+
}
117+
118+
function getFungibleAmountAndKey(address asset)
119+
internal
120+
pure
121+
returns (LibTransient.TUint256 storage p, bytes32 key)
122+
{
123+
key = bytes32(uint256(uint160(asset)));
124+
p = LibTransient.tUint256(key);
125+
}
126+
127+
function getERC1155AmountAndKey(address asset, uint256 tokenId)
128+
internal
129+
pure
130+
returns (LibTransient.TUint256 storage p, bytes32 key)
131+
{
132+
key = keccak256(abi.encodePacked(asset, tokenId));
133+
p = LibTransient.tUint256(key);
134+
}
135+
136+
/// @inheritdoc ABudget
137+
/// @notice Reclaims assets from the budget
138+
/// @param data_ The packed {Transfer} request
139+
/// @return True if the request was successful
140+
/// @dev Only the owner can directly reclaim assets from the budget
141+
/// @dev If the amount is zero, the entire balance of the asset will be transferred to the receiver
142+
/// @dev If the asset transfer fails, the reclamation will revert
143+
function clawback(bytes calldata data_) external virtual override onlyOwner returns (uint256) {
144+
uint256 amount = 0;
145+
Transfer memory request = abi.decode(data_, (Transfer));
146+
if (request.assetType == AssetType.ETH || request.assetType == AssetType.ERC20) {
147+
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
148+
_transferFungible(
149+
request.asset, request.target, payload.amount == 0 ? available(request.asset) : payload.amount
150+
);
151+
amount = payload.amount;
152+
} else if (request.assetType == AssetType.ERC1155) {
153+
ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload));
154+
_transferERC1155(
155+
request.asset,
156+
request.target,
157+
payload.tokenId,
158+
payload.amount == 0 ? IERC1155(request.asset).balanceOf(address(this), payload.tokenId) : payload.amount,
159+
payload.data
160+
);
161+
amount = payload.amount;
162+
}
163+
164+
return amount;
165+
}
166+
167+
/// @inheritdoc ABudget
168+
/// @notice Disburses assets from the budget to a single recipient
169+
/// @param data_ The packed {Transfer} request
170+
/// @return True if the disbursement was successful
171+
/// @dev If the asset transfer fails, the disbursement will revert
172+
function disburse(bytes calldata data_) public virtual override returns (bool) {
173+
Transfer memory request = abi.decode(data_, (Transfer));
174+
if (request.assetType == AssetType.ERC20 || request.assetType == AssetType.ETH) {
175+
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
176+
177+
(LibTransient.TUint256 storage p,) = getFungibleAmountAndKey(request.asset);
178+
uint256 avail = p.get();
179+
if (payload.amount > avail) {
180+
revert InsufficientFunds(request.asset, avail, payload.amount);
181+
}
182+
183+
p.dec(payload.amount);
184+
185+
_transferFungible(request.asset, request.target, payload.amount);
186+
} else if (request.assetType == AssetType.ERC1155) {
187+
ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload));
188+
189+
(LibTransient.TUint256 storage p,) = getERC1155AmountAndKey(request.asset, payload.amount);
190+
uint256 avail = p.get();
191+
if (payload.amount > avail) {
192+
revert InsufficientFunds(request.asset, avail, payload.amount);
193+
}
194+
195+
p.dec(payload.amount);
196+
197+
_transferERC1155(request.asset, request.target, payload.tokenId, payload.amount, payload.data);
198+
} else {
199+
return false;
200+
}
201+
202+
return true;
203+
}
204+
205+
/// @inheritdoc ABudget
206+
/// @notice Disburses assets from the budget to multiple recipients
207+
/// @param data_ The packed array of {Transfer} requests
208+
/// @return True if all disbursements were successful
209+
function disburseBatch(bytes[] calldata data_) external virtual override returns (bool) {
210+
for (uint256 i = 0; i < data_.length; i++) {
211+
if (!disburse(data_[i])) return false;
212+
}
213+
214+
return true;
215+
}
216+
217+
/// @inheritdoc ABudget
218+
/// @notice Get the total amount of assets allocated to the budget, including any that have been distributed
219+
/// @param asset_ The address of the asset
220+
/// @return The total amount of assets
221+
/// @dev This is simply the sum of the current balance and the distributed amount
222+
function total(address asset_) external view virtual override returns (uint256) {
223+
return _distributedFungible[asset_];
224+
}
225+
226+
/// @notice Get the total amount of ERC1155 assets allocated to the budget, including any that have been distributed
227+
/// @param asset_ The address of the asset
228+
/// @param tokenId_ The ID of the token
229+
/// @return The total amount of assets
230+
function total(address asset_, uint256 tokenId_) external view virtual returns (uint256) {
231+
return _distributedERC1155[asset_][tokenId_];
232+
}
233+
234+
/// @inheritdoc ABudget
235+
/// @notice Get the amount of assets available for distribution from the budget
236+
/// @return The amount of assets available
237+
/// @dev This is simply the current balance held by the budget
238+
/// @dev If the zero address is passed, this function will return the native balance
239+
function available(address) public view virtual override returns (uint256) {
240+
return 0;
241+
}
242+
243+
/// @notice Get the amount of ERC1155 assets available for distribution from the budget
244+
/// @return The amount of assets available
245+
function available(address, uint256) public view virtual returns (uint256) {
246+
return 0;
247+
}
248+
249+
/// @inheritdoc ABudget
250+
/// @notice Get the amount of assets that have been distributed from the budget
251+
/// @param asset_ The address of the asset
252+
/// @return The amount of assets distributed
253+
function distributed(address asset_) external view virtual override returns (uint256) {
254+
return _distributedFungible[asset_];
255+
}
256+
257+
/// @notice Get the amount of ERC1155 assets that have been distributed from the budget
258+
/// @param asset_ The address of the asset
259+
/// @param tokenId_ The ID of the token
260+
/// @return The amount of assets distributed
261+
function distributed(address asset_, uint256 tokenId_) external view virtual returns (uint256) {
262+
return _distributedERC1155[asset_][tokenId_];
263+
}
264+
265+
/// @inheritdoc ABudget
266+
/// @dev This is a no-op as there is no local balance to reconcile
267+
function reconcile(bytes calldata) external virtual override returns (uint256) {
268+
return 0;
269+
}
270+
271+
/// @notice Transfer assets to the recipient
272+
/// @param asset_ The address of the asset
273+
/// @param to_ The address of the recipient
274+
/// @param amount_ The amount of the asset to transfer
275+
/// @dev This function is used to transfer assets from the budget to a given recipient (typically an incentive contract)
276+
/// @dev If the destination address is the zero address, or the transfer fails for any reason, this function will revert
277+
function _transferFungible(address asset_, address to_, uint256 amount_) internal virtual nonReentrant {
278+
// Increment the total amount of the asset distributed from the budget
279+
if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_);
280+
if (amount_ > available(asset_)) {
281+
revert InsufficientFunds(asset_, available(asset_), amount_);
282+
}
283+
284+
_distributedFungible[asset_] += amount_;
285+
286+
// Transfer the asset to the recipient
287+
if (asset_ == address(0)) {
288+
SafeTransferLib.safeTransferETH(to_, amount_);
289+
} else {
290+
asset_.safeTransfer(to_, amount_);
291+
}
292+
293+
emit Distributed(asset_, to_, amount_);
294+
}
295+
296+
function _transferERC1155(address asset_, address to_, uint256 tokenId_, uint256 amount_, bytes memory data_)
297+
internal
298+
virtual
299+
nonReentrant
300+
{
301+
// Increment the total amount of the asset distributed from the budget
302+
if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_);
303+
if (amount_ > available(asset_, tokenId_)) {
304+
revert InsufficientFunds(asset_, available(asset_, tokenId_), amount_);
305+
}
306+
307+
_distributedERC1155[asset_][tokenId_] += amount_;
308+
309+
// Transfer the asset to the recipient
310+
// wake-disable-next-line reentrancy (`nonReentrant` modifier is applied to the function)
311+
IERC1155(asset_).safeTransferFrom(address(this), to_, tokenId_, amount_, data_);
312+
313+
emit Distributed(asset_, to_, amount_);
314+
}
315+
316+
/// @inheritdoc IERC1155Receiver
317+
/// @dev This contract does not care about the specifics of the inbound token, so we simply return the magic value (i.e. the selector for `onERC1155Received`)
318+
function onERC1155Received(address, address, uint256, uint256, bytes calldata)
319+
external
320+
pure
321+
override
322+
returns (bytes4)
323+
{
324+
// We don't need to do anything here
325+
return IERC1155Receiver.onERC1155Received.selector;
326+
}
327+
328+
/// @inheritdoc IERC1155Receiver
329+
/// @dev This contract does not care about the specifics of the inbound token, so we simply return the magic value (i.e. the selector for `onERC1155Received`)
330+
function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
331+
external
332+
pure
333+
override
334+
returns (bytes4)
335+
{
336+
// We don't need to do anything here
337+
return IERC1155Receiver.onERC1155BatchReceived.selector;
338+
}
339+
}

packages/evm/test/budgets/SimpleBudget.t.sol

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)