From 9ebe49937b6265cb8f3161dc0d2bfb794d36c418 Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Wed, 16 Oct 2024 17:07:14 +0700 Subject: [PATCH 01/13] feat: zap --- src/facets/ZapFacet.sol | 69 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/facets/ZapFacet.sol diff --git a/src/facets/ZapFacet.sol b/src/facets/ZapFacet.sol new file mode 100644 index 00000000..d9b41dca --- /dev/null +++ b/src/facets/ZapFacet.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import { Modifiers } from "../shared/Modifiers.sol"; +import { LibTokenizedVaultIO } from "../libs/LibTokenizedVaultIO.sol"; +import { LibEntity } from "../libs/LibEntity.sol"; +import { LibAdmin } from "../libs/LibAdmin.sol"; +import { LibObject } from "../libs/LibObject.sol"; +import { LibConstants as LC } from "../libs/LibConstants.sol"; +import { LibACL } from "../libs/LibACL.sol"; +import { LibHelpers } from "../libs/LibHelpers.sol"; +import { ExternalWithdrawInvalidReceiver } from "../shared/CustomErrors.sol"; +import { ReentrancyGuard } from "../utils/ReentrancyGuard.sol"; +import { LibTokenizedVaultStaking } from "../libs/LibTokenizedVaultStaking.sol"; +import { IERC20 } from "../interfaces/IERC20.sol"; + +contract TokenizedVaultIOFacet is Modifiers, ReentrancyGuard { + /** + * @notice Approve, deposit, and stake funds into msg.sender's Nayms platform entity in one transaction + * @dev Approves token transfer, deposits from msg.sender to their associated entity, and stakes the amount + * @param _externalTokenAddress Token address + * @param _amount deposit and stake amount + */ + function zapStake( + address _externalTokenAddress, + uint256 _amount + ) external notLocked nonReentrant assertPrivilege(LibObject._getParentFromAddress(msg.sender), LC.GROUP_EXTERNAL_DEPOSIT) { + // Check if it's a supported ERC20 token + require(LibAdmin._isSupportedExternalTokenAddress(_externalTokenAddress), "zapStake: invalid ERC20 token"); + + // Get the user's entity + bytes32 entityId = LibObject._getParentFromAddress(msg.sender); + require(LibEntity._isEntity(entityId), "zapStake: invalid receiver"); + + // Approve the token transfer + IERC20 token = IERC20(_externalTokenAddress); + require(token.approve(address(this), _amount), "zapStake: approval failed"); + + // Perform the deposit + LibTokenizedVaultIO._externalDeposit(entityId, _externalTokenAddress, _amount); + + // Stake the deposited amount + LibTokenizedVaultStaking._stake(entityId, entityId, _amount); + } + + /** + * @notice Unstake and withdraw funds out of Nayms platform + * @dev Unstakes, withdraws from entity to an external account + * @param _entityId Internal ID of the entity the user is withdrawing from + * @param _receiver External address receiving the funds + * @param _externalTokenAddress Token address + * @param _amount amount to unstake and withdraw + */ + function zapUnstake( + bytes32 _entityId, + address _receiver, + address _externalTokenAddress, + uint256 _amount + ) external notLocked nonReentrant assertPrivilege(LibObject._getParentFromAddress(msg.sender), LC.GROUP_EXTERNAL_WITHDRAW_FROM_ENTITY) { + if (!LibACL._hasGroupPrivilege(LibHelpers._getIdForAddress(_receiver), _entityId, LibHelpers._stringToBytes32(LC.GROUP_EXTERNAL_WITHDRAW_FROM_ENTITY))) + revert ExternalWithdrawInvalidReceiver(_receiver); + + // Unstake the amount + LibTokenizedVaultStaking._unstake(_entityId, _entityId, _amount); + + // Perform the withdrawal directly to the receiver + LibTokenizedVaultIO._externalWithdraw(_entityId, _receiver, _externalTokenAddress, _amount); + } +} From 7663d0e6de799674a367af8b817131014cdde472 Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Thu, 17 Oct 2024 15:00:20 +0700 Subject: [PATCH 02/13] fix: zapUnstake --- src/facets/ZapFacet.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/facets/ZapFacet.sol b/src/facets/ZapFacet.sol index d9b41dca..9dc41c55 100644 --- a/src/facets/ZapFacet.sol +++ b/src/facets/ZapFacet.sol @@ -61,7 +61,8 @@ contract TokenizedVaultIOFacet is Modifiers, ReentrancyGuard { revert ExternalWithdrawInvalidReceiver(_receiver); // Unstake the amount - LibTokenizedVaultStaking._unstake(_entityId, _entityId, _amount); + bytes32 parentId = LibObject._getParentFromAddress(msg.sender); + LibTokenizedVaultStaking._unstake(parentId, _entityId); // Perform the withdrawal directly to the receiver LibTokenizedVaultIO._externalWithdraw(_entityId, _receiver, _externalTokenAddress, _amount); From c19dc1fd223f1135647b639fe7c3d3fe0d2ee9b0 Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Thu, 17 Oct 2024 15:03:02 +0700 Subject: [PATCH 03/13] fix: rename to ZapFacet --- src/facets/ZapFacet.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/facets/ZapFacet.sol b/src/facets/ZapFacet.sol index 9dc41c55..429cbe75 100644 --- a/src/facets/ZapFacet.sol +++ b/src/facets/ZapFacet.sol @@ -14,7 +14,7 @@ import { ReentrancyGuard } from "../utils/ReentrancyGuard.sol"; import { LibTokenizedVaultStaking } from "../libs/LibTokenizedVaultStaking.sol"; import { IERC20 } from "../interfaces/IERC20.sol"; -contract TokenizedVaultIOFacet is Modifiers, ReentrancyGuard { +contract ZapFacet is Modifiers, ReentrancyGuard { /** * @notice Approve, deposit, and stake funds into msg.sender's Nayms platform entity in one transaction * @dev Approves token transfer, deposits from msg.sender to their associated entity, and stakes the amount From 75c11963b3188d408f090ee8a1091f4cbe6db6de Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Mon, 21 Oct 2024 18:19:11 +0700 Subject: [PATCH 04/13] feat: zap execute limit offer --- src/facets/ZapFacet.sol | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/facets/ZapFacet.sol b/src/facets/ZapFacet.sol index 429cbe75..4e3c3a4b 100644 --- a/src/facets/ZapFacet.sol +++ b/src/facets/ZapFacet.sol @@ -13,6 +13,7 @@ import { ExternalWithdrawInvalidReceiver } from "../shared/CustomErrors.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 { /** @@ -43,6 +44,39 @@ contract ZapFacet is Modifiers, ReentrancyGuard { LibTokenizedVaultStaking._stake(entityId, entityId, _amount); } + function zapOrder( + address _externalTokenAddress, + uint256 _amount, + bytes32 _sellToken, + uint256 _sellAmount, + bytes32 _buyToken, + uint256 _buyAmount + ) + external + notLocked + nonReentrant + assertPrivilege(LibObject._getParentFromAddress(msg.sender), LC.GROUP_EXECUTE_LIMIT_OFFER) + returns (uint256 offerId_, uint256 buyTokenCommissionsPaid_, uint256 sellTokenCommissionsPaid_) + { + // Check if it's a supported ERC20 token + require(LibAdmin._isSupportedExternalTokenAddress(_externalTokenAddress), "zapOrder: invalid ERC20 token"); + + // Get the user's entity + bytes32 entityId = LibObject._getParentFromAddress(msg.sender); + require(LibEntity._isEntity(entityId), "zapOrder: invalid entity"); + + // Approve the token transfer to the ZapFacet contract + IERC20 token = IERC20(_externalTokenAddress); + require(token.approve(address(this), _amount), "zapOrder: approval failed"); + + // Perform the external deposit + LibTokenizedVaultIO._externalDeposit(entityId, _externalTokenAddress, _amount); + + // Execute the limit order + // Assumption: executeLimitOrder is a function that takes entityId, token address, amount, and order parameters + return LibMarket._executeLimitOffer(entityId, _sellToken, _sellAmount, _buyToken, _buyAmount, LC.FEE_TYPE_TRADING); + } + /** * @notice Unstake and withdraw funds out of Nayms platform * @dev Unstakes, withdraws from entity to an external account From bc344cf1103f35ede829526bf907a765357334c2 Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Wed, 23 Oct 2024 14:31:43 +0700 Subject: [PATCH 05/13] refactor: remove zapUnstake --- src/facets/ZapFacet.sol | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/facets/ZapFacet.sol b/src/facets/ZapFacet.sol index 4e3c3a4b..ff64b91c 100644 --- a/src/facets/ZapFacet.sol +++ b/src/facets/ZapFacet.sol @@ -76,29 +76,4 @@ contract ZapFacet is Modifiers, ReentrancyGuard { // Assumption: executeLimitOrder is a function that takes entityId, token address, amount, and order parameters return LibMarket._executeLimitOffer(entityId, _sellToken, _sellAmount, _buyToken, _buyAmount, LC.FEE_TYPE_TRADING); } - - /** - * @notice Unstake and withdraw funds out of Nayms platform - * @dev Unstakes, withdraws from entity to an external account - * @param _entityId Internal ID of the entity the user is withdrawing from - * @param _receiver External address receiving the funds - * @param _externalTokenAddress Token address - * @param _amount amount to unstake and withdraw - */ - function zapUnstake( - bytes32 _entityId, - address _receiver, - address _externalTokenAddress, - uint256 _amount - ) external notLocked nonReentrant assertPrivilege(LibObject._getParentFromAddress(msg.sender), LC.GROUP_EXTERNAL_WITHDRAW_FROM_ENTITY) { - if (!LibACL._hasGroupPrivilege(LibHelpers._getIdForAddress(_receiver), _entityId, LibHelpers._stringToBytes32(LC.GROUP_EXTERNAL_WITHDRAW_FROM_ENTITY))) - revert ExternalWithdrawInvalidReceiver(_receiver); - - // Unstake the amount - bytes32 parentId = LibObject._getParentFromAddress(msg.sender); - LibTokenizedVaultStaking._unstake(parentId, _entityId); - - // Perform the withdrawal directly to the receiver - LibTokenizedVaultIO._externalWithdraw(_entityId, _receiver, _externalTokenAddress, _amount); - } } From cfa33e1496a02689fc066a0e97c9ad9e8c69378b Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Mon, 28 Oct 2024 13:57:02 +0700 Subject: [PATCH 06/13] fix: zaps require calling erc20 permit functions --- src/facets/ZapFacet.sol | 62 ++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/src/facets/ZapFacet.sol b/src/facets/ZapFacet.sol index ff64b91c..74e0aa93 100644 --- a/src/facets/ZapFacet.sol +++ b/src/facets/ZapFacet.sol @@ -7,50 +7,74 @@ import { LibEntity } from "../libs/LibEntity.sol"; import { LibAdmin } from "../libs/LibAdmin.sol"; import { LibObject } from "../libs/LibObject.sol"; import { LibConstants as LC } from "../libs/LibConstants.sol"; -import { LibACL } from "../libs/LibACL.sol"; -import { LibHelpers } from "../libs/LibHelpers.sol"; -import { ExternalWithdrawInvalidReceiver } from "../shared/CustomErrors.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 { + struct PermitSignature { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + /** - * @notice Approve, deposit, and stake funds into msg.sender's Nayms platform entity in one transaction - * @dev Approves token transfer, deposits from msg.sender to their associated entity, and stakes the amount + * @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 _amount deposit and stake amount + * @param _entityId Staking entity ID + * @param _amountToDeposit Deposit amount + * @param _amountToStake Stake amount + * @param _permitSignature The permit signature parameters */ function zapStake( address _externalTokenAddress, - uint256 _amount - ) external notLocked nonReentrant assertPrivilege(LibObject._getParentFromAddress(msg.sender), LC.GROUP_EXTERNAL_DEPOSIT) { + bytes32 _entityId, + uint256 _amountToDeposit, + uint256 _amountToStake, + PermitSignature calldata _permitSignature + ) 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 entityId = LibObject._getParentFromAddress(msg.sender); - require(LibEntity._isEntity(entityId), "zapStake: invalid receiver"); + bytes32 parentId = LibObject._getParentFromAddress(msg.sender); + require(LibEntity._isEntity(parentId), "zapStake: invalid receiver"); - // Approve the token transfer - IERC20 token = IERC20(_externalTokenAddress); - require(token.approve(address(this), _amount), "zapStake: approval failed"); + // 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(entityId, _externalTokenAddress, _amount); + LibTokenizedVaultIO._externalDeposit(parentId, _externalTokenAddress, _amountToDeposit); // Stake the deposited amount - LibTokenizedVaultStaking._stake(entityId, entityId, _amount); + LibTokenizedVaultStaking._stake(parentId, _entityId, _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 _amount 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 + * @return offerId_ The ID of the created offer + * @return buyTokenCommissionsPaid_ Commissions paid in buy token + * @return sellTokenCommissionsPaid_ Commissions paid in sell token + */ function zapOrder( address _externalTokenAddress, uint256 _amount, bytes32 _sellToken, uint256 _sellAmount, bytes32 _buyToken, - uint256 _buyAmount + uint256 _buyAmount, + PermitSignature calldata _permitSignature ) external notLocked @@ -65,15 +89,13 @@ contract ZapFacet is Modifiers, ReentrancyGuard { bytes32 entityId = LibObject._getParentFromAddress(msg.sender); require(LibEntity._isEntity(entityId), "zapOrder: invalid entity"); - // Approve the token transfer to the ZapFacet contract - IERC20 token = IERC20(_externalTokenAddress); - require(token.approve(address(this), _amount), "zapOrder: approval failed"); + // Use permit to set allowance + IERC20(_externalTokenAddress).permit(msg.sender, address(this), _amount, _permitSignature.deadline, _permitSignature.v, _permitSignature.r, _permitSignature.s); // Perform the external deposit LibTokenizedVaultIO._externalDeposit(entityId, _externalTokenAddress, _amount); // Execute the limit order - // Assumption: executeLimitOrder is a function that takes entityId, token address, amount, and order parameters return LibMarket._executeLimitOffer(entityId, _sellToken, _sellAmount, _buyToken, _buyAmount, LC.FEE_TYPE_TRADING); } } From 0b443a1b14e617954f407018d195258a6ad68eca Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Mon, 28 Oct 2024 15:15:08 +0700 Subject: [PATCH 07/13] refactor: rename param to parentId --- src/facets/ZapFacet.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/facets/ZapFacet.sol b/src/facets/ZapFacet.sol index 74e0aa93..07f9e647 100644 --- a/src/facets/ZapFacet.sol +++ b/src/facets/ZapFacet.sol @@ -86,16 +86,16 @@ contract ZapFacet is Modifiers, ReentrancyGuard { require(LibAdmin._isSupportedExternalTokenAddress(_externalTokenAddress), "zapOrder: invalid ERC20 token"); // Get the user's entity - bytes32 entityId = LibObject._getParentFromAddress(msg.sender); - require(LibEntity._isEntity(entityId), "zapOrder: invalid entity"); + bytes32 parentId = LibObject._getParentFromAddress(msg.sender); + require(LibEntity._isEntity(parentId), "zapOrder: invalid entity"); // Use permit to set allowance IERC20(_externalTokenAddress).permit(msg.sender, address(this), _amount, _permitSignature.deadline, _permitSignature.v, _permitSignature.r, _permitSignature.s); // Perform the external deposit - LibTokenizedVaultIO._externalDeposit(entityId, _externalTokenAddress, _amount); + LibTokenizedVaultIO._externalDeposit(parentId, _externalTokenAddress, _amount); // Execute the limit order - return LibMarket._executeLimitOffer(entityId, _sellToken, _sellAmount, _buyToken, _buyAmount, LC.FEE_TYPE_TRADING); + return LibMarket._executeLimitOffer(parentId, _sellToken, _sellAmount, _buyToken, _buyAmount, LC.FEE_TYPE_TRADING); } } From 8c0599b1bb8fd221ab04ef55383789050e4751ef Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Mon, 28 Oct 2024 15:25:39 +0700 Subject: [PATCH 08/13] refactor: move PermitSignature struct into FreeStructs.sol --- src/facets/ZapFacet.sol | 8 +------- src/shared/FreeStructs.sol | 7 +++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/facets/ZapFacet.sol b/src/facets/ZapFacet.sol index 07f9e647..e60ab86c 100644 --- a/src/facets/ZapFacet.sol +++ b/src/facets/ZapFacet.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.20; +import { PermitSignature } from "../shared/FreeStructs.sol"; import { Modifiers } from "../shared/Modifiers.sol"; import { LibTokenizedVaultIO } from "../libs/LibTokenizedVaultIO.sol"; import { LibEntity } from "../libs/LibEntity.sol"; @@ -13,13 +14,6 @@ import { IERC20 } from "../interfaces/IERC20.sol"; import { LibMarket } from "../libs/LibMarket.sol"; contract ZapFacet is Modifiers, ReentrancyGuard { - struct PermitSignature { - uint256 deadline; - uint8 v; - bytes32 r; - bytes32 s; - } - /** * @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 diff --git a/src/shared/FreeStructs.sol b/src/shared/FreeStructs.sol index a10206ca..0fa40700 100644 --- a/src/shared/FreeStructs.sol +++ b/src/shared/FreeStructs.sol @@ -126,3 +126,10 @@ struct RewardsBalances { uint256[] amounts; uint64 lastPaidInterval; } + +struct PermitSignature { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; +} From 5ee106fc79b8a4359b415d4fc341367169fbd653 Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Mon, 28 Oct 2024 15:36:43 +0700 Subject: [PATCH 09/13] test: update test erc20 with permit method and related variables --- test/utils/DummyToken.sol | 61 ++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/test/utils/DummyToken.sol b/test/utils/DummyToken.sol index 921d18e0..d1e0de1f 100644 --- a/test/utils/DummyToken.sol +++ b/test/utils/DummyToken.sol @@ -2,8 +2,11 @@ pragma solidity 0.8.20; import { IERC20 } from "src/interfaces/IERC20.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; contract DummyToken is IERC20 { + using ECDSA for bytes32; + string public name = "Dummy"; string public symbol = "DUM"; uint8 public decimals = 18; @@ -11,38 +14,80 @@ contract DummyToken is IERC20 { mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; - function transfer(address to, uint256 value) external returns (bool) { - if (value == 0) { - return false; + // EIP-2612 permit state variables + bytes32 public DOMAIN_SEPARATOR; + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = 0x6d47c92dbe9aa29a8e9e38d25f3f54ab645e5df690ddf0d3e2a24ec2445a44f0; + mapping(address => uint256) public nonces; + + constructor() { + uint256 chainId; + assembly { + chainId := chainid() } + DOMAIN_SEPARATOR = keccak256( + abi.encode( + // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") + 0x8b73e7bb5ba7313e92d4a46294e43b9c1bafabf1adbe7b6f4bdfd44c38a7e6d4, + keccak256(bytes(name)), + keccak256(bytes("1")), + chainId, + address(this) + ) + ); + } + function transfer(address to, uint256 value) external returns (bool) { require(balanceOf[msg.sender] >= value, "not enough balance"); balanceOf[msg.sender] -= value; balanceOf[to] += value; + emit Transfer(msg.sender, to, value); return true; } function approve(address spender, uint256 value) external returns (bool) { allowance[msg.sender][spender] = value; + emit Approval(msg.sender, spender, value); return true; } function transferFrom(address from, address to, uint256 value) external returns (bool) { - if (value == 0) { - revert(); - } - require(allowance[from][msg.sender] >= value, "not enough allowance"); require(balanceOf[from] >= value, "not enough balance"); + allowance[from][msg.sender] -= value; balanceOf[from] -= value; balanceOf[to] += value; + emit Transfer(from, to, value); return true; } function mint(address to, uint256 value) external { balanceOf[to] += value; totalSupply += value; + emit Transfer(address(0), to, value); } - function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external {} + /** + * @notice Approves tokens via signature, as per EIP-2612 + * @param owner The token owner's address + * @param spender The spender's address + * @param value The amount to approve + * @param deadline The deadline timestamp by which the permit must be used + * @param v The recovery byte of the signature + * @param r Half of the ECDSA signature pair + * @param s Half of the ECDSA signature pair + */ + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external override { + require(block.timestamp <= deadline, "permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); + + address recoveredAddress = digest.recover(v, r, s); + require(recoveredAddress == owner, "permit: invalid signature"); + + allowance[owner][spender] = value; + emit Approval(owner, spender, value); + } } From 11b346d5c4dd76027f0441ea02ef4ca36ec5f70c Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Mon, 28 Oct 2024 17:50:06 +0700 Subject: [PATCH 10/13] refactor: rename param to _depositAmount --- src/facets/ZapFacet.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/facets/ZapFacet.sol b/src/facets/ZapFacet.sol index e60ab86c..122a7260 100644 --- a/src/facets/ZapFacet.sol +++ b/src/facets/ZapFacet.sol @@ -51,7 +51,7 @@ contract ZapFacet is Modifiers, ReentrancyGuard { * @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 _amount Amount to deposit + * @param _depositAmount Amount to deposit * @param _sellToken Sell token ID * @param _sellAmount Sell amount * @param _buyToken Buy token ID @@ -63,7 +63,7 @@ contract ZapFacet is Modifiers, ReentrancyGuard { */ function zapOrder( address _externalTokenAddress, - uint256 _amount, + uint256 _depositAmount, bytes32 _sellToken, uint256 _sellAmount, bytes32 _buyToken, @@ -84,10 +84,10 @@ contract ZapFacet is Modifiers, ReentrancyGuard { require(LibEntity._isEntity(parentId), "zapOrder: invalid entity"); // Use permit to set allowance - IERC20(_externalTokenAddress).permit(msg.sender, address(this), _amount, _permitSignature.deadline, _permitSignature.v, _permitSignature.r, _permitSignature.s); + IERC20(_externalTokenAddress).permit(msg.sender, address(this), _depositAmount, _permitSignature.deadline, _permitSignature.v, _permitSignature.r, _permitSignature.s); // Perform the external deposit - LibTokenizedVaultIO._externalDeposit(parentId, _externalTokenAddress, _amount); + LibTokenizedVaultIO._externalDeposit(parentId, _externalTokenAddress, _depositAmount); // Execute the limit order return LibMarket._executeLimitOffer(parentId, _sellToken, _sellAmount, _buyToken, _buyAmount, LC.FEE_TYPE_TRADING); From c9e4997f4e50fcb60ca2e2efc24c28525a8c8b10 Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Mon, 28 Oct 2024 17:50:58 +0700 Subject: [PATCH 11/13] test: add tests for zaps --- test/T07Zaps.t.sol | 108 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 test/T07Zaps.t.sol diff --git a/test/T07Zaps.t.sol b/test/T07Zaps.t.sol new file mode 100644 index 00000000..d9e6cf17 --- /dev/null +++ b/test/T07Zaps.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +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"; + +contract ZapFacetTest is D03ProtocolDefaults { + using LibHelpers for address; + using StdStyle for *; + + DummyToken internal naymToken = new DummyToken(); + DummyToken internal rewardToken; + + NaymsAccount bob = makeNaymsAcc("Bob"); + + NaymsAccount nlf = makeNaymsAcc(LC.NLF_IDENTIFIER); + + uint64 private constant SCALE_FACTOR = 1_000_000; // 6 digits because USDC + uint64 private constant A = (15 * SCALE_FACTOR) / 100; + uint64 private constant R = (85 * SCALE_FACTOR) / 100; + uint64 private constant I = 30 days; + bytes32 NAYM_ID = address(naymToken)._getIdForAddress(); + function initStaking(uint256 initDate) internal { + StakingConfig memory config = StakingConfig({ + tokenId: NAYM_ID, + initDate: initDate, + a: A, // Amplification factor + r: R, // Boost decay factor + divider: SCALE_FACTOR, + interval: I // Amount of time per interval in seconds + }); + + startPrank(sa); + nayms.initStaking(nlf.entityId, config); + vm.stopPrank(); + } + + uint256 internal stakeAmount = 1e18; + uint256 internal unstakeAmount = 1e18; + + function setUp() public { + naymToken.mint(bob.addr, stakeAmount); + + startPrank(sa); + nayms.addSupportedExternalToken(address(naymToken), 100); + + vm.startPrank(sm.addr); + hCreateEntity(bob.entityId, bob, entity, "Bob data"); + hCreateEntity(nlf.entityId, nlf, entity, "NLF"); + } + + function test_zapStake_Success() public { + initStaking(block.timestamp + 1 + 7 days); + + // Prepare permit data + uint256 deadline = block.timestamp; + + // Create permit digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + naymToken.DOMAIN_SEPARATOR(), + keccak256(abi.encode(naymToken.PERMIT_TYPEHASH(), bob.addr, address(nayms), stakeAmount, naymToken.nonces(owner), deadline)) + ) + ); + + // Sign the digest + (uint8 v, bytes32 r, bytes32 s) = vm.sign(bob.pk, digest); + + startPrank(bob); + + PermitSignature memory permitSignature = PermitSignature({ deadline: deadline, v: v, r: r, s: s }); + + nayms.zapStake(address(naymToken), nlf.entityId, stakeAmount, stakeAmount, permitSignature); + + (uint256 staked, ) = nayms.getStakingAmounts(bob.entityId, nlf.entityId); + + assertEq(stakeAmount, staked, "bob's stake amount should increase"); + } + + function test_zapOrder_Success() public { + changePrank(sm.addr); + nayms.enableEntityTokenization(bob.entityId, "e1token", "e1token", 1e6); + + // Selling bob p tokens for weth + nayms.startTokenSale(bob.entityId, 1 ether, 1 ether); + + deal(address(weth), bob.addr, 10 ether); + + // Prepare permit data + uint256 deadline = block.timestamp; + bytes32 PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + uint256 nonce = weth.nonces(bob.addr); + bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, bob.addr, address(nayms), 10 ether, nonce, deadline)); + 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 }); + + startPrank(bob); + // 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); + + assertEq(nayms.internalBalanceOf(bob.entityId, bob.entityId), 1 ether, "bob should've purchased 1e18 bob p tokens"); + } +} From dad9bab879abdf6791d9af98fab2c4e121e5050b Mon Sep 17 00:00:00 2001 From: Aleksandar Marinkovic Date: Tue, 5 Nov 2024 10:59:17 +0100 Subject: [PATCH 12/13] test: fix failing transfer tests --- test/T01LibERC20.t.sol | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/T01LibERC20.t.sol b/test/T01LibERC20.t.sol index b9efd51e..4f84bf80 100644 --- a/test/T01LibERC20.t.sol +++ b/test/T01LibERC20.t.sol @@ -81,10 +81,6 @@ contract T01LibERC20 is D03ProtocolDefaults { vm.expectRevert("not enough balance"); fixture.transfer(tokenAddress, account0, 101); - // failed transfer of 0 - vm.expectRevert("LibERC20: transfer or transferFrom returned false"); - fixture.transfer(tokenAddress, account0, 0); - // successful transfer fixture.transfer(tokenAddress, account0, 100); @@ -113,10 +109,6 @@ contract T01LibERC20 is D03ProtocolDefaults { vm.expectRevert("not enough balance"); fixture.transferFrom(tokenAddress, signer1, account0, 101); - // failed transfer of 0 reverts with empty string - vm.expectRevert("LibERC20: transfer or transferFrom reverted"); - fixture.transferFrom(tokenAddress, signer1, account0, 0); - // successful transfer fixture.transferFrom(tokenAddress, signer1, account0, 100); From e87a169fe3eee8e42b7964ba5d062be30c5353b9 Mon Sep 17 00:00:00 2001 From: Aleksandar Marinkovic Date: Tue, 5 Nov 2024 11:20:41 +0100 Subject: [PATCH 13/13] test: assert erc20 address requirement --- test/T07Zaps.t.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/T07Zaps.t.sol b/test/T07Zaps.t.sol index d9e6cf17..a093b69b 100644 --- a/test/T07Zaps.t.sol +++ b/test/T07Zaps.t.sol @@ -72,6 +72,9 @@ contract ZapFacetTest is D03ProtocolDefaults { PermitSignature memory permitSignature = PermitSignature({ deadline: deadline, v: v, r: r, s: s }); + vm.expectRevert("zapStake: invalid ERC20 token"); + nayms.zapStake(address(111), nlf.entityId, stakeAmount, stakeAmount, permitSignature); + nayms.zapStake(address(naymToken), nlf.entityId, stakeAmount, stakeAmount, permitSignature); (uint256 staked, ) = nayms.getStakingAmounts(bob.entityId, nlf.entityId); @@ -99,6 +102,10 @@ contract ZapFacetTest is D03ProtocolDefaults { PermitSignature memory permitSignature = PermitSignature({ deadline: deadline, v: v, r: r, s: s }); startPrank(bob); + + vm.expectRevert("zapOrder: invalid ERC20 token"); + nayms.zapOrder(address(111), 10 ether, wethId, 1 ether, bob.entityId, 1 ether, permitSignature); + // 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);