Skip to content

Commit 4bbea88

Browse files
committed
add permit to escrow contracts
1 parent cfb6abb commit 4bbea88

4 files changed

Lines changed: 453 additions & 0 deletions

File tree

contracts/escrow/EnterpriseEscrow.sol

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pragma solidity 0.8.12;
66
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
77
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
88
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9+
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
910
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
1011
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
1112
import "../interfaces/IEnterpriseFeeCollector.sol";
@@ -111,6 +112,40 @@ contract EnterpriseEscrow is
111112
_deposit(token[i],amount[i]);
112113
}
113114
}
115+
116+
/**
117+
* @dev depositWithPermit
118+
* Called by payer to deposit funds in the contract using ERC20Permit
119+
* This function allows users to deposit without a separate approval transaction
120+
*
121+
* @param token token to deposit
122+
* @param amount amount in wei to deposit
123+
* @param deadline The time at which the permit expires (unix timestamp)
124+
* @param v The recovery byte of the signature
125+
* @param r Half of the ECDSA signature pair
126+
* @param s Half of the ECDSA signature pair
127+
*/
128+
function depositWithPermit(
129+
address token,
130+
uint256 amount,
131+
uint256 deadline,
132+
uint8 v,
133+
bytes32 r,
134+
bytes32 s
135+
) external nonReentrant {
136+
// Use permit to approve this contract to spend user's tokens
137+
IERC20Permit(token).permit(
138+
msg.sender,
139+
address(this),
140+
amount,
141+
deadline,
142+
v,
143+
r,
144+
s
145+
);
146+
// Deposit the tokens
147+
_deposit(token, amount);
148+
}
114149
function _deposit(address token,uint256 amount) internal{
115150
require(token!=address(0),"Invalid token address");
116151
funds[msg.sender][token].available+=amount;

contracts/escrow/Escrow.sol

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pragma solidity 0.8.12;
66
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
77
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
88
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9+
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
910
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
1011
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
1112
import "../interfaces/IFactoryRouter.sol";
@@ -115,6 +116,40 @@ contract Escrow is
115116
_deposit(token[i],amount[i]);
116117
}
117118
}
119+
120+
/**
121+
* @dev depositWithPermit
122+
* Called by payer to deposit funds in the contract using ERC20Permit
123+
* This function allows users to deposit without a separate approval transaction
124+
*
125+
* @param token token to deposit
126+
* @param amount amount in wei to deposit
127+
* @param deadline The time at which the permit expires (unix timestamp)
128+
* @param v The recovery byte of the signature
129+
* @param r Half of the ECDSA signature pair
130+
* @param s Half of the ECDSA signature pair
131+
*/
132+
function depositWithPermit(
133+
address token,
134+
uint256 amount,
135+
uint256 deadline,
136+
uint8 v,
137+
bytes32 r,
138+
bytes32 s
139+
) external nonReentrant {
140+
// Use permit to approve this contract to spend user's tokens
141+
IERC20Permit(token).permit(
142+
msg.sender,
143+
address(this),
144+
amount,
145+
deadline,
146+
v,
147+
r,
148+
s
149+
);
150+
// Deposit the tokens
151+
_deposit(token, amount);
152+
}
118153
function _deposit(address token,uint256 amount) internal{
119154
require(token!=address(0),"Invalid token address");
120155
funds[msg.sender][token].available+=amount;

test/unit/escrow/EnterpriseEscrow.test.js

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,46 @@ const fastForward = async (seconds) => {
1515
await ethers.provider.send("evm_increaseTime", [seconds]);
1616
await ethers.provider.send("evm_mine");
1717
}
18+
19+
// Helper function to sign ERC20Permit data
20+
async function signPermit(signer, token, spender, amount, deadline, nonce) {
21+
const name = await token.name();
22+
const chainId = (await ethers.provider.getNetwork()).chainId;
23+
24+
const domain = {
25+
name: name,
26+
version: "1",
27+
chainId: chainId,
28+
verifyingContract: token.address,
29+
};
30+
31+
const types = {
32+
Permit: [
33+
{ name: "owner", type: "address" },
34+
{ name: "spender", type: "address" },
35+
{ name: "value", type: "uint256" },
36+
{ name: "nonce", type: "uint256" },
37+
{ name: "deadline", type: "uint256" },
38+
],
39+
};
40+
41+
const value = {
42+
owner: signer.address,
43+
spender: spender,
44+
value: amount,
45+
nonce: nonce,
46+
deadline: deadline,
47+
};
48+
49+
const signature = await signer._signTypedData(domain, types, value);
50+
return ethers.utils.splitSignature(signature);
51+
}
52+
1853
// Start test block
1954
describe('Escrow tests', function () {
2055
let Mock20Contract;
2156
let Mock20DecimalsContract;
57+
let Mock20PermitContract;
2258
let EscrowContract;
2359
let FactoryRouter
2460
let signers;
@@ -37,11 +73,14 @@ describe('Escrow tests', function () {
3773
const EnterpriseFeeCollector = await ethers.getContractFactory("EnterpriseFeeCollector");
3874
const MockErc20 = await ethers.getContractFactory('MockERC20');
3975
const MockErc20Decimals = await ethers.getContractFactory('MockERC20Decimals');
76+
const MockErc20Permit = await ethers.getContractFactory('MockERC20Permit');
4077
const Escrow = await ethers.getContractFactory('EnterpriseEscrow');
4178
Mock20Contract = await MockErc20.deploy(signers[0].address,"MockERC20", 'MockERC20');
4279
Mock20DecimalsContract = await MockErc20Decimals.deploy("Mock6Digits", 'Mock6Digits', 6);
80+
Mock20PermitContract = await MockErc20Permit.deploy("MockPermit", 'MPERMIT', 18);
4381
await Mock20Contract.deployed();
4482
await Mock20DecimalsContract.deployed();
83+
await Mock20PermitContract.deployed();
4584
// DEPLOY ROUTER, SETTING OWNER
4685
FactoryRouter = await Router.deploy(
4786
signers[0].address,
@@ -62,8 +101,13 @@ describe('Escrow tests', function () {
62101
await Mock20DecimalsContract.transfer(payer1.address,ethers.utils.parseUnits("10000", 6))
63102
await Mock20DecimalsContract.transfer(payer2.address,ethers.utils.parseUnits("10000", 6))
64103
await Mock20DecimalsContract.transfer(payer3.address,ethers.utils.parseUnits("10000", 6))
104+
// Transfer permit tokens to payers
105+
await Mock20PermitContract.transfer(payer1.address,web3.utils.toWei("10000"))
106+
await Mock20PermitContract.transfer(payer2.address,web3.utils.toWei("10000"))
107+
await Mock20PermitContract.transfer(payer3.address,web3.utils.toWei("10000"))
65108
await EnterpriseFeeCollectorContract.connect(payer1).updateToken(Mock20Contract.address, 1, 10, ethers.utils.parseEther('0.01'), true);
66109
await EnterpriseFeeCollectorContract.connect(payer1).updateToken(Mock20DecimalsContract.address, 5, 50, ethers.utils.parseEther('0.1'), true);
110+
await EnterpriseFeeCollectorContract.connect(payer1).updateToken(Mock20PermitContract.address, 1, 10, ethers.utils.parseEther('0.01'), true);
67111
});
68112

69113

@@ -337,4 +381,152 @@ it('Escrow - lock', async function () {
337381
await EscrowContract.connect(payer1).withdraw([Mock20Contract.address],[payer1Funds.available]);
338382
expect(await EscrowContract.connect(payer1).getUserTokens(payer1.address)).does.not.include(Mock20Contract.address);
339383
});
384+
385+
it('Escrow - depositWithPermit', async function () {
386+
const depositAmount = web3.utils.toWei("100");
387+
const block = await ethers.provider.getBlock("latest");
388+
const deadline = block.timestamp + 3600; // 1 hour from now
389+
const nonce = await Mock20PermitContract.nonces(payer1.address);
390+
391+
// Get initial balances
392+
const contractBalanceBefore = await Mock20PermitContract.balanceOf(EscrowContract.address);
393+
const payerBalanceBefore = await Mock20PermitContract.balanceOf(payer1.address);
394+
const fundTokensBefore = await EscrowContract.connect(payer1).getUserTokens(payer1.address);
395+
396+
// Sign permit
397+
const { v, r, s } = await signPermit(
398+
payer1,
399+
Mock20PermitContract,
400+
EscrowContract.address,
401+
depositAmount,
402+
deadline,
403+
nonce
404+
);
405+
406+
// Deposit with permit (no prior approval needed)
407+
const tx = await EscrowContract.connect(payer1).depositWithPermit(
408+
Mock20PermitContract.address,
409+
depositAmount,
410+
deadline,
411+
v,
412+
r,
413+
s
414+
);
415+
const txReceipt = await tx.wait();
416+
417+
// Check balances after deposit
418+
const contractBalanceAfter = await Mock20PermitContract.balanceOf(EscrowContract.address);
419+
const payerBalanceAfter = await Mock20PermitContract.balanceOf(payer1.address);
420+
const fundTokensAfter = await EscrowContract.connect(payer1).getUserTokens(payer1.address);
421+
422+
// Verify balances
423+
expect(contractBalanceAfter).to.equal(contractBalanceBefore.add(depositAmount));
424+
expect(payerBalanceAfter).to.equal(payerBalanceBefore.sub(depositAmount));
425+
expect(fundTokensAfter).to.include(Mock20PermitContract.address);
426+
427+
// Verify funds
428+
const funds = await EscrowContract.connect(payer1).getFunds(Mock20PermitContract.address);
429+
expect(funds.available).to.equal(depositAmount);
430+
expect(funds.locked).to.equal(0);
431+
432+
// Check event
433+
const event = getEventFromTx(txReceipt, "Deposit");
434+
expect(event).to.exist;
435+
expect(event.args.payer).to.equal(payer1.address);
436+
expect(event.args.token).to.equal(Mock20PermitContract.address);
437+
expect(event.args.amount).to.equal(depositAmount);
438+
439+
// Verify allowance was consumed
440+
const allowance = await Mock20PermitContract.allowance(payer1.address, EscrowContract.address);
441+
expect(allowance).to.equal(0);
442+
});
443+
444+
it('Escrow - depositWithPermit should revert with expired deadline', async function () {
445+
const depositAmount = web3.utils.toWei("100");
446+
const block = await ethers.provider.getBlock("latest");
447+
const expiredDeadline = block.timestamp - 3600; // 1 hour ago
448+
const nonce = await Mock20PermitContract.nonces(payer2.address);
449+
450+
const { v, r, s } = await signPermit(
451+
payer2,
452+
Mock20PermitContract,
453+
EscrowContract.address,
454+
depositAmount,
455+
expiredDeadline,
456+
nonce
457+
);
458+
459+
await expect(
460+
EscrowContract.connect(payer2).depositWithPermit(
461+
Mock20PermitContract.address,
462+
depositAmount,
463+
expiredDeadline,
464+
v,
465+
r,
466+
s
467+
)
468+
).to.be.revertedWith("ERC20Permit: expired deadline");
469+
});
470+
471+
it('Escrow - depositWithPermit should revert with invalid signature', async function () {
472+
const depositAmount = web3.utils.toWei("100");
473+
const block = await ethers.provider.getBlock("latest");
474+
const deadline = block.timestamp + 3600;
475+
const nonce = await Mock20PermitContract.nonces(payer2.address);
476+
477+
// Sign with wrong signer (payer3 instead of payer2)
478+
const { v, r, s } = await signPermit(
479+
payer3,
480+
Mock20PermitContract,
481+
EscrowContract.address,
482+
depositAmount,
483+
deadline,
484+
nonce
485+
);
486+
487+
await expect(
488+
EscrowContract.connect(payer2).depositWithPermit(
489+
Mock20PermitContract.address,
490+
depositAmount,
491+
deadline,
492+
v,
493+
r,
494+
s
495+
)
496+
).to.be.revertedWith("ERC20Permit: invalid signature");
497+
});
498+
499+
it('Escrow - depositWithPermit works without prior approval', async function () {
500+
const depositAmount = web3.utils.toWei("50");
501+
const block = await ethers.provider.getBlock("latest");
502+
const deadline = block.timestamp + 3600;
503+
const nonce = await Mock20PermitContract.nonces(payer3.address);
504+
505+
// Verify no allowance before
506+
const allowanceBefore = await Mock20PermitContract.allowance(payer3.address, EscrowContract.address);
507+
expect(allowanceBefore).to.equal(0);
508+
509+
const { v, r, s } = await signPermit(
510+
payer3,
511+
Mock20PermitContract,
512+
EscrowContract.address,
513+
depositAmount,
514+
deadline,
515+
nonce
516+
);
517+
518+
// Deposit with permit (no prior approval needed)
519+
await EscrowContract.connect(payer3).depositWithPermit(
520+
Mock20PermitContract.address,
521+
depositAmount,
522+
deadline,
523+
v,
524+
r,
525+
s
526+
);
527+
528+
// Verify deposit succeeded
529+
const funds = await EscrowContract.connect(payer3).getFunds(Mock20PermitContract.address);
530+
expect(funds.available).to.equal(depositAmount);
531+
});
340532
});

0 commit comments

Comments
 (0)