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

Combine approve, deposit, stake/unstake into a single method #150

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
95 changes: 95 additions & 0 deletions src/facets/ZapFacet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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";
import { LibAdmin } from "../libs/LibAdmin.sol";
import { LibObject } from "../libs/LibObject.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 _entityId Staking entity ID
* @param _amountToDeposit Deposit amount
* @param _amountToStake Stake amount
* @param _permitSignature The permit signature parameters
*/
function zapStake(
address _externalTokenAddress,
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 parentId = LibObject._getParentFromAddress(msg.sender);
require(LibEntity._isEntity(parentId), "zapStake: invalid receiver");

// 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, _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 _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
* @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 _depositAmount,
bytes32 _sellToken,
uint256 _sellAmount,
bytes32 _buyToken,
uint256 _buyAmount,
PermitSignature calldata _permitSignature
)
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 parentId = LibObject._getParentFromAddress(msg.sender);
require(LibEntity._isEntity(parentId), "zapOrder: invalid entity");

// 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
return LibMarket._executeLimitOffer(parentId, _sellToken, _sellAmount, _buyToken, _buyAmount, LC.FEE_TYPE_TRADING);
}
Dismissed Show dismissed Hide dismissed
}
7 changes: 7 additions & 0 deletions src/shared/FreeStructs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,10 @@ struct RewardsBalances {
uint256[] amounts;
uint64 lastPaidInterval;
}

struct PermitSignature {
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
8 changes: 0 additions & 8 deletions test/T01LibERC20.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down
115 changes: 115 additions & 0 deletions test/T07Zaps.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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 });

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);

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);

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);

assertEq(nayms.internalBalanceOf(bob.entityId, bob.entityId), 1 ether, "bob should've purchased 1e18 bob p tokens");
}
}
61 changes: 53 additions & 8 deletions test/utils/DummyToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,92 @@
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;
uint256 public totalSupply = 0;
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);
}
}
Loading