Skip to content

Commit

Permalink
feat(sdk): ERC20VariableIncentive sdk integration + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sammccord committed Sep 3, 2024
1 parent e6d84a2 commit a0af829
Show file tree
Hide file tree
Showing 14 changed files with 744 additions and 77 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ VITE_VESTING_BUDGET_BASE=
VITE_ALLOWLIST_INCENTIVE_BASE=
VITE_CGDA_INCENTIVE_BASE=
VITE_ERC20_INCENTIVE_BASE=
VITE_ERC20_VARIABLE_INCENTIVE_BASE=
VITE_ERC1155_INCENTIVE_BASE=
VITE_POINTS_INCENTIVE_BASE=
VITE_SIGNER_VALIDATOR_BASE=
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import VestingBudgetArtifact from '@boostxyz/evm/artifacts/contracts/budgets/Ves
import AllowListIncentiveArtifact from '@boostxyz/evm/artifacts/contracts/incentives/AllowListIncentive.sol/AllowListIncentive.json';
import CGDAIncentiveArtifact from '@boostxyz/evm/artifacts/contracts/incentives/CGDAIncentive.sol/CGDAIncentive.json';
import ERC20IncentiveArtifact from '@boostxyz/evm/artifacts/contracts/incentives/ERC20Incentive.sol/ERC20Incentive.json';
import ERC20VariableIncentiveArtifact from '@boostxyz/evm/artifacts/contracts/incentives/ERC20VariableIncentive.sol/ERC20VariableIncentive.json';
import ERC1155IncentiveArtifact from '@boostxyz/evm/artifacts/contracts/incentives/ERC1155Incentive.sol/ERC1155Incentive.json';
import PointsIncentiveArtifact from '@boostxyz/evm/artifacts/contracts/incentives/PointsIncentive.sol/PointsIncentive.json';
import SignerValidatorArtifact from '@boostxyz/evm/artifacts/contracts/validators/SignerValidator.sol/SignerValidator.json';
Expand All @@ -19,6 +20,7 @@ import {
ContractAction,
type DeployableOptions,
ERC20Incentive,
ERC20VariableIncentive,
ERC721MintAction,
ERC1155Incentive,
EventAction,
Expand Down Expand Up @@ -196,6 +198,15 @@ export const deploy: Command<DeployResult> = async function deploy(opts) {
}),
);

const erc20VariableIncentiveBase = await getDeployedContractAddress(
config,
deployContract(config, {
abi: ERC20VariableIncentiveArtifact.abi,
bytecode: ERC20VariableIncentiveArtifact.bytecode as Hex,
account,
}),
);

