Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/world id start earning #93

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"MSMART"
]
}
46 changes: 45 additions & 1 deletion src/EarnerManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Migratable } from "../lib/common/src/Migratable.sol";

import { IEarnerManager } from "./interfaces/IEarnerManager.sol";
import { IRegistrarLike } from "./interfaces/IRegistrarLike.sol";
import { IWorldIDRouterLike } from "./interfaces/IWorldIDRouterLike.sol";

/**
* @title Earner Manager allows admins to define earners without governance, and take fees from yield.
Expand Down Expand Up @@ -36,15 +37,24 @@ contract EarnerManager is IEarnerManager, Migratable {
/// @inheritdoc IEarnerManager
bytes32 public constant MIGRATOR_KEY_PREFIX = "em_migrator_v1";

/// @inheritdoc IEarnerManager
uint256 public constant WORLD_ID_APPROVE_EARNING_EXTERNAL_NULLIFIER_HASH = 0x0; // TODO: Set to hash of `app_id` and `action`.

/// @inheritdoc IEarnerManager
address public immutable registrar;

/// @inheritdoc IEarnerManager
address public immutable migrationAdmin;

/// @inheritdoc IEarnerManager
address public immutable worldIDRouter;

/// @dev Mapping of account to earner details.
mapping(address account => EarnerDetails earnerDetails) internal _earnerDetails;

/// @inheritdoc IEarnerManager
mapping(uint256 nullifierHash => bool isUsed) public isNullifierHashUsed;

/* ============ Modifiers ============ */

