Skip to content

Commit

Permalink
feat(incentives): add reclaim()
Browse files Browse the repository at this point in the history
  • Loading branch information
ccashwell committed Mar 11, 2024
1 parent 861f9b1 commit 6c4ef3d
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 38 deletions.
15 changes: 9 additions & 6 deletions src/budgets/SimpleBudget.sol
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,17 @@ contract SimpleBudget is Budget, IERC1155Receiver, ReentrancyGuard {
Transfer memory request = abi.decode(data_.cdDecompress(), (Transfer));
if (request.assetType == AssetType.ETH || request.assetType == AssetType.ERC20) {
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
uint256 amount = payload.amount == 0 ? available(request.asset) : payload.amount;
_transferFungible(request.asset, request.target, amount);
_transferFungible(
request.asset, request.target, payload.amount == 0 ? available(request.asset) : payload.amount
);
} else if (request.assetType == AssetType.ERC1155) {
ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload));
uint256 amount =
payload.amount == 0 ? IERC1155(request.asset).balanceOf(address(this), payload.tokenId) : payload.amount;
IERC1155(request.asset).safeTransferFrom(
address(this), request.target, payload.tokenId, amount, payload.data
_transferERC1155(
request.asset,
request.target,
payload.tokenId,
payload.amount == 0 ? IERC1155(request.asset).balanceOf(address(this), payload.tokenId) : payload.amount,
payload.data
);
} else {
return false;
Expand Down
16 changes: 16 additions & 0 deletions src/incentives/ERC20Incentive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ contract ERC20Incentive is Incentive {
return false;
}

/// @inheritdoc Incentive
function reclaim(bytes calldata data_) external override onlyOwner returns (bool) {
ClaimPayload memory claim_ = abi.decode(data_.cdDecompress(), (ClaimPayload));
(uint256 amount) = abi.decode(claim_.data, (uint256));

// Ensure the amount is a multiple of the reward and reduce the max claims accordingly
if (amount % reward != 0) revert BoostError.ClaimFailed(msg.sender, abi.encodePacked(claim_.target, amount));
maxClaims -= amount / reward;

// Transfer the tokens back to the intended recipient
asset.safeTransfer(claim_.target, amount);
emit Claimed(claim_.target, abi.encodePacked(asset, claim_.target, amount));

return true;
}

/// @inheritdoc Incentive
/// @notice Preflight the incentive to determine the required budget action
/// @param data_ The {InitPayload} for the incentive
Expand Down
5 changes: 5 additions & 0 deletions src/incentives/Incentive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ abstract contract Incentive is Ownable, Cloneable {
/// @return True if the incentive was successfully claimed
function claim(bytes calldata data_) external virtual returns (bool);

/// @notice Reclaim assets from the incentive
/// @param data_ The data payload for the reclaim
/// @return True if the assets were successfully reclaimed
function reclaim(bytes calldata data_) external virtual returns (bool);

/// @notice Check if an incentive is claimable
/// @param data_ The data payload for the claim check (data, signature, etc.)
/// @return True if the incentive is claimable based on the data payload
Expand Down
4 changes: 4 additions & 0 deletions test/BoostRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ contract MockIncentive is Incentive {
function preflight(bytes calldata) external view virtual override returns (bytes memory) {
return new bytes(0);
}

function reclaim(bytes calldata) external virtual override returns (bool) {
return true;
}
}

contract BoostRegistryTest is Test {
Expand Down
78 changes: 46 additions & 32 deletions test/budgets/SimpleBudget.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,14 @@ contract SimpleBudgetTest is Test, IERC1155Receiver {

// Allocate 100 of token ID 42 to the budget
bytes memory data = LibZip.cdCompress(
abi.encode(Budget.Transfer({
assetType: Budget.AssetType.ERC1155,
asset: address(mockERC1155),
target: address(this),
data: abi.encode(Budget.ERC1155Payload({tokenId: 42, amount: 100, data: ""}))
}))
abi.encode(
Budget.Transfer({
assetType: Budget.AssetType.ERC1155,
asset: address(mockERC1155),
target: address(this),
data: abi.encode(Budget.ERC1155Payload({tokenId: 42, amount: 100, data: ""}))
})
)
);
assertTrue(simpleBudget.allocate(data));

Expand Down Expand Up @@ -270,24 +272,28 @@ contract SimpleBudgetTest is Test, IERC1155Receiver {

// Allocate 100 of token ID 42 to the budget
bytes memory data = LibZip.cdCompress(
abi.encode(Budget.Transfer({
assetType: Budget.AssetType.ERC1155,
asset: address(mockERC1155),
target: address(this),
data: abi.encode(Budget.ERC1155Payload({tokenId: 42, amount: 100, data: ""}))
}))
abi.encode(
Budget.Transfer({
assetType: Budget.AssetType.ERC1155,
asset: address(mockERC1155),
target: address(this),
data: abi.encode(Budget.ERC1155Payload({tokenId: 42, amount: 100, data: ""}))
})
)
);
simpleBudget.allocate(data);
assertEq(simpleBudget.available(address(mockERC1155), 42), 100);

// Reclaim 99 of token ID 42 from the budget
data = LibZip.cdCompress(
abi.encode(Budget.Transfer({
assetType: Budget.AssetType.ERC1155,
asset: address(mockERC1155),
target: address(this),
data: abi.encode(Budget.ERC1155Payload({tokenId: 42, amount: 99, data: ""}))
}))
abi.encode(
Budget.Transfer({
assetType: Budget.AssetType.ERC1155,
asset: address(mockERC1155),
target: address(this),
data: abi.encode(Budget.ERC1155Payload({tokenId: 42, amount: 99, data: ""}))
})
)
);
assertTrue(simpleBudget.reclaim(data));

Expand Down Expand Up @@ -443,24 +449,28 @@ contract SimpleBudgetTest is Test, IERC1155Receiver {

// Allocate 100 of token ID 42 to the budget
bytes memory data = LibZip.cdCompress(
abi.encode(Budget.Transfer({
assetType: Budget.AssetType.ERC1155,
asset: address(mockERC1155),
target: address(this),
data: abi.encode(Budget.ERC1155Payload({tokenId: 42, amount: 100, data: ""}))
}))
abi.encode(
Budget.Transfer({
assetType: Budget.AssetType.ERC1155,
asset: address(mockERC1155),
target: address(this),
data: abi.encode(Budget.ERC1155Payload({tokenId: 42, amount: 100, data: ""}))
})
)
);
simpleBudget.allocate(data);
assertEq(simpleBudget.total(address(mockERC1155), 42), 100);

// Disburse 100 of token ID 42 from the budget to the recipient
data = LibZip.cdCompress(
abi.encode(Budget.Transfer({
assetType: Budget.AssetType.ERC1155,
asset: address(mockERC1155),
target: address(1),
data: abi.encode(Budget.ERC1155Payload({tokenId: 42, amount: 100, data: ""}))
}))
abi.encode(
Budget.Transfer({
assetType: Budget.AssetType.ERC1155,
asset: address(mockERC1155),
target: address(1),
data: abi.encode(Budget.ERC1155Payload({tokenId: 42, amount: 100, data: ""}))
})
)
);
assertTrue(simpleBudget.disburse(data));
assertEq(mockERC1155.balanceOf(address(1), 42), 100);
Expand All @@ -476,8 +486,12 @@ contract SimpleBudgetTest is Test, IERC1155Receiver {
mockERC1155.setApprovalForAll(address(simpleBudget), true);

// Allocate the assets to the budget
simpleBudget.allocate(_makeFungibleTransfer(Budget.AssetType.ERC20, address(mockERC20), address(this), 50 ether));
simpleBudget.allocate{value: 25 ether}(_makeFungibleTransfer(Budget.AssetType.ETH, address(0), address(this), 25 ether));
simpleBudget.allocate(
_makeFungibleTransfer(Budget.AssetType.ERC20, address(mockERC20), address(this), 50 ether)
);
simpleBudget.allocate{value: 25 ether}(
_makeFungibleTransfer(Budget.AssetType.ETH, address(0), address(this), 25 ether)
);
simpleBudget.allocate(_makeERC1155Transfer(address(mockERC1155), address(this), 42, 50, bytes("")));
assertEq(simpleBudget.total(address(mockERC20)), 50 ether);
assertEq(simpleBudget.total(address(0)), 25 ether);
Expand Down
35 changes: 35 additions & 0 deletions test/incentives/ERC20Incentive.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,41 @@ contract ERC20IncentiveTest is Test {
incentive.claim(claimPayload);
}

////////////////////////////
// ERC20Incentive.reclaim //
////////////////////////////

function testReclaim() public {
// Initialize the ERC20Incentive
_initialize(address(mockAsset), ERC20Incentive.Strategy.POOL, 1 ether, 100);
assertEq(incentive.maxClaims(), 100);

// Reclaim 50x the reward amount
bytes memory reclaimPayload =
LibZip.cdCompress(abi.encode(Incentive.ClaimPayload({target: address(1), data: abi.encode(50 ether)})));
incentive.reclaim(reclaimPayload);
assertEq(mockAsset.balanceOf(address(1)), 50 ether);

// Check that enough assets remain to cover 50 more claims
assertEq(mockAsset.balanceOf(address(incentive)), 50 ether);
assertEq(incentive.maxClaims(), 50);
}

function testReclaim_InvalidAmount() public {
// Initialize the ERC20Incentive
_initialize(address(mockAsset), ERC20Incentive.Strategy.POOL, 1 ether, 100);

// Reclaim 50.1x => not an integer multiple of the reward amount => revert
bytes memory reclaimPayload =
LibZip.cdCompress(abi.encode(Incentive.ClaimPayload({target: address(1), data: abi.encode(50.1 ether)})));
vm.expectRevert(
abi.encodeWithSelector(
BoostError.ClaimFailed.selector, address(this), abi.encodePacked(address(1), uint256(50.1 ether))
)
);
incentive.reclaim(reclaimPayload);
}

////////////////////////////////
// ERC20Incentive.isClaimable //
////////////////////////////////
Expand Down

0 comments on commit 6c4ef3d

Please sign in to comment.