const signerValidatorBase = await getDeployedContractAddress(
config,
deployContract(config, {
Expand Down Expand Up @@ -236,6 +247,9 @@ export const deploy: Command<DeployResult> = async function deploy(opts) {
ERC20Incentive: class TERC20Incentive extends ERC20Incentive {
public static override base = erc20IncentiveBase;
},
ERC20VariableIncentive: class TERC20VariableIncentive extends ERC20VariableIncentive {
public static override base = erc20VariableIncentiveBase;
},
ERC1155Incentive: class TERC1155Incentive extends ERC1155Incentive {
public static override base = erc1155IncentiveBase;
},
Expand Down Expand Up @@ -264,6 +278,7 @@ export const deploy: Command<DeployResult> = async function deploy(opts) {
ALLOWLIST_INCENTIVE_BASE: allowListIncentiveBase,
CGDA_INCENTIVE_BASE: cgdaIncentiveBase,
ERC20_INCENTIVE_BASE: erc20IncentiveBase,
ERC20_VARIABLE_INCENTIVE_BASE: erc20VariableIncentiveBase,
ERC1155_INCENTIVE_BASE: erc1155IncentiveBase,
POINTS_INCENTIVE_BASE: pointsIncentiveBase,
SIGNER_VALIDATOR_BASE: signerValidatorBase,
Expand Down
89 changes: 89 additions & 0 deletions packages/evm/contracts/incentives/AERC20VariableIncentive.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

import {LibPRNG} from "@solady/utils/LibPRNG.sol";
import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol";

import {BoostError} from "contracts/shared/BoostError.sol";
import {Incentive} from "contracts/incentives/Incentive.sol";
import {Budget} from "contracts/budgets/Budget.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/// @title ERC20 Incentive with Variable Rewards
/// @notice A modified ERC20 incentive implementation that allows claiming of variable token amounts with a spending limit
abstract contract AERC20VariableIncentive is Incentive {
using SafeTransferLib for address;

/// @notice The address of the ERC20-like token
address public asset;

/// @notice The spending limit (max total claimable amount)
uint256 public limit;

/// @notice The total amount claimed so far
uint256 public totalClaimed;

/// @notice Claim the incentive with variable rewards
/// @param data_ The data payload for the incentive claim `(address recipient, bytes data)`
/// @return True if the incentive was successfully claimed
function claim(bytes calldata data_) external override onlyOwner returns (bool) {
ClaimPayload memory claim_ = abi.decode(data_, (ClaimPayload));
uint256 signedAmount = abi.decode(claim_.data, (uint256));
uint256 claimAmount;
if (!_isClaimable(claim_.target)) revert NotClaimable();

if (reward == 0) {
claimAmount = signedAmount;
} else {
// NOTE: this is assuming that the signed scalar is in ETH decimal format
claimAmount = reward * signedAmount / 1e18;
}

if (totalClaimed + claimAmount > limit) revert ClaimFailed();

totalClaimed += claimAmount;
asset.safeTransfer(claim_.target, claimAmount);

emit Claimed(claim_.target, abi.encodePacked(asset, claim_.target, claimAmount));
return true;
}

/// @notice Check if an incentive is claimable
/// @param data_ The data payload for the claim check `(address recipient, bytes data)`
/// @return True if the incentive is claimable based on the data payload
function isClaimable(bytes calldata data_) public view override returns (bool) {
ClaimPayload memory claim_ = abi.decode(data_, (ClaimPayload));
return _isClaimable(claim_.target);
}

/// @notice Check if an incentive is claimable for a specific recipient
/// @param recipient_ The address of the recipient
/// @return True if the incentive is claimable for the recipient
function _isClaimable(address recipient_) internal view returns (bool) {
return totalClaimed < limit;
}

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

limit -= amount;

// 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
function getComponentInterface() public pure virtual override returns (bytes4) {
return type(AERC20VariableIncentive).interfaceId;
}

/// @inheritdoc Incentive
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(AERC20VariableIncentive).interfaceId || super.supportsInterface(interfaceId);
}
}
84 changes: 9 additions & 75 deletions packages/evm/contracts/incentives/ERC20VariableIncentive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import {LibPRNG} from "@solady/utils/LibPRNG.sol";
import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol";

import {BoostError} from "contracts/shared/BoostError.sol";
import {Incentive} from "contracts/incentives/Incentive.sol";
import {AERC20VariableIncentive} from "contracts/incentives/AERC20VariableIncentive.sol";
import {Budget} from "contracts/budgets/Budget.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Incentive} from "contracts/incentives/Incentive.sol";

/// @title ERC20 Incentive with Variable Rewards
/// @notice A modified ERC20 incentive implementation that allows claiming of variable token amounts with a spending limit

contract ERC20VariableIncentive is Incentive {
contract ERC20VariableIncentive is AERC20VariableIncentive {
using SafeTransferLib for address;

/// @notice The reward multiplier; if 0, the signed amount from the claim payload is used directly
Expand All @@ -22,14 +23,11 @@ contract ERC20VariableIncentive is Incentive {
uint256 limit;
}

/// @notice The address of the ERC20-like token
address public asset;

/// @notice The spending limit (max total claimable amount)
uint256 public limit;

/// @notice The total amount claimed so far
uint256 public totalClaimed;
/// @notice Construct a new ERC20VariableIncentive
/// @dev Because this contract is a base implementation, it should not be initialized through the constructor. Instead, it should be cloned and initialized using the {initialize} function.
constructor() {
_disableInitializers();
}

/// @notice Initialize the contract with the incentive parameters
/// @param data_ The compressed incentive parameters `(address asset, uint256 reward, uint256 limit)`
Expand All @@ -55,60 +53,6 @@ contract ERC20VariableIncentive is Incentive {
_initializeOwner(msg.sender);
}

/// @notice Claim the incentive with variable rewards
/// @param data_ The data payload for the incentive claim `(address recipient, bytes data)`
/// @return True if the incentive was successfully claimed
function claim(bytes calldata data_) external override onlyOwner returns (bool) {
ClaimPayload memory claim_ = abi.decode(data_, (ClaimPayload));
uint256 signedAmount = abi.decode(claim_.data, (uint256));
uint256 claimAmount;
if (!_isClaimable(claim_.target)) revert NotClaimable();

if (reward == 0) {
claimAmount = signedAmount;
} else {
// NOTE: this is assuming that the signed scalar is in ETH decimal format
claimAmount = reward * signedAmount / 1e18;
}

if (totalClaimed + claimAmount > limit) revert ClaimFailed();

totalClaimed += claimAmount;
asset.safeTransfer(claim_.target, claimAmount);

emit Claimed(claim_.target, abi.encodePacked(asset, claim_.target, claimAmount));
return true;
}

/// @notice Check if an incentive is claimable
/// @param data_ The data payload for the claim check `(address recipient, bytes data)`
/// @return True if the incentive is claimable based on the data payload
function isClaimable(bytes calldata data_) public view override returns (bool) {
ClaimPayload memory claim_ = abi.decode(data_, (ClaimPayload));
return _isClaimable(claim_.target);
}

/// @notice Check if an incentive is claimable for a specific recipient
/// @param recipient_ The address of the recipient
/// @return True if the incentive is claimable for the recipient
function _isClaimable(address recipient_) internal view returns (bool) {
return totalClaimed < limit;
}

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

limit -= amount;

// 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 data payload for the incentive `(address asset, uint256 reward, uint256 limit)`
Expand All @@ -125,14 +69,4 @@ contract ERC20VariableIncentive is Incentive {
})
);
}

/// @inheritdoc Incentive
function getComponentInterface() public pure virtual override returns (bytes4) {
return type(Incentive).interfaceId;
}

/// @inheritdoc Incentive
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(Incentive).interfaceId || super.supportsInterface(interfaceId);
}
}
1 change: 1 addition & 0 deletions packages/evm/test/incentives/ERC20VariableIncentive.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ contract ERC20VariableIncentiveTest is Test {
}

function testSupportsInterface_NotSupported() public {
console.logBytes4(incentive.getComponentInterface());
// Ensure the contract does not support an unsupported interface
assertFalse(incentive.supportsInterface(type(Test).interfaceId));
}
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ VITE_VESTING_BUDGET_BASE=
VITE_ALLOWLIST_INCENTIVE_BASE=
VITE_CGDA_INCENTIVE_BASE=
VITE_ERC20_INCENTIVE_BASE=
VITE_ERC20_VARIABLE_INCENTIVE_BASE=
VITE_ERC1155_INCENTIVE_BASE=
VITE_POINTS_INCENTIVE_BASE=
VITE_SIGNER_VALIDATOR_BASE=
29 changes: 28 additions & 1 deletion packages/sdk/src/BoostCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ import {
ERC1155Incentive,
type ERC1155IncentivePayload,
} from './Incentives/ERC1155Incentive';
import { type Incentive, incentiveFromAddress } from './Incentives/Incentive';
import {
ERC20VariableIncentive,
type Incentive,
incentiveFromAddress,
} from './Incentives/Incentive';
import {
PointsIncentive,
type PointsIncentivePayload,
Expand All @@ -94,6 +98,7 @@ import {
NoContractAddressUponReceiptError,
} from './errors';
import {
type ERC20VariableIncentivePayload,
type EventActionPayload,
type GenericLog,
type BoostPayload as OnChainBoostPayload,
Expand Down Expand Up @@ -1118,6 +1123,28 @@ export class BoostCore extends Deployable<
isBase,
);
}
/**
* Bound {@link ERC20VariableIncentive} constructor that reuses the same configuration as the Boost Core instance.
*
* @example
* ```ts
* const validator = core.ERC20VariableIncentive({ ... }) // is roughly equivalent to
* const validator = new ERC20VariableIncentive({ config: core._config, account: core._account }, { ... })
* ```
* @param {DeployablePayloadOrAddress<ERC20VariableIncentivePayload>} options
* @param {?boolean} [isBase]
* @returns {ERC20VariableIncentive}
*/
ERC20VariableIncentive(
options: DeployablePayloadOrAddress<ERC20VariableIncentivePayload>,
isBase?: boolean,
) {
return new ERC20VariableIncentive(
{ config: this._config, account: this._account },
options,
isBase,
);
}

/**
* @inheritdoc
Expand Down
Loading

0 comments on commit a0af829

Please sign in to comment.