Skip to content

Commit 402481f

Browse files
committed
feat(evm): implement Transparent Budget and Boost Creation flow
This Budget allows any user to create a boost as long as the amount transferred to the budgets is completely consumed by all the incentives created by the boost. Any remainder will cause the entire tx to revert, including the token transfer. Reentrancy risk is mitigated by only doing accounting in transient storage, and only updating the budget token balances after transfer into the budget.
1 parent 5b1bd82 commit 402481f

File tree

5 files changed

+717
-2
lines changed

5 files changed

+717
-2
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: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
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+
{
52+
DynamicArrayLib.DynamicArray memory allocationKeys;
53+
allocationKeys.resize(_allocations.length);
54+
55+
for (uint256 i = 0; i < _allocations.length; i++) {
56+
bytes32 key = _allocate(_allocations[i]);
57+
allocationKeys.set(i, key);
58+
}
59+
60+
core.createBoost(_boostPayload);
61+
62+
bytes32[] memory keys = allocationKeys.asBytes32Array();
63+
for (uint256 i = 0; i < keys.length; i++) {
64+
LibTransient.TUint256 storage p = LibTransient.tUint256(keys[i]);
65+
if (p.get() != 0) revert BoostError.Unauthorized();
66+
}
67+
}
68+
69+
/// @notice Allocates assets to be distributed in the boost
70+
/// @param data_ The packed data for the {Transfer} request
71+
/// @return key The key of the amount allocated
72+
/// @dev The caller must have already approved the contract to transfer the asset
73+
/// @dev If the asset transfer fails, the allocation will revert
74+
function _allocate(bytes calldata data_) internal virtual returns (bytes32 key) {
75+
Transfer memory request = abi.decode(data_, (Transfer));
76+
if (request.assetType == AssetType.ETH) {
77+
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
78+
79+
// Ensure the value received is equal to the `payload.amount`
80+
if (msg.value != payload.amount) {
81+
revert InvalidAllocation(request.asset, payload.amount);
82+
}
83+
(LibTransient.TUint256 storage p, bytes32 tKey) = getFungibleAmountAndKey(address(0));
84+
p.inc(payload.amount);
85+
key = tKey;
86+
} else if (request.assetType == AssetType.ERC20) {
87+
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
88+
89+
// Transfer `payload.amount` of the token to this contract
90+
request.asset.safeTransferFrom(request.target, address(this), payload.amount);
91+
if (request.asset.balanceOf(address(this)) < payload.amount) {
92+
revert InvalidAllocation(request.asset, payload.amount);
93+
}
94+
key = bytes32(uint256(uint160(request.asset)));
95+
(LibTransient.TUint256 storage p, bytes32 tKey) = getFungibleAmountAndKey(request.asset);
96+
p.inc(payload.amount);
97+
key = tKey;
98+
} else if (request.assetType == AssetType.ERC1155) {
99+
ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload));
100+
101+
// Transfer `payload.amount` of `payload.tokenId` to this contract
102+
IERC1155(request.asset).safeTransferFrom(
103+
request.target, address(this), payload.tokenId, payload.amount, payload.data
104+
);
105+
if (IERC1155(request.asset).balanceOf(address(this), payload.tokenId) < payload.amount) {
106+
revert InvalidAllocation(request.asset, payload.amount);
107+
}
108+
(LibTransient.TUint256 storage p, bytes32 tKey) = getERC1155AmountAndKey(request.asset, payload.amount);
109+
p.inc(payload.amount);
110+
key = tKey;
111+
} else {
112+
// Unsupported asset type
113+
revert BoostError.NotImplemented();
114+
}
115+
}
116+
117+
function isAuthorized(address account_) public view virtual override returns (bool) {
118+
if (account_ == address(this)) return true;
119+
return false;
120+
}
121+
122+
function getFungibleAmountAndKey(address asset)
123+
internal
124+
pure
125+
returns (LibTransient.TUint256 storage p, bytes32 key)
126+
{
127+
key = bytes32(uint256(uint160(asset)));
128+
p = LibTransient.tUint256(key);
129+
}
130+
131+
function getERC1155AmountAndKey(address asset, uint256 tokenId)
132+
internal
133+
pure
134+
returns (LibTransient.TUint256 storage p, bytes32 key)
135+
{
136+
key = keccak256(abi.encodePacked(asset, tokenId));
137+
p = LibTransient.tUint256(key);
138+
}
139+
140+
/// @inheritdoc ABudget
141+
/// @notice Reclaims assets from the budget
142+
/// @dev Only the owner can directly reclaim assets from the budget
143+
/// @dev If the amount is zero, the entire balance of the asset will be transferred to the receiver
144+
/// @dev If the asset transfer fails, the reclamation will revert
145+
function clawback(bytes calldata) external virtual override onlyOwner returns (uint256) {
146+
revert BoostError.NotImplemented();
147+
}
148+
149+
/// @inheritdoc ABudget
150+
/// @notice Disburses assets from the budget to a single recipient
151+
/// @param data_ The packed {Transfer} request
152+
/// @return True if the disbursement was successful
153+
/// @dev If the asset transfer fails, the disbursement will revert
154+
function disburse(bytes calldata data_) public virtual override returns (bool) {
155+
Transfer memory request = abi.decode(data_, (Transfer));
156+
if (request.assetType == AssetType.ERC20 || request.assetType == AssetType.ETH) {
157+
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
158+
159+
(LibTransient.TUint256 storage p,) = getFungibleAmountAndKey(request.asset);
160+
uint256 avail = p.get();
161+
if (payload.amount > avail) {
162+
revert InsufficientFunds(request.asset, avail, payload.amount);
163+
}
164+
165+
_transferFungible(request.asset, request.target, payload.amount);
166+
} else if (request.assetType == AssetType.ERC1155) {
167+
ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload));
168+
169+
(LibTransient.TUint256 storage p,) = getERC1155AmountAndKey(request.asset, payload.amount);
170+
uint256 avail = p.get();
171+
if (payload.amount > avail) {
172+
revert InsufficientFunds(request.asset, avail, payload.amount);
173+
}
174+
175+
p.dec(payload.amount);
176+
177+
_transferERC1155(request.asset, request.target, payload.tokenId, payload.amount, payload.data);
178+
} else {
179+
return false;
180+
}
181+
182+
return true;
183+
}
184+
185+
/// @inheritdoc ABudget
186+
/// @notice Disburses assets from the budget to multiple recipients
187+
/// @param data_ The packed array of {Transfer} requests
188+
/// @return True if all disbursements were successful
189+
function disburseBatch(bytes[] calldata data_) external virtual override returns (bool) {
190+
for (uint256 i = 0; i < data_.length; i++) {
191+
if (!disburse(data_[i])) return false;
192+
}
193+
194+
return true;
195+
}
196+
197+
/// @inheritdoc ABudget
198+
/// @notice Get the total amount of assets allocated to the budget, including any that have been distributed
199+
/// @param asset_ The address of the asset
200+
/// @return The total amount of assets
201+
/// @dev This is simply the sum of the current balance and the distributed amount
202+
function total(address asset_) external view virtual override returns (uint256) {
203+
return _distributedFungible[asset_];
204+
}
205+
206+
/// @notice Get the total amount of ERC1155 assets allocated to the budget, including any that have been distributed
207+
/// @param asset_ The address of the asset
208+
/// @param tokenId_ The ID of the token
209+
/// @return The total amount of assets
210+
function total(address asset_, uint256 tokenId_) external view virtual returns (uint256) {
211+
return _distributedERC1155[asset_][tokenId_];
212+
}
213+
214+
/// @inheritdoc ABudget
215+
/// @notice Get the amount of assets available for distribution from the budget
216+
/// @return The amount of assets available
217+
/// @dev This is simply the current balance held by the budget
218+
/// @dev If the zero address is passed, this function will return the native balance
219+
function available(address) public view virtual override returns (uint256) {
220+
return 0;
221+
}
222+
223+
/// @notice Get the amount of ERC1155 assets available for distribution from the budget
224+
/// @return The amount of assets available
225+
function available(address, uint256) public view virtual returns (uint256) {
226+
return 0;
227+
}
228+
229+
/// @inheritdoc ABudget
230+
/// @notice Get the amount of assets that have been distributed from the budget
231+
/// @param asset_ The address of the asset
232+
/// @return The amount of assets distributed
233+
function distributed(address asset_) external view virtual override returns (uint256) {
234+
return _distributedFungible[asset_];
235+
}
236+
237+
/// @notice Get the amount of ERC1155 assets that have been distributed from the budget
238+
/// @param asset_ The address of the asset
239+
/// @param tokenId_ The ID of the token
240+
/// @return The amount of assets distributed
241+
function distributed(address asset_, uint256 tokenId_) external view virtual returns (uint256) {
242+
return _distributedERC1155[asset_][tokenId_];
243+
}
244+
245+
/// @inheritdoc ABudget
246+
/// @dev This is a no-op as there is no local balance to reconcile
247+
function reconcile(bytes calldata) external virtual override returns (uint256) {
248+
return 0;
249+
}
250+
251+
/// @notice Transfer assets to the recipient
252+
/// @param asset_ The address of the asset
253+
/// @param to_ The address of the recipient
254+
/// @param amount_ The amount of the asset to transfer
255+
/// @dev This function is used to transfer assets from the budget to a given recipient (typically an incentive contract)
256+
/// @dev If the destination address is the zero address, or the transfer fails for any reason, this function will revert
257+
function _transferFungible(address asset_, address to_, uint256 amount_) internal virtual nonReentrant {
258+
(LibTransient.TUint256 storage p,) = getFungibleAmountAndKey(asset_);
259+
uint256 avail = p.get();
260+
// Increment the total amount of the asset distributed from the budget
261+
if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_);
262+
if (amount_ > avail) {
263+
revert InsufficientFunds(asset_, avail, amount_);
264+
}
265+
p.dec(amount_);
266+
267+
_distributedFungible[asset_] += amount_;
268+
269+
// Transfer the asset to the recipient
270+
if (asset_ == address(0)) {
271+
SafeTransferLib.safeTransferETH(to_, amount_);
272+
} else {
273+
asset_.safeTransfer(to_, amount_);
274+
}
275+
276+
emit Distributed(asset_, to_, amount_);
277+
}
278+
279+
function _transferERC1155(address asset_, address to_, uint256 tokenId_, uint256 amount_, bytes memory data_)
280+
internal
281+
virtual
282+
nonReentrant
283+
{
284+
// Increment the total amount of the asset distributed from the budget
285+
if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_);
286+
if (amount_ > available(asset_, tokenId_)) {
287+
revert InsufficientFunds(asset_, available(asset_, tokenId_), amount_);
288+
}
289+
290+
_distributedERC1155[asset_][tokenId_] += amount_;
291+
292+
// Transfer the asset to the recipient
293+
// wake-disable-next-line reentrancy (`nonReentrant` modifier is applied to the function)
294+
IERC1155(asset_).safeTransferFrom(address(this), to_, tokenId_, amount_, data_);
295+
296+
emit Distributed(asset_, to_, amount_);
297+
}
298+
299+
/// @inheritdoc IERC1155Receiver
300+
/// @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`)
301+
function onERC1155Received(address, address, uint256, uint256, bytes calldata)
302+
external
303+
pure
304+
override
305+
returns (bytes4)
306+
{
307+
// We don't need to do anything here
308+
return IERC1155Receiver.onERC1155Received.selector;
309+
}
310+
311+
/// @inheritdoc IERC1155Receiver
312+
/// @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`)
313+
function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
314+
external
315+
pure
316+
override
317+
returns (bytes4)
318+
{
319+
// We don't need to do anything here
320+
return IERC1155Receiver.onERC1155BatchReceived.selector;
321+
}
322+
}

0 commit comments

Comments
 (0)