Skip to content

Commit 4a7b0cd

Browse files
authored
[BOOST-5261] feat(evm): add authorized clawbacks from incentives to TransparentBudget (#391)
1 parent 06becaa commit 4a7b0cd

File tree

5 files changed

+301
-23
lines changed

5 files changed

+301
-23
lines changed

.github/workflows/verify.yml

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
uses: SpicyPizza/[email protected]
3737
with:
3838
envkey_TEST_SIGNER_PRIVATE_KEY: ${{ secrets.TEST_SIGNER_PRIVATE_KEY }}
39+
envkey_VITE_SEPOLIA_RPC_URL: ${{ secrets.VITE_SEPOLIA_RPC_URL }}
3940
directory: packages/evm
4041
file_name: .env
4142

packages/evm/.env.example

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ TEST_SIGNER_PRIVATE_KEY=
22
BOOST_FEE_RECIPIENT=
33
BOOST_DEPLOYMENT_SALT=
44
ETHERSCAN_API_KEY=
5-
MAIN_ETHERSCAN_API_KEY=
5+
MAIN_ETHERSCAN_API_KEY=
6+
VITE_SEPOLIA_RPC_URL=

packages/evm/contracts/budgets/TransparentBudget.sol

+94-21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity ^0.8.24;
44
import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
55
import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
66
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
7+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
78
import {DynamicArrayLib} from "@solady/utils/DynamicArrayLib.sol";
89
import {LibTransient} from "@solady/utils/LibTransient.sol";
910

@@ -13,16 +14,13 @@ import {ReentrancyGuard} from "@solady/utils/ReentrancyGuard.sol";
1314

1415
import {BoostCore} from "contracts/BoostCore.sol";
1516
import {BoostError} from "contracts/shared/BoostError.sol";
17+
import {BoostLib} from "contracts/shared/BoostLib.sol";
1618
import {ABudget} from "contracts/budgets/ABudget.sol";
1719
import {ACloneable} from "contracts/shared/ACloneable.sol";
20+
import {AIncentive} from "contracts/incentives/AIncentive.sol";
1821
import {ATransparentBudget} from "contracts/budgets/ATransparentBudget.sol";
19-
20-
/*
21-
TODO
22-
1. implement clawback logic and tracking on deposits
23-
2. implement clawback auth
24-
3. implement permit2 support
25-
*/
22+
import {IClaw} from "contracts/shared/IClaw.sol";
23+
import {IPermit2} from "contracts/shared/IPermit2.sol";
2624

2725
/// @title Simple ABudget
2826
/// @notice A minimal budget implementation that simply holds and distributes tokens (ERC20-like and native)
@@ -32,6 +30,8 @@ contract TransparentBudget is ATransparentBudget, ReentrancyGuard {
3230
using DynamicArrayLib for *;
3331
using LibTransient for *;
3432

33+
IPermit2 public constant PERMIT2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3);
34+
3535
/// @dev The total amount of each fungible asset distributed from the budget
3636
mapping(address => uint256) private _distributedFungible;
3737

@@ -44,10 +44,7 @@ contract TransparentBudget is ATransparentBudget, ReentrancyGuard {
4444
revert BoostError.NotImplemented();
4545
}
4646

47-
function createBoost(bytes[] calldata _allocations, BoostCore core, bytes calldata _boostPayload)
48-
external
49-
payable
50-
{
47+
function createBoost(bytes[] calldata _allocations, BoostCore core, bytes calldata _boostPayload) public payable {
5148
DynamicArrayLib.DynamicArray memory allocationKeys;
5249
allocationKeys.resize(_allocations.length);
5350

@@ -65,6 +62,58 @@ contract TransparentBudget is ATransparentBudget, ReentrancyGuard {
6562
}
6663
}
6764

65+
function createBoostWithPermit2(
66+
bytes[] calldata _allocations,
67+
BoostCore core,
68+
bytes calldata _boostPayload,
69+
bytes calldata _permit2Signature,
70+
uint256 nonce,
71+
uint256 deadline
72+
) external payable {
73+
DynamicArrayLib.DynamicArray memory allocationKeys;
74+
allocationKeys.resize(_allocations.length);
75+
76+
IPermit2.SignatureTransferDetails[] memory transferDetails =
77+
new IPermit2.SignatureTransferDetails[](_allocations.length);
78+
IPermit2.TokenPermissions[] memory permissions = new IPermit2.TokenPermissions[](_allocations.length);
79+
bytes32 key;
80+
for (uint256 i = 0; i < _allocations.length; i++) {
81+
Transfer memory request = abi.decode(_allocations[i], (Transfer));
82+
if (request.assetType == AssetType.ERC20) {
83+
(address asset, uint256 amount, bytes32 tKey) = _allocateERC20(request, false);
84+
key = tKey;
85+
transferDetails[i] = IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: amount});
86+
permissions[i] = IPermit2.TokenPermissions({token: IERC20(asset), amount: amount});
87+
} else {
88+
key = _allocate(_allocations[i]);
89+
}
90+
allocationKeys.set(i, key);
91+
}
92+
93+
PERMIT2.permitTransferFrom(
94+
// The permit message. Spender will be inferred as the caller (us).
95+
IPermit2.PermitBatchTransferFrom({permitted: permissions, nonce: nonce, deadline: deadline}),
96+
// The transfer recipients and amounts.
97+
transferDetails,
98+
// The owner of the tokens, which must also be
99+
// the signer of the message, otherwise this call
100+
// will fail.
101+
msg.sender,
102+
// The packed signature that was the result of signing
103+
// the EIP712 hash of `permit`.
104+
_permit2Signature
105+
);
106+
107+
// Transfer `payload.amount` of the token to this contract
108+
109+
core.createBoost(_boostPayload);
110+
bytes32[] memory keys = allocationKeys.asBytes32Array();
111+
for (uint256 i = 0; i < keys.length; i++) {
112+
LibTransient.TUint256 storage p = LibTransient.tUint256(keys[i]);
113+
if (p.get() != 0) revert BoostError.Unauthorized();
114+
}
115+
}
116+
68117
/// @notice Allocates assets to be distributed in the boost
69118
/// @param data_ The packed data for the {Transfer} request
70119
/// @return key The key of the amount allocated
@@ -83,16 +132,7 @@ contract TransparentBudget is ATransparentBudget, ReentrancyGuard {
83132
p.inc(payload.amount);
84133
key = tKey;
85134
} else if (request.assetType == AssetType.ERC20) {
86-
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
87-
88-
// Transfer `payload.amount` of the token to this contract
89-
request.asset.safeTransferFrom(request.target, address(this), payload.amount);
90-
if (request.asset.balanceOf(address(this)) < payload.amount) {
91-
revert InvalidAllocation(request.asset, payload.amount);
92-
}
93-
key = bytes32(uint256(uint160(request.asset)));
94-
(LibTransient.TUint256 storage p, bytes32 tKey) = getFungibleAmountAndKey(request.asset);
95-
p.inc(payload.amount);
135+
(,, bytes32 tKey) = _allocateERC20(request, true);
96136
key = tKey;
97137
} else if (request.assetType == AssetType.ERC1155) {
98138
ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload));
@@ -113,6 +153,24 @@ contract TransparentBudget is ATransparentBudget, ReentrancyGuard {
113153
}
114154
}
115155

