diff --git a/addresses/address.json b/addresses/address.json index 96456599..278c7afe 100755 --- a/addresses/address.json +++ b/addresses/address.json @@ -145,10 +145,10 @@ "VestingWalletC": "0x29F74B853C4B8D36273666FB63a3b71c754424Ed", "OceanNodesBooster": "0x73558Ef3bb6543A8107ac032C5a98Da03ceb0eEf", "OPFCommunityFeeCollectorCompute": "0x1705CaF86B86e85804e20FE342f39B17322Ff592", - "Escrow": "0x225Fd0CfA1200F1e42c6C54071B646738A1d81bb", + "Escrow": "0xC98C71A9efeF2a940c73B7986f1D8469f1815F94", "AccessListFactory": "0x7ed6ac4243472adA55408fEE45c5Dff379f7aBE0", "EnterpriseFeeCollector": "0x254302d1Ae1e1200319c885D93D40a8927ACFcD7", - "EnterpriseEscrow": "0x7F773EE2B8AFE158FA03B72fC20672B408Cd9818", + "EnterpriseEscrow": "0x2213FeBd4E681633bE255AfceFbd233EbcFc33c9", "FixedPriceEnterprise": "0x6C97D128f7E7D21ac3C722458Dc5d71f7e1bBa6e" }, "goerli": { @@ -305,14 +305,14 @@ "Dispenser": "0x2720d405ef7cDC8a2E2e5AeBC8883C99611d893C", "ERC721Factory": "0xEF62FB495266C72a5212A11Dce8baa79Ec0ABeB1", "OPFCommunityFeeCollectorCompute": "0x0Ee8b03Dafc868a0899ebE1c12d734D53A957250", - "Escrow": "0x8149276f275EEFAc110D74AFE8AFECEaeC7d1593", + "Escrow": "0xd09789Ce05aa0f6feFc18315de4964Db07Cc0618", "EnterpriseFeeCollector": "0x8c98ea273bA22327F896Aa1a1a46E1BFf56e9b1D", "FixedPriceEnterprise": "0xfa48673a7C36A2A768f89AC1ee8C355D5c367B02", "AccessListFactory": "0x43eC0a34E1b70C7f8E579ab866F37642777727E7", - "EnterpriseEscrow": "0x49E35cd2bAE043Abd9074B6e5a649a5AdEB05C33", - "COMPY":"0x973e69303259B0c2543a38665122b773D28405fB", - "COMPYFaucet":"0x3EFDD8f728c8e774aB81D14d0B2F07a8238960f4", - "COMPYSwap":"0x8B8E187CF9c551e63f54AA04E21F48CDAF2296aE" + "EnterpriseEscrow": "0x6fd867E5AEE6D62a24f97939db90C4e67A73A651", + "COMPY": "0x973e69303259B0c2543a38665122b773D28405fB", + "COMPYFaucet": "0x3EFDD8f728c8e774aB81D14d0B2F07a8238960f4", + "COMPYSwap": "0x8B8E187CF9c551e63f54AA04E21F48CDAF2296aE" }, "oasis_sapphire": { "chainId": 23294, @@ -339,7 +339,7 @@ "AccessListFactory": "0x12bB8D85a091A69A07E22E52d4567dBB91568f52", "BatchPayments": "0x9497d1d64F2aFeBcd4f9916Eef3d9094E5Df962f", "OPFCommunityFeeCollectorCompute": "0xE5aa2C9B551aFcA4C0A98BB3B37D7A43084d0a66", - "Escrow": "0xA0329eFFa1370eAb1DC5998Db4292ae0F535a282" + "Escrow": "0x0Ee8b03Dafc868a0899ebE1c12d734D53A957250" }, "optimism_sepolia": { "chainId": 11155420, @@ -358,7 +358,7 @@ "Dispenser": "0x30E4CC2C7A9c6aA2b2Ce93586E3Df24a3A00bcDD", "ERC721Factory": "0xDEfD0018969cd2d4E648209F876ADe184815f038", "OPFCommunityFeeCollectorCompute": "0xe47991939632757DEe324E8ec9A3268c62C0DB76", - "Escrow": "0xD3173c3E3d9ea662E914C30Bcd647eF573AcE405", + "Escrow": "0x23A8b2D7176485a6349e4830605F323f31019333", "AccessListFactory": "0xEcD0C3519a081e3924D6F3197f86980eA7dfCf71" }, "optimism": { @@ -378,10 +378,10 @@ "Dispenser": "0x2112Eb973af1DBf83a4f11eda82f7a7527D7Fde5", "ERC721Factory": "0x80E63f73cAc60c1662f27D2DFd2EA834acddBaa8", "OPFCommunityFeeCollectorCompute": "0xc37F8341Ac6e4a94538302bCd4d49Cf0852D30C0", - "Escrow": "0x98679D582AB3398C03D3308dEB9c7AeC50B52ded", + "Escrow": "0x4dD281EB67DED07E76E413Df16176D66ae69e240", "AccessListFactory": "0x4E6058dC00e90C0DCA47A5d0D3346F409939A5ab", "EnterpriseFeeCollector": "0xE9397625Df9B63f0C152f975234b7988b54710B8", - "EnterpriseEscrow": "0xc313e19146Fc9a04470689C9d41a4D3054693531", + "EnterpriseEscrow": "0xFe7967A5176fDAFa8DE109b3507016B885a82D6e", "FixedPriceEnterprise": "0x1d535147a97bd87c8443125376E6671B60556E07" }, "pontus-x-devnet": { @@ -418,10 +418,10 @@ "ERC721Factory": "0x159924ca0F47D6F704B97E29099b89e518A17B5E", "BatchPayments": "0xFe7967A5176fDAFa8DE109b3507016B885a82D6e", "OPFCommunityFeeCollectorCompute": "0x7b0576CF01E868bce46cca91b2a8E674141b0355", - "Escrow": "0xf0c7A31D7Ee26bEBfb4BAD8e37490bEadE3F846f", + "Escrow": "0x43eC0a34E1b70C7f8E579ab866F37642777727E7", "AccessListFactory": "0xE5aa2C9B551aFcA4C0A98BB3B37D7A43084d0a66", - "COMPY":"0x298f163244e0c8cc9316D6E97162e5792ac5d410", - "COMPYFaucet":"0x23A8b2D7176485a6349e4830605F323f31019333", - "COMPYSwap":"0xb65F19225fEBb650Fcc211dC9F18FEC6f4a328D5" + "COMPY": "0x298f163244e0c8cc9316D6E97162e5792ac5d410", + "COMPYFaucet": "0x23A8b2D7176485a6349e4830605F323f31019333", + "COMPYSwap": "0xb65F19225fEBb650Fcc211dC9F18FEC6f4a328D5" } } \ No newline at end of file diff --git a/contracts/escrow/EnterpriseEscrow.sol b/contracts/escrow/EnterpriseEscrow.sol index 463c98f0..f56ff335 100644 --- a/contracts/escrow/EnterpriseEscrow.sol +++ b/contracts/escrow/EnterpriseEscrow.sol @@ -6,6 +6,7 @@ pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "../interfaces/IEnterpriseFeeCollector.sol"; @@ -111,6 +112,40 @@ contract EnterpriseEscrow is _deposit(token[i],amount[i]); } } + + /** + * @dev depositWithPermit + * Called by payer to deposit funds in the contract using ERC20Permit + * This function allows users to deposit without a separate approval transaction + * + * @param token token to deposit + * @param amount amount in wei to deposit + * @param deadline The time at which the permit expires (unix timestamp) + * @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 depositWithPermit( + address token, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external nonReentrant { + // Use permit to approve this contract to spend user's tokens + IERC20Permit(token).permit( + msg.sender, + address(this), + amount, + deadline, + v, + r, + s + ); + // Deposit the tokens + _deposit(token, amount); + } function _deposit(address token,uint256 amount) internal{ require(token!=address(0),"Invalid token address"); funds[msg.sender][token].available+=amount; diff --git a/contracts/escrow/Escrow.sol b/contracts/escrow/Escrow.sol index 99b0ef8f..3f6b9807 100644 --- a/contracts/escrow/Escrow.sol +++ b/contracts/escrow/Escrow.sol @@ -6,6 +6,7 @@ pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "../interfaces/IFactoryRouter.sol"; @@ -115,6 +116,40 @@ contract Escrow is _deposit(token[i],amount[i]); } } + + /** + * @dev depositWithPermit + * Called by payer to deposit funds in the contract using ERC20Permit + * This function allows users to deposit without a separate approval transaction + * + * @param token token to deposit + * @param amount amount in wei to deposit + * @param deadline The time at which the permit expires (unix timestamp) + * @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 depositWithPermit( + address token, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external nonReentrant { + // Use permit to approve this contract to spend user's tokens + IERC20Permit(token).permit( + msg.sender, + address(this), + amount, + deadline, + v, + r, + s + ); + // Deposit the tokens + _deposit(token, amount); + } function _deposit(address token,uint256 amount) internal{ require(token!=address(0),"Invalid token address"); funds[msg.sender][token].available+=amount; diff --git a/contracts/utils/mock/MockERC20Permit.sol b/contracts/utils/mock/MockERC20Permit.sol new file mode 100644 index 00000000..42e2aee0 --- /dev/null +++ b/contracts/utils/mock/MockERC20Permit.sol @@ -0,0 +1,37 @@ +pragma solidity 0.8.12; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; + +/** + * @dev Mock ERC20 token with configurable decimals and ERC20Permit support. + * This is a test utility contract that implements ERC20Permit for testing permit functionality. + */ +contract MockERC20Permit is ERC20, ERC20Permit { + uint8 private _decimals; + + /** + * @dev Sets the values for {name}, {symbol}, and {decimals}. + * @param name_ Token name + * @param symbol_ Token symbol + * @param decimals_ Number of decimals + */ + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) ERC20(name_, symbol_) ERC20Permit(name_) { + _decimals = decimals_; + _mint(msg.sender, 10e25); + } + + /** + * @dev Returns the number of decimals used to get its user representation. + */ + function decimals() public view virtual override returns (uint8) { + return _decimals; + } +} diff --git a/scripts/deploy_enterpriseescrow.js b/scripts/deploy_enterpriseescrow.js index 74bfa195..e4847add 100644 --- a/scripts/deploy_enterpriseescrow.js +++ b/scripts/deploy_enterpriseescrow.js @@ -49,7 +49,7 @@ async function main() { case 1: networkName = "mainnet"; gasLimit = 6500000; - gasPrice = ethers.utils.parseUnits("1.15", "gwei"); + gasPrice = ethers.utils.parseUnits("0.08", "gwei"); break; case 10: networkName = "optimism"; @@ -58,7 +58,7 @@ async function main() { break; case 11155111: networkName = "sepolia"; - gasPrice = ethers.utils.parseUnits("0.001000011", "gwei"); + gasPrice = ethers.utils.parseUnits("5", "gwei"); gasLimit = 6500000; break; } @@ -99,7 +99,7 @@ async function main() { addresses.EnterpriseFeeCollector, options ); - await deployEscrow.deployTransaction.wait(); + await deployEscrow.deployTransaction.wait(1); if (show_verify) { console.log("\tRun the following to verify on etherscan"); console.log( @@ -116,13 +116,6 @@ async function main() { if (addressFile) { // write address.json if needed oldAddresses[networkName] = addresses; - if (logging) - console.info( - "writing to " + - addressFile + - "\r\n" + - JSON.stringify(oldAddresses, null, 2) - ); try { fs.writeFileSync(addressFile, JSON.stringify(oldAddresses, null, 2)); } catch (e) { diff --git a/scripts/deploy_escrow.js b/scripts/deploy_escrow.js index 2f4a70ff..a2389902 100644 --- a/scripts/deploy_escrow.js +++ b/scripts/deploy_escrow.js @@ -92,7 +92,7 @@ async function main() { networkName = "sepolia"; OPFOwner = '0xC7EC1970B09224B317c52d92f37F5e1E4fF6B687'; RouterAddress = "0x2112Eb973af1DBf83a4f11eda82f7a7527D7Fde5"; - gasPrice = ethers.utils.parseUnits('0.001000011', 'gwei') + gasPrice = ethers.utils.parseUnits('5', 'gwei') gasLimit = 5000000 break; case 11155420: @@ -167,7 +167,7 @@ async function main() { ); const deployEscrow = await Escrow.connect(owner).deploy(RouterAddress,addresses.OPFCommunityFeeCollectorCompute,options) - await deployEscrow.deployTransaction.wait(); + await deployEscrow.deployTransaction.wait(1); if (show_verify) { console.log("\tRun the following to verify on etherscan"); console.log("\tnpx hardhat verify --network " + networkName + " " + deployEscrow.address+ " " + RouterAddress + " " + addresses.OPFCommunityFeeCollectorCompute) diff --git a/test/unit/escrow/EnterpriseEscrow.test.js b/test/unit/escrow/EnterpriseEscrow.test.js index 72ca083c..f92e809d 100644 --- a/test/unit/escrow/EnterpriseEscrow.test.js +++ b/test/unit/escrow/EnterpriseEscrow.test.js @@ -15,10 +15,46 @@ const fastForward = async (seconds) => { await ethers.provider.send("evm_increaseTime", [seconds]); await ethers.provider.send("evm_mine"); } + +// Helper function to sign ERC20Permit data +async function signPermit(signer, token, spender, amount, deadline, nonce) { + const name = await token.name(); + const chainId = (await ethers.provider.getNetwork()).chainId; + + const domain = { + name: name, + version: "1", + chainId: chainId, + verifyingContract: token.address, + }; + + const types = { + Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], + }; + + const value = { + owner: signer.address, + spender: spender, + value: amount, + nonce: nonce, + deadline: deadline, + }; + + const signature = await signer._signTypedData(domain, types, value); + return ethers.utils.splitSignature(signature); +} + // Start test block describe('Escrow tests', function () { let Mock20Contract; let Mock20DecimalsContract; + let Mock20PermitContract; let EscrowContract; let FactoryRouter let signers; @@ -37,11 +73,14 @@ describe('Escrow tests', function () { const EnterpriseFeeCollector = await ethers.getContractFactory("EnterpriseFeeCollector"); const MockErc20 = await ethers.getContractFactory('MockERC20'); const MockErc20Decimals = await ethers.getContractFactory('MockERC20Decimals'); + const MockErc20Permit = await ethers.getContractFactory('MockERC20Permit'); const Escrow = await ethers.getContractFactory('EnterpriseEscrow'); Mock20Contract = await MockErc20.deploy(signers[0].address,"MockERC20", 'MockERC20'); Mock20DecimalsContract = await MockErc20Decimals.deploy("Mock6Digits", 'Mock6Digits', 6); + Mock20PermitContract = await MockErc20Permit.deploy("MockPermit", 'MPERMIT', 18); await Mock20Contract.deployed(); await Mock20DecimalsContract.deployed(); + await Mock20PermitContract.deployed(); // DEPLOY ROUTER, SETTING OWNER FactoryRouter = await Router.deploy( signers[0].address, @@ -62,8 +101,13 @@ describe('Escrow tests', function () { await Mock20DecimalsContract.transfer(payer1.address,ethers.utils.parseUnits("10000", 6)) await Mock20DecimalsContract.transfer(payer2.address,ethers.utils.parseUnits("10000", 6)) await Mock20DecimalsContract.transfer(payer3.address,ethers.utils.parseUnits("10000", 6)) + // Transfer permit tokens to payers + await Mock20PermitContract.transfer(payer1.address,web3.utils.toWei("10000")) + await Mock20PermitContract.transfer(payer2.address,web3.utils.toWei("10000")) + await Mock20PermitContract.transfer(payer3.address,web3.utils.toWei("10000")) await EnterpriseFeeCollectorContract.connect(payer1).updateToken(Mock20Contract.address, 1, 10, ethers.utils.parseEther('0.01'), true); await EnterpriseFeeCollectorContract.connect(payer1).updateToken(Mock20DecimalsContract.address, 5, 50, ethers.utils.parseEther('0.1'), true); + await EnterpriseFeeCollectorContract.connect(payer1).updateToken(Mock20PermitContract.address, 1, 10, ethers.utils.parseEther('0.01'), true); }); @@ -337,4 +381,152 @@ it('Escrow - lock', async function () { await EscrowContract.connect(payer1).withdraw([Mock20Contract.address],[payer1Funds.available]); expect(await EscrowContract.connect(payer1).getUserTokens(payer1.address)).does.not.include(Mock20Contract.address); }); + + it('Escrow - depositWithPermit', async function () { + const depositAmount = web3.utils.toWei("100"); + const block = await ethers.provider.getBlock("latest"); + const deadline = block.timestamp + 3600; // 1 hour from now + const nonce = await Mock20PermitContract.nonces(payer1.address); + + // Get initial balances + const contractBalanceBefore = await Mock20PermitContract.balanceOf(EscrowContract.address); + const payerBalanceBefore = await Mock20PermitContract.balanceOf(payer1.address); + const fundTokensBefore = await EscrowContract.connect(payer1).getUserTokens(payer1.address); + + // Sign permit + const { v, r, s } = await signPermit( + payer1, + Mock20PermitContract, + EscrowContract.address, + depositAmount, + deadline, + nonce + ); + + // Deposit with permit (no prior approval needed) + const tx = await EscrowContract.connect(payer1).depositWithPermit( + Mock20PermitContract.address, + depositAmount, + deadline, + v, + r, + s + ); + const txReceipt = await tx.wait(); + + // Check balances after deposit + const contractBalanceAfter = await Mock20PermitContract.balanceOf(EscrowContract.address); + const payerBalanceAfter = await Mock20PermitContract.balanceOf(payer1.address); + const fundTokensAfter = await EscrowContract.connect(payer1).getUserTokens(payer1.address); + + // Verify balances + expect(contractBalanceAfter).to.equal(contractBalanceBefore.add(depositAmount)); + expect(payerBalanceAfter).to.equal(payerBalanceBefore.sub(depositAmount)); + expect(fundTokensAfter).to.include(Mock20PermitContract.address); + + // Verify funds + const funds = await EscrowContract.connect(payer1).getFunds(Mock20PermitContract.address); + expect(funds.available).to.equal(depositAmount); + expect(funds.locked).to.equal(0); + + // Check event + const event = getEventFromTx(txReceipt, "Deposit"); + expect(event).to.exist; + expect(event.args.payer).to.equal(payer1.address); + expect(event.args.token).to.equal(Mock20PermitContract.address); + expect(event.args.amount).to.equal(depositAmount); + + // Verify allowance was consumed + const allowance = await Mock20PermitContract.allowance(payer1.address, EscrowContract.address); + expect(allowance).to.equal(0); + }); + + it('Escrow - depositWithPermit should revert with expired deadline', async function () { + const depositAmount = web3.utils.toWei("100"); + const block = await ethers.provider.getBlock("latest"); + const expiredDeadline = block.timestamp - 3600; // 1 hour ago + const nonce = await Mock20PermitContract.nonces(payer2.address); + + const { v, r, s } = await signPermit( + payer2, + Mock20PermitContract, + EscrowContract.address, + depositAmount, + expiredDeadline, + nonce + ); + + await expect( + EscrowContract.connect(payer2).depositWithPermit( + Mock20PermitContract.address, + depositAmount, + expiredDeadline, + v, + r, + s + ) + ).to.be.revertedWith("ERC20Permit: expired deadline"); + }); + + it('Escrow - depositWithPermit should revert with invalid signature', async function () { + const depositAmount = web3.utils.toWei("100"); + const block = await ethers.provider.getBlock("latest"); + const deadline = block.timestamp + 3600; + const nonce = await Mock20PermitContract.nonces(payer2.address); + + // Sign with wrong signer (payer3 instead of payer2) + const { v, r, s } = await signPermit( + payer3, + Mock20PermitContract, + EscrowContract.address, + depositAmount, + deadline, + nonce + ); + + await expect( + EscrowContract.connect(payer2).depositWithPermit( + Mock20PermitContract.address, + depositAmount, + deadline, + v, + r, + s + ) + ).to.be.revertedWith("ERC20Permit: invalid signature"); + }); + + it('Escrow - depositWithPermit works without prior approval', async function () { + const depositAmount = web3.utils.toWei("50"); + const block = await ethers.provider.getBlock("latest"); + const deadline = block.timestamp + 3600; + const nonce = await Mock20PermitContract.nonces(payer3.address); + + // Verify no allowance before + const allowanceBefore = await Mock20PermitContract.allowance(payer3.address, EscrowContract.address); + expect(allowanceBefore).to.equal(0); + + const { v, r, s } = await signPermit( + payer3, + Mock20PermitContract, + EscrowContract.address, + depositAmount, + deadline, + nonce + ); + + // Deposit with permit (no prior approval needed) + await EscrowContract.connect(payer3).depositWithPermit( + Mock20PermitContract.address, + depositAmount, + deadline, + v, + r, + s + ); + + // Verify deposit succeeded + const funds = await EscrowContract.connect(payer3).getFunds(Mock20PermitContract.address); + expect(funds.available).to.equal(depositAmount); + }); }); \ No newline at end of file diff --git a/test/unit/escrow/Escrow.test.js b/test/unit/escrow/Escrow.test.js index 3cc1dd03..7c9dc6ff 100644 --- a/test/unit/escrow/Escrow.test.js +++ b/test/unit/escrow/Escrow.test.js @@ -15,10 +15,46 @@ const fastForward = async (seconds) => { await ethers.provider.send("evm_increaseTime", [seconds]); await ethers.provider.send("evm_mine"); } + +// Helper function to sign ERC20Permit data +async function signPermit(signer, token, spender, amount, deadline, nonce) { + const name = await token.name(); + const chainId = (await ethers.provider.getNetwork()).chainId; + + const domain = { + name: name, + version: "1", + chainId: chainId, + verifyingContract: token.address, + }; + + const types = { + Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], + }; + + const value = { + owner: signer.address, + spender: spender, + value: amount, + nonce: nonce, + deadline: deadline, + }; + + const signature = await signer._signTypedData(domain, types, value); + return ethers.utils.splitSignature(signature); +} + // Start test block describe('Escrow tests', function () { let Mock20Contract; let Mock20DecimalsContract; + let Mock20PermitContract; let EscrowContract; let FactoryRouter let signers; @@ -36,11 +72,14 @@ describe('Escrow tests', function () { const Router = await ethers.getContractFactory("FactoryRouter"); const MockErc20 = await ethers.getContractFactory('MockERC20'); const MockErc20Decimals = await ethers.getContractFactory('MockERC20Decimals'); + const MockErc20Permit = await ethers.getContractFactory('MockERC20Permit'); const Escrow = await ethers.getContractFactory('Escrow'); Mock20Contract = await MockErc20.deploy(signers[0].address,"MockERC20", 'MockERC20'); Mock20DecimalsContract = await MockErc20Decimals.deploy("Mock6Digits", 'Mock6Digits', 6); + Mock20PermitContract = await MockErc20Permit.deploy("MockPermit", 'MPERMIT', 18); await Mock20Contract.deployed(); await Mock20DecimalsContract.deployed(); + await Mock20PermitContract.deployed(); // DEPLOY ROUTER, SETTING OWNER FactoryRouter = await Router.deploy( signers[0].address, @@ -59,6 +98,10 @@ describe('Escrow tests', function () { await Mock20DecimalsContract.transfer(payer1.address,ethers.utils.parseUnits("10000", 6)) await Mock20DecimalsContract.transfer(payer2.address,ethers.utils.parseUnits("10000", 6)) await Mock20DecimalsContract.transfer(payer3.address,ethers.utils.parseUnits("10000", 6)) + // Transfer permit tokens to payers + await Mock20PermitContract.transfer(payer1.address,web3.utils.toWei("10000")) + await Mock20PermitContract.transfer(payer2.address,web3.utils.toWei("10000")) + await Mock20PermitContract.transfer(payer3.address,web3.utils.toWei("10000")) }); @@ -334,4 +377,152 @@ it('Escrow - lock', async function () { await EscrowContract.connect(payer1).withdraw([Mock20Contract.address],[payer1Funds.available]); expect(await EscrowContract.connect(payer1).getUserTokens(payer1.address)).does.not.include(Mock20Contract.address); }); + + it('Escrow - depositWithPermit', async function () { + const depositAmount = web3.utils.toWei("100"); + const block = await ethers.provider.getBlock("latest"); + const deadline = block.timestamp + 3600; // 1 hour from now + const nonce = await Mock20PermitContract.nonces(payer1.address); + + // Get initial balances + const contractBalanceBefore = await Mock20PermitContract.balanceOf(EscrowContract.address); + const payerBalanceBefore = await Mock20PermitContract.balanceOf(payer1.address); + const fundTokensBefore = await EscrowContract.connect(payer1).getUserTokens(payer1.address); + + // Sign permit + const { v, r, s } = await signPermit( + payer1, + Mock20PermitContract, + EscrowContract.address, + depositAmount, + deadline, + nonce + ); + + // Deposit with permit (no prior approval needed) + const tx = await EscrowContract.connect(payer1).depositWithPermit( + Mock20PermitContract.address, + depositAmount, + deadline, + v, + r, + s + ); + const txReceipt = await tx.wait(); + + // Check balances after deposit + const contractBalanceAfter = await Mock20PermitContract.balanceOf(EscrowContract.address); + const payerBalanceAfter = await Mock20PermitContract.balanceOf(payer1.address); + const fundTokensAfter = await EscrowContract.connect(payer1).getUserTokens(payer1.address); + + // Verify balances + expect(contractBalanceAfter).to.equal(contractBalanceBefore.add(depositAmount)); + expect(payerBalanceAfter).to.equal(payerBalanceBefore.sub(depositAmount)); + expect(fundTokensAfter).to.include(Mock20PermitContract.address); + + // Verify funds + const funds = await EscrowContract.connect(payer1).getFunds(Mock20PermitContract.address); + expect(funds.available).to.equal(depositAmount); + expect(funds.locked).to.equal(0); + + // Check event + const event = getEventFromTx(txReceipt, "Deposit"); + expect(event).to.exist; + expect(event.args.payer).to.equal(payer1.address); + expect(event.args.token).to.equal(Mock20PermitContract.address); + expect(event.args.amount).to.equal(depositAmount); + + // Verify allowance was consumed + const allowance = await Mock20PermitContract.allowance(payer1.address, EscrowContract.address); + expect(allowance).to.equal(0); + }); + + it('Escrow - depositWithPermit should revert with expired deadline', async function () { + const depositAmount = web3.utils.toWei("100"); + const block = await ethers.provider.getBlock("latest"); + const expiredDeadline = block.timestamp - 3600; // 1 hour ago + const nonce = await Mock20PermitContract.nonces(payer2.address); + + const { v, r, s } = await signPermit( + payer2, + Mock20PermitContract, + EscrowContract.address, + depositAmount, + expiredDeadline, + nonce + ); + + await expect( + EscrowContract.connect(payer2).depositWithPermit( + Mock20PermitContract.address, + depositAmount, + expiredDeadline, + v, + r, + s + ) + ).to.be.revertedWith("ERC20Permit: expired deadline"); + }); + + it('Escrow - depositWithPermit should revert with invalid signature', async function () { + const depositAmount = web3.utils.toWei("100"); + const block = await ethers.provider.getBlock("latest"); + const deadline = block.timestamp + 3600; + const nonce = await Mock20PermitContract.nonces(payer2.address); + + // Sign with wrong signer (payer3 instead of payer2) + const { v, r, s } = await signPermit( + payer3, + Mock20PermitContract, + EscrowContract.address, + depositAmount, + deadline, + nonce + ); + + await expect( + EscrowContract.connect(payer2).depositWithPermit( + Mock20PermitContract.address, + depositAmount, + deadline, + v, + r, + s + ) + ).to.be.revertedWith("ERC20Permit: invalid signature"); + }); + + it('Escrow - depositWithPermit works without prior approval', async function () { + const depositAmount = web3.utils.toWei("50"); + const block = await ethers.provider.getBlock("latest"); + const deadline = block.timestamp + 3600; + const nonce = await Mock20PermitContract.nonces(payer3.address); + + // Verify no allowance before + const allowanceBefore = await Mock20PermitContract.allowance(payer3.address, EscrowContract.address); + expect(allowanceBefore).to.equal(0); + + const { v, r, s } = await signPermit( + payer3, + Mock20PermitContract, + EscrowContract.address, + depositAmount, + deadline, + nonce + ); + + // Deposit with permit (no prior approval needed) + await EscrowContract.connect(payer3).depositWithPermit( + Mock20PermitContract.address, + depositAmount, + deadline, + v, + r, + s + ); + + // Verify deposit succeeded + const funds = await EscrowContract.connect(payer3).getFunds(Mock20PermitContract.address); + expect(funds.available).to.equal(depositAmount); + }); }); \ No newline at end of file