From bba825d2c8e1ac010ad5437f5e4cced634e63962 Mon Sep 17 00:00:00 2001 From: Aleksandar Marinkovic Date: Wed, 6 Nov 2024 13:35:24 +0100 Subject: [PATCH] feat: combine zaps with onboarding approval --- src/facets/AdminFacet.sol | 9 ++++---- src/facets/ZapFacet.sol | 32 +++++++++++++++++------------ src/libs/LibAdmin.sol | 42 ++++++++++++++++++++++---------------- src/shared/FreeStructs.sol | 7 +++++++ test/T04Entity.t.sol | 20 +++++++++--------- test/T07Zaps.t.sol | 13 +++++++----- 6 files changed, 72 insertions(+), 51 deletions(-) diff --git a/src/facets/AdminFacet.sol b/src/facets/AdminFacet.sol index 2399dad6..29fba217 100644 --- a/src/facets/AdminFacet.sol +++ b/src/facets/AdminFacet.sol @@ -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"; @@ -149,12 +150,10 @@ contract AdminFacet is Modifiers { /** * @notice Create a token holder entity for a user account - * @param _entityId object ID for which the fee schedule is being set, use system ID for global fee schedule - * @param _roleId Role to assign to the entity - * @param _sig Signature approving the user to be onboarded to the given role + * @param _onboardingApproval onboarding approval parameters, includes user address, entity ID and role ID */ - function onboardViaSignature(bytes32 _entityId, bytes32 _roleId, bytes calldata _sig) external { - LibAdmin._onboardUserViaSignature(msg.sender, _entityId, _roleId, _sig); + function onboardViaSignature(OnboardingApproval calldata _onboardingApproval) external { + LibAdmin._onboardUserViaSignature(_onboardingApproval); } /** diff --git a/src/facets/ZapFacet.sol b/src/facets/ZapFacet.sol index 122a7260..b7c487ec 100644 --- a/src/facets/ZapFacet.sol +++ b/src/facets/ZapFacet.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.20; -import { PermitSignature } from "../shared/FreeStructs.sol"; +import { PermitSignature, OnboardingApproval } from "../shared/FreeStructs.sol"; import { Modifiers } from "../shared/Modifiers.sol"; import { LibTokenizedVaultIO } from "../libs/LibTokenizedVaultIO.sol"; import { LibEntity } from "../libs/LibEntity.sol"; @@ -18,24 +18,28 @@ 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 _entityId Staking entity ID + * @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 _entityId, + bytes32 _stakingEntityId, uint256 _amountToDeposit, uint256 _amountToStake, - PermitSignature calldata _permitSignature + 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"); - // Get the user's entity bytes32 parentId = LibObject._getParentFromAddress(msg.sender); - require(LibEntity._isEntity(parentId), "zapStake: invalid receiver"); + + if (!LibEntity._isEntity(parentId)) { + LibAdmin._onboardUserViaSignature(_onboardingApproval); + } // Use permit to set allowance IERC20(_externalTokenAddress).permit(msg.sender, address(this), _amountToDeposit, _permitSignature.deadline, _permitSignature.v, _permitSignature.r, _permitSignature.s); @@ -44,7 +48,7 @@ contract ZapFacet is Modifiers, ReentrancyGuard { LibTokenizedVaultIO._externalDeposit(parentId, _externalTokenAddress, _amountToDeposit); // Stake the deposited amount - LibTokenizedVaultStaking._stake(parentId, _entityId, _amountToStake); + LibTokenizedVaultStaking._stake(parentId, _stakingEntityId, _amountToStake); } /** @@ -57,6 +61,7 @@ contract ZapFacet is Modifiers, ReentrancyGuard { * @param _buyToken Buy token ID * @param _buyAmount Buy amount * @param _permitSignature The permit signature parameters + * @param _onboardingApproval The onboarding approval parameters * @return offerId_ The ID of the created offer * @return buyTokenCommissionsPaid_ Commissions paid in buy token * @return sellTokenCommissionsPaid_ Commissions paid in sell token @@ -68,7 +73,8 @@ contract ZapFacet is Modifiers, ReentrancyGuard { uint256 _sellAmount, bytes32 _buyToken, uint256 _buyAmount, - PermitSignature calldata _permitSignature + PermitSignature calldata _permitSignature, + OnboardingApproval calldata _onboardingApproval ) external notLocked @@ -79,17 +85,17 @@ contract ZapFacet is Modifiers, ReentrancyGuard { // Check if it's a supported ERC20 token require(LibAdmin._isSupportedExternalTokenAddress(_externalTokenAddress), "zapOrder: invalid ERC20 token"); - // Get the user's entity - bytes32 parentId = LibObject._getParentFromAddress(msg.sender); - require(LibEntity._isEntity(parentId), "zapOrder: invalid entity"); + if (!LibEntity._isEntity(LibObject._getParentFromAddress(msg.sender))) { + LibAdmin._onboardUserViaSignature(_onboardingApproval); + } // 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); + LibTokenizedVaultIO._externalDeposit(LibObject._getParentFromAddress(msg.sender), _externalTokenAddress, _depositAmount); // Execute the limit order - return LibMarket._executeLimitOffer(parentId, _sellToken, _sellAmount, _buyToken, _buyAmount, LC.FEE_TYPE_TRADING); + return LibMarket._executeLimitOffer(LibObject._getParentFromAddress(msg.sender), _sellToken, _sellAmount, _buyToken, _buyAmount, LC.FEE_TYPE_TRADING); } } diff --git a/src/libs/LibAdmin.sol b/src/libs/LibAdmin.sol index abc7e340..fcdd6be8 100644 --- a/src/libs/LibAdmin.sol +++ b/src/libs/LibAdmin.sol @@ -2,7 +2,7 @@ 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"; @@ -228,42 +228,48 @@ library LibAdmin { emit FunctionsUnlocked(lockedFunctions); } - function _onboardUserViaSignature(address _userAddress, bytes32 _entityId, bytes32 _roleId, bytes calldata sig) internal { + function _onboardUserViaSignature(OnboardingApproval memory _approval) internal { AppStorage storage s = LibAppStorage.diamondStorage(); - if (_entityId == 0 || _roleId == 0 || sig.length == 0) revert EntityOnboardingNotApproved(_userAddress); + address userAddress = msg.sender; - bool isTokenHolder = _roleId == LibHelpers._stringToBytes32(LC.ROLE_ENTITY_TOKEN_HOLDER); - bool isCapitalProvider = _roleId == LibHelpers._stringToBytes32(LC.ROLE_ENTITY_CP); + bytes32 entityId = _approval.entityId; + bytes32 roleId = _approval.roleId; + bytes memory sig = _approval.signature; + + 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); if (!isTokenHolder && !isCapitalProvider) { - revert InvalidSelfOnboardRoleApproval(_roleId); + revert InvalidSelfOnboardRoleApproval(roleId); } - bytes32 signingHash = _getOnboardingHash(_userAddress, _entityId, _roleId); + bytes32 signingHash = _getOnboardingHash(userAddress, entityId, roleId); bytes32 signerId = LibHelpers._getIdForAddress(_getSigner(signingHash, sig)); if (!LibACL._isInGroup(signerId, LibAdmin._getSystemId(), LibHelpers._stringToBytes32(LC.GROUP_ONBOARDING_APPROVERS))) { - revert EntityOnboardingNotApproved(_userAddress); + revert EntityOnboardingNotApproved(userAddress); } - if (!s.existingEntities[_entityId]) { + if (!s.existingEntities[entityId]) { Entity memory entity; - bytes32 userId = LibHelpers._getIdForAddress(_userAddress); - LibEntity._createEntity(_entityId, userId, entity, 0); + bytes32 userId = LibHelpers._getIdForAddress(userAddress); + LibEntity._createEntity(entityId, userId, entity, 0); } - if (s.roles[_entityId][_entityId] != 0) { - LibACL._unassignRole(_entityId, _entityId); + if (s.roles[entityId][entityId] != 0) { + LibACL._unassignRole(entityId, entityId); } - if (s.roles[_entityId][LibAdmin._getSystemId()] != 0) { - LibACL._unassignRole(_entityId, LibAdmin._getSystemId()); + if (s.roles[entityId][LibAdmin._getSystemId()] != 0) { + LibACL._unassignRole(entityId, LibAdmin._getSystemId()); } - LibACL._assignRole(_entityId, LibAdmin._getSystemId(), _roleId); - LibACL._assignRole(_entityId, _entityId, _roleId); + LibACL._assignRole(entityId, LibAdmin._getSystemId(), roleId); + LibACL._assignRole(entityId, entityId, roleId); - emit SelfOnboardingCompleted(_userAddress); + emit SelfOnboardingCompleted(userAddress); } function _setMinimumSell(bytes32 _objectId, uint256 _minimumSell) internal { diff --git a/src/shared/FreeStructs.sol b/src/shared/FreeStructs.sol index 0fa40700..68f419d3 100644 --- a/src/shared/FreeStructs.sol +++ b/src/shared/FreeStructs.sol @@ -40,6 +40,7 @@ struct Entity { bool simplePolicyEnabled; } +// DEPRECATED, but don't remove, referenced in appstorage struct EntityApproval { bytes32 entityId; bytes32 roleId; @@ -133,3 +134,9 @@ struct PermitSignature { bytes32 r; bytes32 s; } + +struct OnboardingApproval { + bytes32 entityId; + bytes32 roleId; + bytes signature; +} diff --git a/test/T04Entity.t.sol b/test/T04Entity.t.sol index 7ed77c21..053c9289 100644 --- a/test/T04Entity.t.sol +++ b/test/T04Entity.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.20; import { Vm } from "forge-std/Vm.sol"; import { c, D03ProtocolDefaults, LibHelpers, LC } from "./defaults/D03ProtocolDefaults.sol"; -import { Entity, MarketInfo, SimplePolicy, SimplePolicyInfo, Stakeholders } from "src/shared/FreeStructs.sol"; +import { Entity, MarketInfo, SimplePolicy, SimplePolicyInfo, Stakeholders, OnboardingApproval } from "src/shared/FreeStructs.sol"; import { IDiamondCut } from "lib/diamond-2-hardhat/contracts/interfaces/IDiamondCut.sol"; import { StdStyle } from "forge-std/StdStyle.sol"; @@ -1174,15 +1174,15 @@ contract T04EntityTest is D03ProtocolDefaults { vm.startPrank(userAddress); vm.expectRevert(abi.encodeWithSelector(EntityOnboardingNotApproved.selector, userAddress)); - nayms.onboardViaSignature(entityId, roleId, noSig); + nayms.onboardViaSignature(OnboardingApproval({ entityId: entityId, roleId: roleId, signature: noSig })); bytes memory sig = signWithPK(em.pk, nayms.getOnboardingHash(userAddress, entityId, roleId)); vm.expectRevert(abi.encodeWithSelector(EntityOnboardingNotApproved.selector, userAddress)); - nayms.onboardViaSignature(0x0, roleId, sig); + nayms.onboardViaSignature(OnboardingApproval({ entityId: 0x0, roleId: roleId, signature: sig })); vm.expectRevert(abi.encodeWithSelector(EntityOnboardingNotApproved.selector, userAddress)); - nayms.onboardViaSignature(entityId, 0x0, sig); + nayms.onboardViaSignature(OnboardingApproval({ entityId: entityId, roleId: 0x0, signature: sig })); vm.stopPrank(); vm.startPrank(sm.addr); @@ -1191,7 +1191,7 @@ contract T04EntityTest is D03ProtocolDefaults { vm.startPrank(userAddress); vm.expectRevert(abi.encodeWithSelector(EntityOnboardingNotApproved.selector, userAddress)); - nayms.onboardViaSignature(entityId, roleId, sig); + nayms.onboardViaSignature(OnboardingApproval({ entityId: entityId, roleId: roleId, signature: sig })); } function testSelfOnboardingInvalidGroup() public { @@ -1205,7 +1205,7 @@ contract T04EntityTest is D03ProtocolDefaults { vm.startPrank(userAddress); vm.expectRevert(abi.encodeWithSelector(InvalidSelfOnboardRoleApproval.selector, sysMgrRoleId)); - nayms.onboardViaSignature(entityId, sysMgrRoleId, sig); + nayms.onboardViaSignature(OnboardingApproval({ entityId: entityId, roleId: sysMgrRoleId, signature: sig })); vm.stopPrank(); } @@ -1219,7 +1219,7 @@ contract T04EntityTest is D03ProtocolDefaults { bytes memory sig = signWithPK(em.pk, nayms.getOnboardingHash(userAddress, entityId, roleId)); vm.startPrank(userAddress); - nayms.onboardViaSignature(entityId, roleId, sig); + nayms.onboardViaSignature(OnboardingApproval({ entityId: entityId, roleId: roleId, signature: sig })); vm.stopPrank(); assertEq(nayms.getEntity(LibHelpers._getIdForAddress(userAddress)), entityId, "parent should be set"); @@ -1241,11 +1241,11 @@ contract T04EntityTest is D03ProtocolDefaults { bytes memory sigCapitalProvider = signWithPK(em.pk, nayms.getOnboardingHash(userAddress, e1, roleIdCapitalProvider)); vm.startPrank(userAddress); - nayms.onboardViaSignature(e1, roleIdTokenHolder, sigTokenHolder); + nayms.onboardViaSignature(OnboardingApproval({ entityId: e1, roleId: roleIdTokenHolder, signature: sigTokenHolder })); vm.recordLogs(); - nayms.onboardViaSignature(e1, roleIdCapitalProvider, sigCapitalProvider); + nayms.onboardViaSignature(OnboardingApproval({ entityId: e1, roleId: roleIdCapitalProvider, signature: sigCapitalProvider })); Vm.Log[] memory entries = vm.getRecordedLogs(); @@ -1266,7 +1266,7 @@ contract T04EntityTest is D03ProtocolDefaults { vm.startPrank(userAddress); vm.expectRevert(abi.encodeWithSelector(InvalidObjectType.selector, entityId, LC.OBJECT_TYPE_ENTITY)); - nayms.onboardViaSignature(entityId, roleId, sig); + nayms.onboardViaSignature(OnboardingApproval({ entityId: entityId, roleId: roleId, signature: sig })); vm.stopPrank(); } diff --git a/test/T07Zaps.t.sol b/test/T07Zaps.t.sol index a093b69b..6c28bf64 100644 --- a/test/T07Zaps.t.sol +++ b/test/T07Zaps.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.20; import { D03ProtocolDefaults, c, LC, LibHelpers, StdStyle } from "./defaults/D03ProtocolDefaults.sol"; import { DummyToken } from "test/utils/DummyToken.sol"; -import { StakingConfig, PermitSignature } from "src/shared/FreeStructs.sol"; +import { StakingConfig, PermitSignature, OnboardingApproval } from "src/shared/FreeStructs.sol"; contract ZapFacetTest is D03ProtocolDefaults { using LibHelpers for address; @@ -71,11 +71,12 @@ contract ZapFacetTest is D03ProtocolDefaults { startPrank(bob); PermitSignature memory permitSignature = PermitSignature({ deadline: deadline, v: v, r: r, s: s }); + OnboardingApproval memory approval; vm.expectRevert("zapStake: invalid ERC20 token"); - nayms.zapStake(address(111), nlf.entityId, stakeAmount, stakeAmount, permitSignature); + nayms.zapStake(address(111), nlf.entityId, stakeAmount, stakeAmount, permitSignature, approval); - nayms.zapStake(address(naymToken), nlf.entityId, stakeAmount, stakeAmount, permitSignature); + nayms.zapStake(address(naymToken), nlf.entityId, stakeAmount, stakeAmount, permitSignature, approval); (uint256 staked, ) = nayms.getStakingAmounts(bob.entityId, nlf.entityId); @@ -99,16 +100,18 @@ contract ZapFacetTest is D03ProtocolDefaults { bytes32 digest = keccak256(abi.encodePacked("\x19\x01", weth.DOMAIN_SEPARATOR(), structHash)); // Sign the digest (uint8 v, bytes32 r, bytes32 s) = vm.sign(bob.pk, digest); + PermitSignature memory permitSignature = PermitSignature({ deadline: deadline, v: v, r: r, s: s }); + OnboardingApproval memory approval; startPrank(bob); vm.expectRevert("zapOrder: invalid ERC20 token"); - nayms.zapOrder(address(111), 10 ether, wethId, 1 ether, bob.entityId, 1 ether, permitSignature); + nayms.zapOrder(address(111), 10 ether, wethId, 1 ether, bob.entityId, 1 ether, permitSignature, approval); // Call zapOrder // Caller should ensure they deposit enough to cover order fees. - nayms.zapOrder(address(weth), 10 ether, wethId, 1 ether, bob.entityId, 1 ether, permitSignature); + nayms.zapOrder(address(weth), 10 ether, wethId, 1 ether, bob.entityId, 1 ether, permitSignature, approval); assertEq(nayms.internalBalanceOf(bob.entityId, bob.entityId), 1 ether, "bob should've purchased 1e18 bob p tokens"); }