diff --git a/contracts/DispenserProvider.sol b/contracts/DispenserProvider.sol index 3ddde1f..8193dbe 100644 --- a/contracts/DispenserProvider.sol +++ b/contracts/DispenserProvider.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/interfaces/IERC20.sol"; -import "hardhat/console.sol"; import "./DispenserView.sol"; contract DispenserProvider is DispenserView { @@ -70,13 +69,16 @@ contract DispenserProvider is DispenserView { bytes memory signature ) external validProviderId(poolId) { require( - lockDealNFT.isApprovedForAll(owner, address(this)), - "DispenserProvider: Owner has not approved the DispenserProvider" + msg.sender == owner || + lockDealNFT.getApproved(poolId) == msg.sender || + lockDealNFT.isApprovedForAll(owner, msg.sender), + "DispenserProvider: Caller is not approved" ); require( validUntil >= block.timestamp, "DispenserProvider: Invalid validUntil" ); + require(!isTaken[poolId][owner], "DispenserProvider: Tokens already taken"); // Check the signature bytes memory dataToCheck = abi.encodePacked( poolId, @@ -89,13 +91,14 @@ contract DispenserProvider is DispenserView { "DispenserProvider: Invalid signature" ); _createSimpleNFTs(poolId, owner, data); + isTaken[poolId][owner] = true; } function _encodeBuilder( Builder[] calldata builder ) internal pure returns (bytes memory data) { for (uint256 i = 0; i < builder.length; ++i) { - data = abi.encodePacked(data, abi.encode(builder)); + data = abi.encodePacked(data, address(builder[i].simpleProvider), builder[i].params); } } @@ -108,17 +111,18 @@ contract DispenserProvider is DispenserView { uint256 poolId = lockDealNFT.mintForProvider(owner, data[i].simpleProvider); data[i].simpleProvider.registerPool(poolId, data[i].params); lockDealNFT.cloneVaultId(poolId, tokenPoolId); - _withdrawIfAvaliable(data[i].simpleProvider, poolId, owner); + leftAmount[tokenPoolId] -= data[i].params[0]; + _withdrawIfAvailable(data[i].simpleProvider, poolId, owner); } } - function _withdrawIfAvaliable( + function _withdrawIfAvailable( ISimpleProvider provider, uint256 poolId, address owner ) internal { if (provider.getWithdrawableAmount(poolId) > 0) { - lockDealNFT.safeTransferFrom(address(this), owner, poolId); + lockDealNFT.safeTransferFrom(owner, address(lockDealNFT), poolId); } } diff --git a/contracts/DispenserState.sol b/contracts/DispenserState.sol index 6d27944..488ff1d 100644 --- a/contracts/DispenserState.sol +++ b/contracts/DispenserState.sol @@ -6,7 +6,7 @@ import "@poolzfinance/poolz-helper-v2/contracts/interfaces/IVaultManager.sol"; import "@poolzfinance/poolz-helper-v2/contracts/interfaces/ISimpleProvider.sol"; contract DispenserState { - mapping(uint256 => mapping(address => bool)) public isAvailable; + mapping(uint256 => mapping(address => bool)) public isTaken; mapping(uint256 => uint256) public leftAmount; struct Builder { diff --git a/contracts/mock/MockVaultManager.sol b/contracts/mock/MockVaultManager.sol index 669c811..0e1aecc 100644 --- a/contracts/mock/MockVaultManager.sol +++ b/contracts/mock/MockVaultManager.sol @@ -1,4 +1,31 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "@poolzfinance/lockdeal-nft/contracts/mock/MockVaultManager.sol"; \ No newline at end of file +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MockVaultManager { + mapping(address => uint) public tokenToVaultId; + mapping(uint256 => address) vaultIdtoToken; + bool public transfers = true; + uint256 public Id = 0; + + function safeDeposit(address _tokenAddress, uint _amount, address from, bytes memory signature) external returns (uint vaultId) { + require(keccak256(abi.encodePacked(signature)) == keccak256(abi.encodePacked("signature")), "wrong signature"); + IERC20(_tokenAddress).transferFrom(from, address(this), _amount); + vaultId = _depositByToken(_tokenAddress); + } + + function _depositByToken(address _tokenAddress) internal returns (uint vaultId) { + vaultId = ++Id; + vaultIdtoToken[vaultId] = _tokenAddress; + tokenToVaultId[_tokenAddress] = vaultId; + } + + function withdrawByVaultId(uint _vaultId, address _to, uint _amount) external { + IERC20(vaultIdtoToken[_vaultId]).transfer(_to, _amount); + } + + function vaultIdToTokenAddress(uint _vaultId) external view returns (address) { + return vaultIdtoToken[_vaultId]; + } +} diff --git a/test/DispenserProvider.ts b/test/DispenserProvider.ts index 85b0cb1..9e8fe35 100644 --- a/test/DispenserProvider.ts +++ b/test/DispenserProvider.ts @@ -1,18 +1,19 @@ import { DispenserProvider } from "../typechain-types/contracts/DispenserProvider" import { LockDealNFT } from "../typechain-types/@poolzfinance/lockdeal-nft/contracts/LockDealNFT/LockDealNFT" import { DealProvider } from "../typechain-types/@poolzfinance/lockdeal-nft/contracts/SimpleProviders/DealProvider/DealProvider" -import { MockVaultManager as VaultManager } from "../typechain-types/@poolzfinance/lockdeal-nft/contracts/mock/MockVaultManager" +import { MockVaultManager as VaultManager } from "../typechain-types/contracts/mock/MockVaultManager" import { LockDealProvider } from "../typechain-types/@poolzfinance/lockdeal-nft/contracts/SimpleProviders/LockProvider/LockDealProvider" import { ERC20Token } from "../typechain-types/@poolzfinance/poolz-helper-v2/contracts/token/ERC20Token" import { TimedDealProvider } from "../typechain-types/@poolzfinance/lockdeal-nft/contracts/SimpleProviders/TimedDealProvider/TimedDealProvider" import { DispenserState } from "../typechain-types/contracts/DispenserProvider" import { time } from "@nomicfoundation/hardhat-network-helpers" import { expect } from "chai" -import { Bytes, constants } from "ethers" +import { createSignature } from "./helper" +import { Bytes, constants, BigNumber } from "ethers" import { ethers } from "hardhat" import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers" -describe("DispenserProvider", function () { +describe("Dispenser Provider tests", function () { let owner: SignerWithAddress let user: SignerWithAddress let signer: SignerWithAddress @@ -20,12 +21,14 @@ describe("DispenserProvider", function () { let token: ERC20Token let lockDealNFT: LockDealNFT let dealProvider: DealProvider - let userData: DispenserState.BuilderStruct[] + let userData: DispenserState.BuilderStruct + let usersData: DispenserState.BuilderStruct[] let lockProvider: LockDealProvider let timedProvider: TimedDealProvider let vaultManager: VaultManager let packedData: string let poolId: number + let validTime: BigNumber const builderType = ["uint256", "uint256", "address", "tuple(address,uint256[])[]"] const creationSignature: Bytes = ethers.utils.toUtf8Bytes("signature") const amount = ethers.utils.parseUnits("10", 18) @@ -45,7 +48,6 @@ describe("DispenserProvider", function () { lockProvider = await LockDealProvider.deploy(lockDealNFT.address, dealProvider.address) const TimedDealProvider = await ethers.getContractFactory("TimedDealProvider") timedProvider = await TimedDealProvider.deploy(lockDealNFT.address, lockProvider.address) - await lockDealNFT.setApprovedContract(dealProvider.address, true) await lockDealNFT.setApprovedContract(lockProvider.address, true) await lockDealNFT.setApprovedContract(timedProvider.address, true) @@ -57,8 +59,11 @@ describe("DispenserProvider", function () { const ERC20Token = await ethers.getContractFactory("ERC20Token") token = await ERC20Token.deploy("Test", "TST") poolId = (await lockDealNFT.totalSupply()).toNumber() + await token.approve(vaultManager.address, amount) await dispenserProvider.connect(owner).deposit(signer.address, token.address, amount, creationSignature) - const validTime = ethers.BigNumber.from((await time.latest()) + ONE_DAY) + validTime = ethers.BigNumber.from((await time.latest()) + ONE_DAY) + userData = { simpleProvider: lockProvider.address, params: [amount.div(2), validTime] } + usersData = [{ simpleProvider: lockProvider.address, params: [amount.div(2), validTime] }] packedData = ethers.utils.defaultAbiCoder.encode(builderType, [ poolId, validTime, @@ -67,11 +72,6 @@ describe("DispenserProvider", function () { ]) }) - async function createSignature(signer: SignerWithAddress, data: string[]) { - const packedData = ethers.utils.defaultAbiCoder.encode(builderType, data) - return await signer.signMessage(ethers.utils.arrayify(packedData)) - } - it("should return name of contract", async () => { expect(await dispenserProvider.name()).to.equal("DispenserProvider") }) @@ -80,13 +80,50 @@ describe("DispenserProvider", function () { expect(await dispenserProvider.leftAmount(poolId)).to.equal(amount) }) - it("should transfer dealProvider nft", async () => { - const validTime = ethers.BigNumber.from((await time.latest()) + ONE_DAY) - userData = [{ simpleProvider: dealProvider.address, params: [amount] }] - const builderData = [[dealProvider.address, [amount]]] - const data = [poolId, validTime, user.address, builderData] - const signature = await createSignature(signer, data) - //await dispenserProvider.connect(user).createLock(poolId, validTime, user.address, userData, signature) + it("should deacrease leftAmount after lock", async () => { + const signatureData = [poolId, validTime, user.address, userData] + const signature = await createSignature(signer, signatureData) + await dispenserProvider.connect(user).createLock(poolId, validTime, user.address, usersData, signature) + expect(await dispenserProvider.leftAmount(poolId)).to.equal(amount.div(2)) + }) + + it("should transfer if available", async () => { + userData = { simpleProvider: dealProvider.address, params: [amount] } + usersData = [{ simpleProvider: dealProvider.address, params: [amount] }] + const signatureData = [poolId, validTime, user.address, userData] + const signature = await createSignature(signer, signatureData) + const beforeBalance = await token.balanceOf(user.address) + await dispenserProvider.connect(user).createLock(poolId, validTime, user.address, usersData, signature) + // check if user has tokens after the transfer + expect(await token.balanceOf(user.address)).to.equal(beforeBalance.add(amount)) + }) + + it("should create lock if approved for all", async () => { + await lockDealNFT.connect(user).setApprovalForAll(owner.address, true) + const signatureData = [poolId, validTime, user.address, userData] + const signature = await createSignature(signer, signatureData) + await expect( + dispenserProvider.connect(owner).createLock(poolId, validTime, user.address, usersData, signature) + ).to.not.reverted + await lockDealNFT.connect(user).setApprovalForAll(owner.address, false) + }) + + it("should create lock if approved poolId", async () => { + await lockDealNFT.connect(signer).approve(owner.address, poolId) + const signatureData = [poolId, validTime, user.address, userData] + const signature = await createSignature(signer, signatureData) + await expect( + dispenserProvider.connect(owner).createLock(poolId, validTime, user.address, usersData, signature) + ).to.not.reverted + }) + + it("should revert double creation", async () => { + const signatureData = [poolId, validTime, user.address, userData] + const signature = await createSignature(signer, signatureData) + await dispenserProvider.connect(user).createLock(poolId, validTime, user.address, usersData, signature) + await expect( + dispenserProvider.connect(user).createLock(poolId, validTime, user.address, usersData, signature) + ).to.be.revertedWith("DispenserProvider: Tokens already taken") }) it("should revert invalid signer address", async () => { @@ -95,6 +132,14 @@ describe("DispenserProvider", function () { ).to.be.revertedWith("DispenserProvider: Invalid signer address") }) + it("should revert if sender is invalid", async () => { + const signatureData = [poolId, validTime, user.address, userData] + const signature = await createSignature(signer, signatureData) + await expect( + dispenserProvider.connect(owner).createLock(poolId, validTime, user.address, usersData, signature) + ).to.be.revertedWith("DispenserProvider: Caller is not approved") + }) + it("should revert invalid signer address", async () => { await expect( dispenserProvider.connect(owner).deposit(owner.address, constants.AddressZero, amount, creationSignature) diff --git a/test/helper.ts b/test/helper.ts new file mode 100644 index 0000000..606a024 --- /dev/null +++ b/test/helper.ts @@ -0,0 +1,26 @@ +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers" +import { ethers } from "hardhat" + +export async function createSignature(signer: SignerWithAddress, data: any[]): Promise { + const types: string[] = [] + const values: any[] = [] + for (const element of data) { + if (typeof element === "string") { + types.push("address") + values.push(element) + } else if (typeof element === "object" && Array.isArray(element)) { + types.push("uint256[]") + values.push(element) + } else if (typeof element === "number" || ethers.BigNumber.isBigNumber(element)) { + types.push("uint256") + values.push(element) + } else if (typeof element === "object" && !Array.isArray(element)) { + types.push("address") + values.push(element.simpleProvider) + types.push("uint256[]") + values.push(element.params) + } + } + const packedData = ethers.utils.solidityKeccak256(types, values) + return signer.signMessage(ethers.utils.arrayify(packedData)) +} \ No newline at end of file