156+
function _allocateERC20(Transfer memory request, bool transfer)
157+
internal
158+
returns (address asset, uint256 amount, bytes32 key)
159+
{
160+
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
161+
if (transfer) {
162+
request.asset.safeTransferFrom(request.target, address(this), payload.amount);
163+
if (request.asset.balanceOf(address(this)) < payload.amount) {
164+
revert InvalidAllocation(request.asset, payload.amount);
165+
}
166+
}
167+
(LibTransient.TUint256 storage p, bytes32 tKey) = getFungibleAmountAndKey(request.asset);
168+
p.inc(payload.amount);
169+
amount = payload.amount;
170+
asset = request.asset;
171+
key = tKey;
172+
}
173+
116174
function isAuthorized(address account_) public view virtual override returns (bool) {
117175
if (account_ == address(this)) return true;
118176
return false;
@@ -145,6 +203,21 @@ contract TransparentBudget is ATransparentBudget, ReentrancyGuard {
145203
revert BoostError.NotImplemented();
146204
}
147205

206+
function clawbackFromTarget(address target, bytes calldata data_, uint256 boostId, uint256 incentiveId)
207+
external
208+
virtual
209+
override
210+
returns (uint256, address)
211+
{
212+
BoostLib.Boost memory boost = BoostCore(target).getBoost(boostId);
213+
if (msg.sender != boost.owner) revert BoostError.Unauthorized();
214+
AIncentive.ClawbackPayload memory payload =
215+
AIncentive.ClawbackPayload({target: address(msg.sender), data: data_});
216+
IClaw incentive = IClaw(target);
217+
(uint256 amount, address asset) = incentive.clawback(abi.encode(payload), boostId, incentiveId);
218+
return (amount, asset);
219+
}
220+
148221
/// @inheritdoc ABudget
149222
/// @notice Disburses assets from the budget to a single recipient
150223
/// @param data_ The packed {Transfer} request
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
6+
// Minimal Permit2 interface, derived from
7+
// https://github.com/Uniswap/permit2/blob/main/src/interfaces/ISignatureTransfer.sol
8+
interface IPermit2 {
9+
// Token and amount in a permit message.
10+
struct TokenPermissions {
11+
// Token to transfer.
12+
IERC20 token;
13+
// Amount to transfer.
14+
uint256 amount;
15+
}
16+
17+
// The permit2 message.
18+
struct PermitTransferFrom {
19+
// Permitted token and maximum amount.
20+
TokenPermissions permitted; // deadline on the permit signature
21+
// Unique identifier for this permit.
22+
uint256 nonce;
23+
// Expiration for this permit.
24+
uint256 deadline;
25+
}
26+
27+
// The permit2 message for batch transfers.
28+
struct PermitBatchTransferFrom {
29+
// Permitted tokens and maximum amounts.
30+
TokenPermissions[] permitted;
31+
// Unique identifier for this permit.
32+
uint256 nonce;
33+
// Expiration for this permit.
34+
uint256 deadline;
35+
}
36+
37+
// Transfer details for permitTransferFrom().
38+
struct SignatureTransferDetails {
39+
// Recipient of tokens.
40+
address to;
41+
// Amount to transfer.
42+
uint256 requestedAmount;
43+
}
44+
45+
// Consume a permit2 message and transfer tokens.
46+
function permitTransferFrom(
47+
PermitTransferFrom calldata permit,
48+
SignatureTransferDetails calldata transferDetails,
49+
address owner,
50+
bytes calldata signature
51+
) external;
52+
53+
// Consume a batch permit2 message and transfer tokens.
54+
function permitTransferFrom(
55+
PermitBatchTransferFrom calldata permit,
56+
SignatureTransferDetails[] calldata transferDetails,
57+
address owner,
58+
bytes calldata signature
59+
) external;
60+
61+
function DOMAIN_SEPARATOR() external view returns (bytes32);
62+
}

0 commit comments

Comments
 (0)