From d2eaf1b0fc181d9eb10232204c161f592c39b6cb Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Wed, 28 May 2025 02:04:06 +0300 Subject: [PATCH 1/6] add fees on escrow --- contracts/escrow/Escrow.sol | 19 +++++++++- scripts/deploy-contracts.js | 2 +- test/unit/escrow/Escrow.test.js | 61 ++++++++++++++++++++++++++++++--- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/contracts/escrow/Escrow.sol b/contracts/escrow/Escrow.sol index b65658ed..6a7b5d4b 100644 --- a/contracts/escrow/Escrow.sol +++ b/contracts/escrow/Escrow.sol @@ -8,6 +8,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "../interfaces/IFactoryRouter.sol"; /** * @title Escrow contract @@ -32,6 +33,8 @@ contract Escrow is using SafeMath for uint256; using SafeERC20 for IERC20; + // OPC fee router + address public factoryRouter; /* User funds are stored per user and per token */ struct userFunds{ @@ -74,6 +77,11 @@ contract Escrow is event Claimed(address indexed payee,uint256 jobId,address token,address indexed payer,uint256 amount,bytes proof); event Canceled(address indexed payee,uint256 jobId,address token,address indexed payer,uint256 amount); + // Add constructor to set router + constructor(address _factoryRouter) { + require(_factoryRouter != address(0), "Invalid router"); + factoryRouter = _factoryRouter; + } /* Payer actions */ /** @@ -475,11 +483,20 @@ contract Escrow is userAuths[payer][token][i].currentLocks-=1; } } + // OPC fee logic + uint256 opcFee = IFactoryRouter(factoryRouter).getOPCFee(token); + uint256 feeAmount = amount.mul(opcFee).div(1e18); + uint256 payout = amount.sub(feeAmount); + // Transfer OPC fee to collector if any + if(feeAmount > 0){ + address opcCollector = IFactoryRouter(factoryRouter).getOPCCollector(); + IERC20(token).safeTransfer(opcCollector, feeAmount); + } //update user funds funds[payer][token].available+=tempLock.amount-amount; funds[payer][token].locked-=tempLock.amount; //update payee balance - funds[msg.sender][token].available+=amount; + funds[msg.sender][token].available+=payout; //delete the lock if(index Date: Wed, 28 May 2025 02:10:13 +0300 Subject: [PATCH 2/6] fix --- scripts/deploy-contracts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deploy-contracts.js b/scripts/deploy-contracts.js index 0a184e9f..4f873307 100644 --- a/scripts/deploy-contracts.js +++ b/scripts/deploy-contracts.js @@ -67,6 +67,7 @@ async function main() { let sleepAmount = 10; let additionalApprovedTokens = [] let pdrTrueValSubmiter = null + let router console.log("Using chain " + networkDetails.chainId); switch (networkDetails.chainId) { case 1: @@ -442,7 +443,6 @@ async function main() { if (logging) console.log("Deploying Router"); const Router = await ethers.getContractFactory("FactoryRouter", owner); - let router if (options) router = await Router.connect(owner).deploy( owner.address, addresses.Ocean, From 9deb0bf020c3b01479f050b07ee3749f76d23dad Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Mon, 23 Jun 2025 09:27:08 +0300 Subject: [PATCH 3/6] add user token list --- contracts/escrow/Escrow.sol | 29 ++++++++++++++++++++++++++++- test/unit/escrow/Escrow.test.js | 13 ++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/contracts/escrow/Escrow.sol b/contracts/escrow/Escrow.sol index 6a7b5d4b..92714c80 100644 --- a/contracts/escrow/Escrow.sol +++ b/contracts/escrow/Escrow.sol @@ -43,7 +43,12 @@ contract Escrow is } mapping(address => mapping(address => userFunds)) private funds; // user -> token -> userFunds - + // Mapping from user to an array of token addresses they have funds in + mapping(address => address[]) private userTokens; + + // A helper mapping to avoid duplicates: user => token => hasTokenFunds + mapping(address => mapping(address => bool)) private hasFundsInToken; + /* Payee authorizations are stored per user and per token */ struct auth{ address payee; @@ -110,6 +115,10 @@ contract Escrow is function _deposit(address token,uint256 amount) internal{ require(token!=address(0),"Invalid token address"); funds[msg.sender][token].available+=amount; + if (!hasFundsInToken[msg.sender][token]) { + userTokens[msg.sender].push(token); + hasFundsInToken[msg.sender][token] = true; + } emit Deposit(msg.sender,token,amount); uint256 balanceBefore = IERC20(token).balanceOf(address(this)); IERC20(token).safeTransferFrom(msg.sender, address(this), amount); @@ -120,6 +129,7 @@ contract Escrow is } + /** * @dev withdraw * Called by payer to withdraw available (not locked) funds from the contract @@ -136,13 +146,30 @@ contract Escrow is function _withdraw(address token,uint256 amount) internal{ if(funds[msg.sender][token].available>=amount){ funds[msg.sender][token].available-=amount; + if(funds[msg.sender][token].available==0){ + address[] storage tokens = userTokens[msg.sender]; + for (uint256 i = 0; i < tokens.length; i++) { + if (tokens[i] == token) { + tokens[i] = tokens[tokens.length - 1]; // overwrite with last element + tokens.pop(); // remove last element + break; + } + } + // Update interaction status + hasFundsInToken[msg.sender][token] = false; + } emit Withdraw(msg.sender,token,amount); IERC20(token).safeTransfer( msg.sender, amount ); + } } + function getUserTokens(address user) external view returns (address[] memory) { + return userTokens[user]; + } + /** * @dev authorize diff --git a/test/unit/escrow/Escrow.test.js b/test/unit/escrow/Escrow.test.js index 9a36c4d7..f65427e9 100644 --- a/test/unit/escrow/Escrow.test.js +++ b/test/unit/escrow/Escrow.test.js @@ -69,11 +69,14 @@ describe('Escrow tests', function () { }); it('Escrow - deposit', async function () { + let fundTokens=await EscrowContract.connect(payer1).getUserTokens(payer1.address) + expect(fundTokens).to.be.empty; expect(await Mock20Contract.balanceOf(EscrowContract.address)).to.equal(0); expect(await Mock20DecimalsContract.balanceOf(EscrowContract.address)).to.equal(0); await Mock20Contract.connect(payer1).approve(EscrowContract.address, web3.utils.toWei("10000")); await EscrowContract.connect(payer1).deposit(Mock20Contract.address,web3.utils.toWei("100")); - + fundTokens=await EscrowContract.connect(payer1).getUserTokens(payer1.address) + expect(fundTokens).to.include(Mock20Contract.address); expect(await Mock20Contract.balanceOf(EscrowContract.address)).to.equal(web3.utils.toWei("100")); expect(await Mock20DecimalsContract.balanceOf(EscrowContract.address)).to.equal(0); const funds=await EscrowContract.connect(payer1).getFunds(Mock20Contract.address) @@ -93,8 +96,10 @@ it('Escrow - withdraw', async function () { expect(await Mock20Contract.balanceOf(EscrowContract.address)).to.equal(balanceMock20); await EscrowContract.connect(payer1).withdraw([Mock20Contract.address],[web3.utils.toWei("10")]); expect(await Mock20Contract.balanceOf(EscrowContract.address)).to.equal(web3.utils.toWei("90")); + expect(await EscrowContract.connect(payer1).getUserTokens(payer1.address)).to.include(Mock20Contract.address); }); + it('Escrow - auth', async function () { await EscrowContract.connect(payer1).authorizeMultiple([Mock20Contract.address],[payee1.address],[web3.utils.toWei("50")],[100],[2]); const auths=await EscrowContract.connect(payer1).getAuthorizations(Mock20Contract.address,payer1.address,payee1.address) @@ -322,4 +327,10 @@ it('Escrow - lock', async function () { await EscrowContract.connect(payer1).withdraw([Mock20DecimalsContract.address],[ethers.utils.parseUnits("10", 6)]); expect(await Mock20DecimalsContract.balanceOf(EscrowContract.address)).to.equal(ethers.utils.parseUnits("90", 6)); }); + it('Escrow - withdraw all funds', async function () { + expect(await EscrowContract.connect(payer1).getUserTokens(payer1.address)).to.include(Mock20Contract.address); + const payer1Funds=await EscrowContract.connect(payer1).getFunds(Mock20Contract.address) + await EscrowContract.connect(payer1).withdraw([Mock20Contract.address],[payer1Funds.available]); + expect(await EscrowContract.connect(payer1).getUserTokens(payer1.address)).does.not.include(Mock20Contract.address); + }); }); \ No newline at end of file From f9f26b2d26ee7fb9c24fa62c6e45364ccebcad7b Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Mon, 23 Jun 2025 09:42:23 +0300 Subject: [PATCH 4/6] allow custom OPCCollector --- contracts/escrow/Escrow.sol | 12 +++++++++--- test/unit/escrow/Escrow.test.js | 5 +++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/contracts/escrow/Escrow.sol b/contracts/escrow/Escrow.sol index 92714c80..6b362780 100644 --- a/contracts/escrow/Escrow.sol +++ b/contracts/escrow/Escrow.sol @@ -35,6 +35,7 @@ contract Escrow is // OPC fee router address public factoryRouter; + address public opcCollector; /* User funds are stored per user and per token */ struct userFunds{ @@ -83,9 +84,10 @@ contract Escrow is event Canceled(address indexed payee,uint256 jobId,address token,address indexed payer,uint256 amount); // Add constructor to set router - constructor(address _factoryRouter) { + constructor(address _factoryRouter,address _opcCollector) { require(_factoryRouter != address(0), "Invalid router"); factoryRouter = _factoryRouter; + opcCollector = _opcCollector; } /* Payer actions */ @@ -516,8 +518,12 @@ contract Escrow is uint256 payout = amount.sub(feeAmount); // Transfer OPC fee to collector if any if(feeAmount > 0){ - address opcCollector = IFactoryRouter(factoryRouter).getOPCCollector(); - IERC20(token).safeTransfer(opcCollector, feeAmount); + if(opcCollector==address(0)){ + IERC20(token).safeTransfer(IFactoryRouter(factoryRouter).getOPCCollector(), feeAmount); + } + else{ + IERC20(token).safeTransfer(opcCollector, feeAmount); + } } //update user funds funds[payer][token].available+=tempLock.amount-amount; diff --git a/test/unit/escrow/Escrow.test.js b/test/unit/escrow/Escrow.test.js index f65427e9..f329aeaa 100644 --- a/test/unit/escrow/Escrow.test.js +++ b/test/unit/escrow/Escrow.test.js @@ -2,7 +2,8 @@ const { assert,expect } = require('chai'); const { ethers } = require("hardhat"); const { json } = require('hardhat/internal/core/params/argumentTypes'); const { web3 } = require("@openzeppelin/test-helpers/src/setup"); -const { getEventFromTx } = require("../../helpers/utils") +const { getEventFromTx } = require("../../helpers/utils"); + const addressZero = '0x0000000000000000000000000000000000000000'; @@ -49,7 +50,7 @@ describe('Escrow tests', function () { [] ); await FactoryRouter.deployed(); - EscrowContract = await Escrow.deploy(FactoryRouter.address); + EscrowContract = await Escrow.deploy(FactoryRouter.address,addressZero); await EscrowContract.deployed(); // top up accounts await Mock20Contract.transfer(payer1.address,web3.utils.toWei("10000")) From a9b0601db25315fb14641b50c257717d443b693a Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Mon, 23 Jun 2025 09:47:44 +0300 Subject: [PATCH 5/6] lint --- contracts/escrow/Escrow.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/escrow/Escrow.sol b/contracts/escrow/Escrow.sol index 6b362780..5f649473 100644 --- a/contracts/escrow/Escrow.sol +++ b/contracts/escrow/Escrow.sol @@ -34,8 +34,8 @@ contract Escrow is using SafeERC20 for IERC20; // OPC fee router - address public factoryRouter; - address public opcCollector; + address immutable public factoryRouter; + address immutable public opcCollector; /* User funds are stored per user and per token */ struct userFunds{ From dcf38d13c6539329d1a76379c6bb5a181290b96b Mon Sep 17 00:00:00 2001 From: alexcos20 Date: Mon, 23 Jun 2025 09:50:02 +0300 Subject: [PATCH 6/6] fix escrow deployment --- scripts/deploy-contracts.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/deploy-contracts.js b/scripts/deploy-contracts.js index 4f873307..0a8b7392 100644 --- a/scripts/deploy-contracts.js +++ b/scripts/deploy-contracts.js @@ -11,6 +11,7 @@ const { UV_FS_O_FILEMAP } = require("constants"); const ethers = hre.ethers; require("dotenv").config(); const DEAD_ADDRESS = "0x000000000000000000000000000000000000dEaD" +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; let shouldDeployV4 = true; let shouldDeployDF = true; let shouldDeployVE = true; @@ -933,7 +934,7 @@ async function main() { owner ); - const deployEscrow = await Escrow.connect(owner).deploy(router.address,options) + const deployEscrow = await Escrow.connect(owner).deploy(router.address,ZERO_ADDRESS,options) await deployEscrow.deployTransaction.wait(); if (show_verify) { console.log("\tRun the following to verify on etherscan");