modifier onlyAdmin() {
Expand All @@ -59,13 +69,47 @@ contract EarnerManager is IEarnerManager, Migratable {
* @param registrar_ The address of a Registrar contract.
* @param migrationAdmin_ The address of a migration admin.
*/
constructor(address registrar_, address migrationAdmin_) {
constructor(address registrar_, address worldIDRouter_, address migrationAdmin_) {
if ((registrar = registrar_) == address(0)) revert ZeroRegistrar();
if ((worldIDRouter = worldIDRouter_) == address(0)) revert ZeroWorldIDRouter();
if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin();
}

/* ============ Interactive Functions ============ */

/// @inheritdoc IEarnerManager
function approveEarning(address account_, bytes calldata authorizationData_) external {
(
uint256 root_,
uint256 groupId_,
uint256 signalHash_,
uint256 nullifierHash_,
uint256 externalNullifierHash_,
uint256[8] memory proof_
) = abi.decode(authorizationData_, (uint256, uint256, uint256, uint256, uint256, uint256[8]));

if (externalNullifierHash_ != WORLD_ID_APPROVE_EARNING_EXTERNAL_NULLIFIER_HASH) {
revert InvalidAuthorizationData();
}

if (uint256(keccak256(abi.encode(account_))) != signalHash_) revert InvalidAuthorizationData();

if (isNullifierHashUsed[nullifierHash_]) revert InvalidAuthorizationData();

isNullifierHashUsed[nullifierHash_] = true;

IWorldIDRouterLike(worldIDRouter).verifyProof(
root_,
groupId_,
signalHash_,
nullifierHash_,
externalNullifierHash_,
proof_
);

_setDetails(account_, true, 0);
}

/// @inheritdoc IEarnerManager
function setEarnerDetails(address account_, bool status_, uint16 feeRate_) external onlyAdmin {
if (earnersListsIgnored()) revert EarnersListsIgnored();
Expand Down
18 changes: 13 additions & 5 deletions src/SmartMToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ contract SmartMToken is ISmartMToken, Migratable, ERC20Extended {

/**
* @dev Struct to represent an account's balance and yield earning details
* @param isEarning Whether the account is actively earning yield.
* @param balance The present amount of tokens held by the account.
* @param lastIndex The index of the last interaction for the account (0 for non-earning accounts).
* @param hasEarnerDetails Whether the account has additional details for earning yield.
* @param isEarning Whether the account is actively earning yield.
* @param balance The present amount of tokens held by the account.
* @param lastIndex The index of the last interaction for the account (0 for non-earning accounts).
* @param hasEarnerDetails Whether the account has additional details for earning yield.
* @param hasClaimRecipient Whether the account has an explicitly set claim recipient.
*/
struct Account {
// First Slot
Expand Down Expand Up @@ -217,7 +218,14 @@ contract SmartMToken is ISmartMToken, Migratable, ERC20Extended {
}

/// @inheritdoc ISmartMToken
function startEarningFor(address account_) external {
function startEarningFor(bytes calldata data_) external {
IEarnerManager(earnerManager).approveEarning(msg.sender, data_);

startEarningFor(msg.sender);
}

/// @inheritdoc ISmartMToken
function startEarningFor(address account_) public {
if (!isEarningEnabled()) revert EarningIsDisabled();

// NOTE: Use `currentIndex()` if/when upgrading to support `startEarningFor` while earning is disabled.
Expand Down
29 changes: 29 additions & 0 deletions src/interfaces/IEarnerManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ interface IEarnerManager is IMigratable {
/// @notice Emitted when the fee rate provided is to high (higher than 100% in basis points).
error FeeRateTooHigh();

error InvalidAuthorizationData();

/// @notice Emitted when setting fee rate to a nonzero value while setting status to false.
error InvalidDetails();

Expand All @@ -58,10 +60,22 @@ interface IEarnerManager is IMigratable {
/// @notice Emitted in constructor if Registrar is 0x0.
error ZeroRegistrar();

/// @notice Emitted in constructor if World ID Router is 0x0.
error ZeroWorldIDRouter();

/* ============ Interactive Functions ============ */

/**
* @notice Approves earning for `account` given some encoded authorization data.
* @param account The account under which yield should generate.
* @param authorizationData Encoded data to be used for authorization.
*/
function approveEarning(address account, bytes calldata authorizationData) external;

/**
* @notice Sets the status for `account` to `status`.
* @notice If approving an earner that is already earning, but was recently removed from the Registrar earners list,
* call `smartM.stopEarning(account)` before calling this, then call `smartM.startEarning(account)`.
* @param account The account under which yield could generate.
* @param status Whether the account is an earner, according to the admin.
* @param feeRate The fee rate to be taken from the yield.
Expand All @@ -70,6 +84,8 @@ interface IEarnerManager is IMigratable {

/**
* @notice Sets the status for multiple accounts.
* @notice If approving an earner that is already earning, but was recently removed from the Registrar earners list,
* call `smartM.stopEarning(account)` before calling this, then call `smartM.startEarning(account)`.
* @param accounts The accounts under which yield could generate.
* @param statuses Whether each account is an earner, respectively, according to the admin.
* @param feeRates The fee rates to be taken from the yield, respectively.
Expand Down Expand Up @@ -105,6 +121,9 @@ interface IEarnerManager is IMigratable {
/// @notice Registrar key prefix to determine the migrator contract.
function MIGRATOR_KEY_PREFIX() external pure returns (bytes32 migratorKeyPrefix);

/// @notice World ID external nullifier hash for an `approveEarning` call.
function WORLD_ID_APPROVE_EARNING_EXTERNAL_NULLIFIER_HASH() external pure returns (uint256 externalNullifierHash);

/**
* @notice Returns the earner status for `account`.
* @param account The account being queried.
Expand Down Expand Up @@ -166,9 +185,19 @@ interface IEarnerManager is IMigratable {
*/
function isAdmin(address account) external view returns (bool isAdmin);

/**
* @notice Returns whether a nullifier hash was already used.
* @param nullifierHash Some nullifier hash.
* @return isUsed Whether the nullifier hash was already used.
*/
function isNullifierHashUsed(uint256 nullifierHash) external view returns (bool isUsed);

/// @notice The account that can bypass the Registrar and call the `migrate(address migrator)` function.
function migrationAdmin() external view returns (address migrationAdmin);

/// @notice Returns the address of the Registrar.
function registrar() external view returns (address);

/// @notice Returns the address of the World ID Router.
function worldIDRouter() external view returns (address);
}
18 changes: 12 additions & 6 deletions src/interfaces/ISmartMToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ interface ISmartMToken is IMigratable, IERC20Extended {
error EarningCannotBeReenabled();

/**
* @notice Emitted when calling `stopEarning` for an account approved as earner by the Registrar.
* @notice Emitted when calling `mToken.stopEarning` for an account approved as an earner.
* @param account The account that is an approved earner.
*/
error IsApprovedEarner(address account);
Expand All @@ -83,7 +83,7 @@ interface ISmartMToken is IMigratable, IERC20Extended {
error InsufficientBalance(address account, uint240 balance, uint240 amount);

/**
* @notice Emitted when calling `startEarning` for an account not approved as earner by the Registrar.
* @notice Emitted when calling `mToken.startEarning` for an account not approved as an.
* @param account The account that is not an approved earner.
*/
error NotApprovedEarner(address account);
Expand Down Expand Up @@ -195,25 +195,31 @@ interface ISmartMToken is IMigratable, IERC20Extended {
function disableEarning() external;

/**
* @notice Starts earning for `account` if allowed by the Registrar.
* @notice Starts earning for caller if allowed by the Earner Manager, given data.
* @param data Some data used by the Earner Manager to authorize earning.
*/
function startEarningFor(bytes calldata data) external;

/**
* @notice Starts earning for `account` if allowed by the Earner Manager.
* @param account The account to start earning for.
*/
function startEarningFor(address account) external;

/**
* @notice Starts earning for multiple accounts if individually allowed by the Registrar.
* @notice Starts earning for multiple accounts if individually allowed by the Earner Manager.
* @param accounts The accounts to start earning for.
*/
function startEarningFor(address[] calldata accounts) external;

/**
* @notice Stops earning for `account` if disallowed by the Registrar.
* @notice Stops earning for `account` if disallowed by the Earner Manager.
* @param account The account to stop earning for.
*/
function stopEarningFor(address account) external;

/**
* @notice Stops earning for multiple accounts if individually disallowed by the Registrar.
* @notice Stops earning for multiple accounts if individually disallowed by the Earner Manager.
* @param accounts The account to stop earning for.
*/
function stopEarningFor(address[] calldata accounts) external;
Expand Down
18 changes: 18 additions & 0 deletions src/interfaces/IWorldIDRouterLike.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.26;

/**
* @title Subset of World Id Router v1 interface required for source contracts.
* @author M^0 Labs
*/
interface IWorldIDRouterLike {
function verifyProof(
uint256 root,
uint256 groupId,
uint256 signalHash,
uint256 nullifierHash,
uint256 externalNullifierHash,
uint256[8] calldata proof
) external view;
}
2 changes: 1 addition & 1 deletion test/unit/EarnerManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IEarnerManager } from "../../src/interfaces/IEarnerManager.sol";
import { MockRegistrar } from "./../utils/Mocks.sol";
import { EarnerManagerHarness } from "../utils/EarnerManagerHarness.sol";

contract EarnerStatusManagerTests is Test {
contract EarnerManagerTests is Test {
bytes32 internal constant _EARNERS_LIST_IGNORED_KEY = "earners_list_ignored";
bytes32 internal constant _EARNERS_LIST_NAME = "earners";
bytes32 internal constant _ADMINS_LIST_NAME = "em_admins";
Expand Down
Loading
Loading