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

Staking and trading zaps with onboarding via signature #152

Open
wants to merge 31 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cbf2dce
feat: build onboarding approval hash
amarinkovic Oct 14, 2024
e5a5af4
feat: add onboarding via signature verification
amarinkovic Oct 16, 2024
0aa3a09
chore: rename sig onboarding methods
amarinkovic Oct 21, 2024
947877a
refactor: rename sig helper method
amarinkovic Oct 21, 2024
23e2a36
test: fix failing tests
amarinkovic Oct 21, 2024
15848c2
chore: remove unused errors
amarinkovic Oct 22, 2024
53f3918
doc: add natspec for new onboarding methods
amarinkovic Oct 23, 2024
4f8ea2c
chore: bump version to v3.9.4
amarinkovic Oct 23, 2024
3b4f057
chore: minor clean up
amarinkovic Oct 23, 2024
0806d0e
fix: emit event when onboarding via signature
amarinkovic Oct 25, 2024
37ebdc8
fix: remove whitespace from sig struct description
amarinkovic Oct 30, 2024
a1fb4da
chore: remove old approval implementation
amarinkovic Nov 1, 2024
927c3f2
fix: don't assign capital provider role in self context
amarinkovic Nov 8, 2024
6a507c5
feat: zap
kevin-fruitful Oct 16, 2024
2189bff
fix: zapUnstake
kevin-fruitful Oct 17, 2024
fa3629e
fix: rename to ZapFacet
kevin-fruitful Oct 17, 2024
5bfcedc
feat: zap execute limit offer
kevin-fruitful Oct 21, 2024
e60be8e
refactor: remove zapUnstake
kevin-fruitful Oct 23, 2024
6c39b9c
fix: zaps require calling erc20 permit functions
kevin-fruitful Oct 28, 2024
4718700
refactor: rename param to parentId
kevin-fruitful Oct 28, 2024
5bc0a6f
refactor: move PermitSignature struct into FreeStructs.sol
kevin-fruitful Oct 28, 2024
aad86b8
test: update test erc20 with permit method and related variables
kevin-fruitful Oct 28, 2024
0dfed52
refactor: rename param to _depositAmount
kevin-fruitful Oct 28, 2024
9482efe
test: add tests for zaps
kevin-fruitful Oct 28, 2024
a563b90
test: fix failing transfer tests
amarinkovic Nov 5, 2024
2103316
test: assert erc20 address requirement
amarinkovic Nov 5, 2024
bd4d6ce
feat: combine zaps with onboarding approval
amarinkovic Nov 6, 2024
e4a0b0a
fix: compilation issues after rebase
amarinkovic Nov 8, 2024
7cf45f3
fix: onboarding trigger condition
amarinkovic Nov 12, 2024
eb2ce90
fix: assert zap priviledge after eventual onboarding
amarinkovic Nov 12, 2024
f8c02f0
refactor: improve zap order testability
amarinkovic Nov 13, 2024
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nayms/contracts",
"version": "3.9.3",
"version": "3.9.4",
"main": "index.js",
"repository": "https://github.com/nayms/contracts-v3.git",
"author": "Kevin Park <[email protected]>",
Expand Down
26 changes: 11 additions & 15 deletions src/facets/AdminFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.20;

import { AppStorage, LibAppStorage } from "../shared/AppStorage.sol";
import { OnboardingApproval } from "../shared/FreeStructs.sol";
import { Modifiers } from "../shared/Modifiers.sol";
import { LibAdmin } from "../libs/LibAdmin.sol";
import { LibObject } from "../libs/LibObject.sol";
Expand Down Expand Up @@ -148,25 +149,20 @@ contract AdminFacet is Modifiers {
}

/**
* @notice Approve a user address for self-onboarding
* @param _userAddress user account address
* @notice Create a token holder entity for a user account
* @param _onboardingApproval onboarding approval parameters, includes user address, entity ID and role ID
*/
function approveSelfOnboarding(address _userAddress, bytes32 _entityId, bytes32 _roleId) external assertPrivilege(LibAdmin._getSystemId(), LC.GROUP_ONBOARDING_APPROVERS) {
LibAdmin._approveSelfOnboarding(_userAddress, _entityId, _roleId);
function onboardViaSignature(OnboardingApproval calldata _onboardingApproval) external {
LibAdmin._onboardUserViaSignature(_onboardingApproval);
}

/**
* @notice Create a token holder entity for a user account
* @notice Hash to be signed by the onboarding approver
* @param _userAddress Address being approved to onboard
* @param _entityId Entity ID being approved for onboarding
* @param _roleId Role being apprved for onboarding
*/
function onboard() external {
LibAdmin._onboardUser(msg.sender);
}

function isSelfOnboardingApproved(address _userAddress, bytes32 _entityId, bytes32 _roleId) external view returns (bool) {
return LibAdmin._isSelfOnboardingApproved(_userAddress, _entityId, _roleId);
}

function cancelSelfOnboarding(address _user) external assertPrivilege(LibAdmin._getSystemId(), LC.GROUP_SYSTEM_MANAGERS) {
LibAdmin._cancelSelfOnboarding(_user);
function getOnboardingHash(address _userAddress, bytes32 _entityId, bytes32 _roleId) external view returns (bytes32) {
return LibAdmin._getOnboardingHash(_userAddress, _entityId, _roleId);
}
}
100 changes: 100 additions & 0 deletions src/facets/ZapFacet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import { PermitSignature, OnboardingApproval } from "../shared/FreeStructs.sol";
import { Modifiers } from "../shared/Modifiers.sol";
import { LibTokenizedVaultIO } from "../libs/LibTokenizedVaultIO.sol";
import { LibACL } from "../libs/LibACL.sol";
import { LibAdmin } from "../libs/LibAdmin.sol";
import { LibObject } from "../libs/LibObject.sol";
import { LibHelpers } from "../libs/LibHelpers.sol";
import { LibConstants as LC } from "../libs/LibConstants.sol";
import { ReentrancyGuard } from "../utils/ReentrancyGuard.sol";
import { LibTokenizedVaultStaking } from "../libs/LibTokenizedVaultStaking.sol";
import { IERC20 } from "../interfaces/IERC20.sol";
import { LibMarket } from "../libs/LibMarket.sol";

contract ZapFacet is Modifiers, ReentrancyGuard {
/**
* @notice Deposit and stake funds into msg.sender's Nayms platform entity in one transaction using permit
* @dev Uses permit to approve token transfer, deposits from msg.sender to their associated entity, and stakes the amount
* @param _externalTokenAddress Token address
* @param _stakingEntityId Staking entity ID
* @param _amountToDeposit Deposit amount
* @param _amountToStake Stake amount
* @param _permitSignature The permit signature parameters
* @param _onboardingApproval The onboarding approval parameters
*/
function zapStake(
address _externalTokenAddress,
bytes32 _stakingEntityId,
uint256 _amountToDeposit,
uint256 _amountToStake,
PermitSignature calldata _permitSignature,
OnboardingApproval calldata _onboardingApproval
) external notLocked nonReentrant {
// Check if it's a supported ERC20 token
require(LibAdmin._isSupportedExternalTokenAddress(_externalTokenAddress), "zapStake: invalid ERC20 token");

if (_onboardingApproval.entityId != 0 && LibObject._getParentFromAddress(msg.sender) != _onboardingApproval.entityId) {
LibAdmin._onboardUserViaSignature(_onboardingApproval);
}

bytes32 parentId = LibObject._getParentFromAddress(msg.sender);

// Use permit to set allowance
IERC20(_externalTokenAddress).permit(msg.sender, address(this), _amountToDeposit, _permitSignature.deadline, _permitSignature.v, _permitSignature.r, _permitSignature.s);

// Perform the deposit
LibTokenizedVaultIO._externalDeposit(parentId, _externalTokenAddress, _amountToDeposit);

// Stake the deposited amount
LibTokenizedVaultStaking._stake(parentId, _stakingEntityId, _amountToStake);
}

/**
* @notice Deposit tokens and execute a limit order in one transaction using permit
* @dev Uses permit to approve token transfer and performs external deposit and limit order execution
* @param _externalTokenAddress Token address
* @param _depositAmount Amount to deposit
* @param _sellToken Sell token ID
* @param _sellAmount Sell amount
* @param _buyToken Buy token ID
* @param _buyAmount Buy amount
* @param _permitSignature The permit signature parameters
* @param _onboardingApproval The onboarding approval parameters
*/
function zapOrder(
address _externalTokenAddress,
uint256 _depositAmount,
bytes32 _sellToken,
uint256 _sellAmount,
bytes32 _buyToken,
uint256 _buyAmount,
PermitSignature calldata _permitSignature,
OnboardingApproval calldata _onboardingApproval
) external notLocked nonReentrant {
// Check if it's a supported ERC20 token
require(LibAdmin._isSupportedExternalTokenAddress(_externalTokenAddress), "zapOrder: invalid ERC20 token");

bytes32 parentId = _onboardingApproval.entityId;

bool isOnboardingCP = _onboardingApproval.roleId == LibHelpers._stringToBytes32(LC.ROLE_CAPITAL_PROVIDER);
bool isCurrentlyCP = LibACL._isInGroup(parentId, LibHelpers._stringToBytes32(LC.SYSTEM_IDENTIFIER), LibHelpers._stringToBytes32(LC.GROUP_CAPITAL_PROVIDERS));

if (!isCurrentlyCP && isOnboardingCP) {
LibAdmin._onboardUserViaSignature(_onboardingApproval);
}

LibACL._assertPriviledge(parentId, LC.GROUP_EXECUTE_LIMIT_OFFER);

// Use permit to set allowance
IERC20(_externalTokenAddress).permit(msg.sender, address(this), _depositAmount, _permitSignature.deadline, _permitSignature.v, _permitSignature.r, _permitSignature.s);

// Perform the external deposit
LibTokenizedVaultIO._externalDeposit(parentId, _externalTokenAddress, _depositAmount);

// Execute the limit order
LibMarket._executeLimitOffer(parentId, _sellToken, _sellAmount, _buyToken, _buyAmount, LC.FEE_TYPE_TRADING);
}
}
20 changes: 18 additions & 2 deletions src/libs/LibACL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import { LibHelpers } from "./LibHelpers.sol";
import { LibAdmin } from "./LibAdmin.sol";
import { LibObject } from "./LibObject.sol";
import { LibConstants } from "./LibConstants.sol";
import { CannotUnassignRoleFromSelf, OwnerCannotBeSystemAdmin, RoleIsMissing, AssignerGroupIsMissing } from "../shared/CustomErrors.sol";

import { CannotUnassignRoleFromSelf, OwnerCannotBeSystemAdmin, RoleIsMissing, AssignerGroupIsMissing, InvalidGroupPrivilege } from "../shared/CustomErrors.sol";

import { LibString } from "solady/utils/LibString.sol";

library LibACL {
using LibHelpers for bytes32;
using LibString for *;
using LibHelpers for *;

/**
* @dev Emitted when a role gets updated. Empty roleId is assigned upon role removal
Expand Down Expand Up @@ -129,6 +133,18 @@ library LibACL {
return false;
}

function _assertPriviledge(bytes32 _context, string memory _group) internal view {
if (!_hasGroupPrivilege(LibHelpers._getIdForAddress(msg.sender), _context, LibHelpers._stringToBytes32(_group)))
/// Note: If the role returned by `_getRoleInContext` is empty (represented by bytes32(0)), we explicitly return an empty string.
/// This ensures the user doesn't receive a string that could potentially include unwanted data (like pointer and length) without any meaningful content.
revert InvalidGroupPrivilege(
msg.sender._getIdForAddress(),
_context,
(_getRoleInContext(msg.sender._getIdForAddress(), _context) == bytes32(0)) ? "" : _getRoleInContext(msg.sender._getIdForAddress(), _context).fromSmallString(),
_group
);
}

function _getRoleInContext(bytes32 _objectId, bytes32 _contextId) internal view returns (bytes32) {
AppStorage storage s = LibAppStorage.diamondStorage();
return s.roles[_objectId][_contextId];
Expand Down
128 changes: 69 additions & 59 deletions src/libs/LibAdmin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,40 @@
pragma solidity 0.8.20;

import { AppStorage, FunctionLockedStorage, LibAppStorage } from "../shared/AppStorage.sol";
import { Entity, EntityApproval } from "../shared/FreeStructs.sol";
import { Entity, OnboardingApproval } from "../shared/FreeStructs.sol";
import { LibConstants as LC } from "./LibConstants.sol";
import { LibHelpers } from "./LibHelpers.sol";
import { LibObject } from "./LibObject.sol";
import { LibERC20 } from "./LibERC20.sol";
import { LibEntity } from "./LibEntity.sol";
import { LibACL } from "./LibACL.sol";
import { LibEIP712 } from "./LibEIP712.sol";

import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

// prettier-ignore
import {
CannotAddNullDiscountToken,
CannotAddNullSupportedExternalToken,
CannotSupportExternalTokenWithMoreThan18Decimals,
ObjectTokenSymbolAlreadyInUse,
MinimumSellCannotBeZero,
EntityExistsAlready,
EntityOnboardingAlreadyApproved,
EntityOnboardingNotApproved,
InvalidSelfOnboardRoleApproval,
InvalidEntityId
InvalidSignatureError,
InvalidSignatureSError
} from "../shared/CustomErrors.sol";

import { IDiamondProxy } from "src/generated/IDiamondProxy.sol";

library LibAdmin {
using ECDSA for bytes32;

event MaxDividendDenominationsUpdated(uint8 oldMax, uint8 newMax);
event SupportedTokenAdded(address indexed tokenAddress);
event FunctionsLocked(bytes4[] functionSelectors);
event FunctionsUnlocked(bytes4[] functionSelectors);
event ObjectMinimumSellUpdated(bytes32 objectId, uint256 newMinimumSell);
event SelfOnboardingApproved(address indexed userAddress);
event SelfOnboardingCompleted(address indexed userAddress);
event SelfOnboardingCancelled(address indexed userAddress);

/// @notice The minimum amount of an object (par token, external token) that can be sold on the market
event MinimumSellUpdated(bytes32 objectId, uint256 minimumSell);
Expand Down Expand Up @@ -226,83 +228,91 @@ library LibAdmin {
emit FunctionsUnlocked(lockedFunctions);
}

function _approveSelfOnboarding(address _userAddress, bytes32 _entityId, bytes32 _roleId) internal {
function _onboardUserViaSignature(OnboardingApproval memory _approval) internal {
AppStorage storage s = LibAppStorage.diamondStorage();

// The entityId must be the valid type (entity).
if (!LibObject._isObjectType(_entityId, LC.OBJECT_TYPE_ENTITY)) revert InvalidEntityId(_entityId);
address userAddress = msg.sender;

bytes32 entityId = _approval.entityId;
bytes32 roleId = _approval.roleId;
bytes memory sig = _approval.signature;

// Require that the user is not approved for the role already
if (_isSelfOnboardingApproved(_userAddress, _entityId, _roleId)) revert EntityOnboardingAlreadyApproved(_userAddress);
if (entityId == 0 || roleId == 0 || sig.length == 0) revert EntityOnboardingNotApproved(userAddress);

bool isTokenHolder = _roleId == LibHelpers._stringToBytes32(LC.ROLE_ENTITY_TOKEN_HOLDER);
bool isCapitalProvider = _roleId == LibHelpers._stringToBytes32(LC.ROLE_ENTITY_CP);
bool isTokenHolder = roleId == LibHelpers._stringToBytes32(LC.ROLE_ENTITY_TOKEN_HOLDER);
bool isCapitalProvider = roleId == LibHelpers._stringToBytes32(LC.ROLE_ENTITY_CP);
if (!isTokenHolder && !isCapitalProvider) {
revert InvalidSelfOnboardRoleApproval(_roleId);
revert InvalidSelfOnboardRoleApproval(roleId);
}

s.selfOnboarding[_userAddress] = EntityApproval({ entityId: _entityId, roleId: _roleId });

emit SelfOnboardingApproved(_userAddress);
}

function _onboardUser(address _userAddress) internal {
AppStorage storage s = LibAppStorage.diamondStorage();
EntityApproval memory approval = s.selfOnboarding[_userAddress];
bytes32 signingHash = _getOnboardingHash(userAddress, entityId, roleId);
bytes32 signerId = LibHelpers._getIdForAddress(_getSigner(signingHash, sig));

if (approval.entityId == 0 || approval.roleId == 0) {
revert EntityOnboardingNotApproved(_userAddress);
if (!LibACL._isInGroup(signerId, LibAdmin._getSystemId(), LibHelpers._stringToBytes32(LC.GROUP_ONBOARDING_APPROVERS))) {
revert EntityOnboardingNotApproved(userAddress);
}

bytes32 userId = LibHelpers._getIdForAddress(_userAddress);

if (!s.existingEntities[approval.entityId]) {
if (!s.existingEntities[entityId]) {
Entity memory entity;
LibEntity._createEntity(approval.entityId, userId, entity, 0);
}

if (s.roles[approval.entityId][approval.entityId] != 0) {
LibACL._unassignRole(approval.entityId, approval.entityId);
bytes32 userId = LibHelpers._getIdForAddress(userAddress);
LibEntity._createEntity(entityId, userId, entity, 0);
}

if (s.roles[approval.entityId][LibAdmin._getSystemId()] != 0) {
LibACL._unassignRole(approval.entityId, LibAdmin._getSystemId());
if (s.roles[entityId][LibAdmin._getSystemId()] != 0) {
LibACL._unassignRole(entityId, LibAdmin._getSystemId());
}
LibACL._assignRole(entityId, LibAdmin._getSystemId(), roleId);

LibACL._assignRole(approval.entityId, LibAdmin._getSystemId(), approval.roleId);
LibACL._assignRole(approval.entityId, approval.entityId, approval.roleId);

delete s.selfOnboarding[_userAddress];

emit SelfOnboardingCompleted(_userAddress);
emit SelfOnboardingCompleted(userAddress);
}

function _isSelfOnboardingApproved(address _userAddress, bytes32 _entityId, bytes32 _roleId) internal view returns (bool) {
function _setMinimumSell(bytes32 _objectId, uint256 _minimumSell) internal {
AppStorage storage s = LibAppStorage.diamondStorage();
if (_minimumSell == 0) revert MinimumSellCannotBeZero();

EntityApproval memory approval = s.selfOnboarding[_userAddress];
s.objectMinimumSell[_objectId] = _minimumSell;

return approval.entityId == _entityId && approval.roleId == _roleId;
emit MinimumSellUpdated(_objectId, _minimumSell);
}

function _cancelSelfOnboarding(address _userAddress) internal {
AppStorage storage s = LibAppStorage.diamondStorage();
function _getOnboardingHash(address _userAddress, bytes32 _entityId, bytes32 _roleId) internal view returns (bytes32) {
return
LibEIP712._hashTypedDataV4(
keccak256(abi.encode(keccak256("OnboardingApproval(address _userAddress,bytes32 _entityId,bytes32 _roleId)"), _userAddress, _entityId, _roleId))
);
}

if (s.selfOnboarding[_userAddress].entityId == 0 && s.selfOnboarding[_userAddress].roleId == 0) {
revert EntityOnboardingNotApproved(_userAddress);
function _getSigner(bytes32 signingHash, bytes memory signature) internal pure returns (address) {
bytes32 r;
bytes32 s;
uint8 v;

// ecrecover takes the signature parameters, and the only way to get them
if (signature.length == 65) {
// currently is to use assembly.
/// @solidity memory-safe-assembly
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))

switch v
// if v == 0, then v = 27
case 0 {
v := 27
}
// if v == 1, then v = 28
case 1 {
v := 28
}
}
}

delete s.selfOnboarding[_userAddress];
(address signer, ECDSA.RecoverError err, ) = ECDSA.tryRecover(MessageHashUtils.toEthSignedMessageHash(signingHash), v, r, s);

emit SelfOnboardingCancelled(_userAddress);
}
if (err == ECDSA.RecoverError.InvalidSignature) revert InvalidSignatureError(signingHash);
else if (err == ECDSA.RecoverError.InvalidSignatureS) revert InvalidSignatureSError(s);

function _setMinimumSell(bytes32 _objectId, uint256 _minimumSell) internal {
AppStorage storage s = LibAppStorage.diamondStorage();
if (_minimumSell == 0) revert MinimumSellCannotBeZero();

s.objectMinimumSell[_objectId] = _minimumSell;

emit MinimumSellUpdated(_objectId, _minimumSell);
return signer;
}
}
Loading
Loading