diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c2f5cd7a..c5f7a40f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = v2.2.1 +current_version = v2.3.0a1 commit = True tag = True diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8ad6d14..b0b9a4d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,7 +184,7 @@ jobs: run: find ./artifacts/* -name "*.dbg.json" -type f -delete - name: Delete useless files run: rm -rf ./artifacts/build-info/* - - run: npm publish + - run: npm publish --tag next env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/addresses/address.json b/addresses/address.json index 0c2d5e01..d6234a65 100755 --- a/addresses/address.json +++ b/addresses/address.json @@ -142,7 +142,8 @@ "VestingWallet0": "0x8D011915C437AD5f5e3B0E4c6dd6380c92599f99", "VestingWalletA": "0xf9FB1f54eA825734E3a77e73A3864f4B46C815d9", "VestingWalletB": "0xf02e3163Dc3409D69D88D7AcDA613432E9A18741", - "VestingWalletC": "0x29F74B853C4B8D36273666FB63a3b71c754424Ed" + "VestingWalletC": "0x29F74B853C4B8D36273666FB63a3b71c754424Ed", + "OceanNodesBooster": "0x73558Ef3bb6543A8107ac032C5a98Da03ceb0eEf" }, "goerli": { "chainId": 5, @@ -296,7 +297,8 @@ "1": "0x9C9eE07b8Ce907D2f9244F8317C1Ed29A3193bAe" }, "Dispenser": "0x2720d405ef7cDC8a2E2e5AeBC8883C99611d893C", - "ERC721Factory": "0xEF62FB495266C72a5212A11Dce8baa79Ec0ABeB1" + "ERC721Factory": "0xEF62FB495266C72a5212A11Dce8baa79Ec0ABeB1", + "Escrow": "0x7b0576CF01E868bce46cca91b2a8E674141b0355" }, "oasis_saphire": { "chainId": 23294, @@ -321,7 +323,8 @@ "DFStrategyV1": "0x3c21a90599b5B7f37014cA5Bf30d3f1b73d7e391", "PredictoorHelper": "0xE9397625Df9B63f0C152f975234b7988b54710B8", "AccessListFactory": "0x12bB8D85a091A69A07E22E52d4567dBB91568f52", - "BatchPayments": "0x9497d1d64F2aFeBcd4f9916Eef3d9094E5Df962f" + "BatchPayments": "0x9497d1d64F2aFeBcd4f9916Eef3d9094E5Df962f", + "Escrow": "0x7b0576CF01E868bce46cca91b2a8E674141b0355" }, "optimism_sepolia": { "chainId": 11155420, @@ -338,7 +341,8 @@ "1": "0x3C5605202eD47C162450AE975415473e73F93072" }, "Dispenser": "0x30E4CC2C7A9c6aA2b2Ce93586E3Df24a3A00bcDD", - "ERC721Factory": "0xDEfD0018969cd2d4E648209F876ADe184815f038" + "ERC721Factory": "0xDEfD0018969cd2d4E648209F876ADe184815f038", + "Escrow": "0xA0329eFFa1370eAb1DC5998Db4292ae0F535a282" }, "optimism": { "chainId": 10, diff --git a/contracts/escrow/Escrow.sol b/contracts/escrow/Escrow.sol new file mode 100644 index 00000000..f5c59c51 --- /dev/null +++ b/contracts/escrow/Escrow.sol @@ -0,0 +1,421 @@ +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/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +/** + * @title Escrow contract + * + * @dev escrow contract between payer (aka user, consumer, etc) + * and payee (app/services that is performing a task which needs to be paid). + * + * The payer flow looks like: + * - payer deposits token + * - payer sets limit for payees (max amount, max process time) + * + * + * The payee flow looks like: + * - payer asks for service (like compute) offchain + * - payee computes the maximum amount and locks that amount in the escrow contract + * - payee performs the service + * - payee takes the actual amount from the lock and releases back the remaining + */ +contract Escrow is + ReentrancyGuard +{ + using SafeMath for uint256; + using SafeERC20 for IERC20; + + + /* User funds are stored per user and per token */ + struct userFunds{ + uint256 available; + uint256 locked; + } + + mapping(address => mapping(address => userFunds)) private funds; // user -> token -> userFunds + + /* Payee authorizations are stored per user and per token */ + struct auth{ + address payee; + uint256 maxLockedAmount; + uint256 currentLockedAmount; + uint256 maxLockSeconds; + uint256 maxLockCounts; + uint256 currentLocks; + } + + mapping(address => mapping(address => auth[])) private userAuths; // user -> token -> userAuths + + + // locks + struct lock{ + uint256 jobId; + address payer; + address payee; + uint256 amount; + uint256 expiry; + address token; + } + lock[] locks; + + // events + event Deposit(address indexed payer,address token,uint256 amount); + event Withdraw(address indexed payer,address token,uint256 amount); + event Auth(address indexed payer,address indexed payee,uint256 maxLockedAmount, + uint256 maxLockSeconds,uint256 maxLockCounts); + event Lock(address payer,address payee,uint256 jobId,uint256 amount,uint256 expiry,address token); + 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); + + /* Payer actions */ + + /** + * @dev deposit + * Called by payer to deposit funds in the contract + * + * @param token token to deposit + * @param amount amount in wei to deposit + */ + function deposit(address token,uint256 amount) external nonReentrant{ + require(token!=address(0),"Invalid token address"); + funds[msg.sender][token].available+=amount; + emit Deposit(msg.sender,token,amount); + _pullUnderlying(token,msg.sender,address(this),amount); + + } + + /** + * @dev withdraw + * Called by payer to withdraw available (not locked) funds from the contract + * + * @param token token to withdraw + * @param amount amount in wei to withdraw + */ + function withdraw(address token,uint256 amount) external nonReentrant{ + require(funds[msg.sender][token].available>=amount,"Not enough available funds"); + funds[msg.sender][token].available-=amount; + emit Withdraw(msg.sender,token,amount); + IERC20(token).safeTransfer( + msg.sender, + amount + ); + } + + /** + * @dev authorize + * Called by payer to authorize a payee to lock and claim funds + * + * @param token token to lock + * @param payee payee address + * @param maxLockedAmount maximum amount locked by payee in one lock + * @param maxLockSeconds maximum lock duration in seconds + * @param maxLockCounts maximum locks held by this payee + */ + function authorize(address token,address payee,uint256 maxLockedAmount, + uint256 maxLockSeconds,uint256 maxLockCounts) external{ + + require(token!=address(0),'Invalid token'); + require(payee!=address(0),'Invalid payee'); + uint256 i; + uint256 length=userAuths[msg.sender][token].length; + for(i=0;i0,"Invalid amount"); + require(jobId>0,"Invalid jobId"); + auth memory tempAuth=auth(address(0),0,0,0,0,0); + uint256 index; + require(funds[payer][token].available>=amount,"Payer does not have enough funds"); + uint256 length=userAuths[payer][token].length; + for(index=0;index0,'Invalid jobId'); + lock memory tempLock=lock(0,address(0),address(0),0,0,address(0)); + uint256 index; + uint256 length=locks.length; + for(index=0;index=amount,"Amount too high"); + + //update auths + length=userAuths[payer][token].length; + for(uint256 i=0;i= balanceBefore.add(amount), + "Transfer amount is too low" + ); + } +} \ No newline at end of file diff --git a/hardhat.config.js b/hardhat.config.js index a1fc0666..0de625b5 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -192,6 +192,7 @@ module.exports = { optimism_sepolia: process.env.ETHERSCAN_API_KEY, optimism: process.env.ETHERSCAN_API_KEY, sepolia: process.env.ETHERSCAN_API_KEY, + mainnet: process.env.ETHERSCAN_API_KEY, }, customChains: [ { diff --git a/package-lock.json b/package-lock.json index 8baa3097..bf0acdb3 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@oceanprotocol/contracts", - "version": "2.2.0", + "version": "2.3.0-next.1", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 725ac39b..32b1b5d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@oceanprotocol/contracts", - "version": "2.2.1", + "version": "2.3.0-next.1", "description": "Ocean Protocol Smartcontracts", "bugs": { "url": "https://github.com/oceanprotocol/contracts/issues" @@ -25,6 +25,7 @@ "scripts": { "test:full": "npx hardhat test test/unit/*/*.test.js test/flow/*.test.js ", "test:batch": "npx hardhat test test/flow/SwapBatch.test.js ", + "test:escrow": "npx hardhat test test/unit/escrow/Escrow.test.js ", "test:fre": "npx hardhat test test/flow/FixedRateExchange.test.js ", "test:dispenser": "npx hardhat test test/flow/Dispenser.test.js ", "test:debug": "npx hardhat test test/flow/Vesting.test.js ", diff --git a/scripts/check_deployment.js b/scripts/check_deployment.js index bd7e01eb..a020a9fe 100644 --- a/scripts/check_deployment.js +++ b/scripts/check_deployment.js @@ -16,7 +16,7 @@ async function testDeployment() { console.error("Missing development network") process.exit(1) } - keys = ["startBlock", "Router", "FixedPrice", "ERC20Template", "ERC721Template", "Dispenser", "ERC721Factory","AccessListFactory"] + keys = ["startBlock", "Router", "FixedPrice", "ERC20Template", "ERC721Template", "Dispenser", "ERC721Factory","AccessListFactory","Escrow"] for (const key of keys) { if (!(key in addresses['development'])) { console.error("Missing " + key + " deployment") diff --git a/scripts/deploy-contracts.js b/scripts/deploy-contracts.js index d9d10d78..883a44e4 100644 --- a/scripts/deploy-contracts.js +++ b/scripts/deploy-contracts.js @@ -901,6 +901,21 @@ async function main() { addresses.BatchPayments = deployBatchPayments.address; if (sleepAmount > 0) await sleep(sleepAmount) + // Escrow + if (logging) console.info("Deploying Escrow"); + const Escrow = await ethers.getContractFactory( + "Escrow", + owner + ); + + const deployEscrow = await Escrow.connect(owner).deploy(options) + await deployEscrow.deployTransaction.wait(); + if (show_verify) { + console.log("\tRun the following to verify on etherscan"); + console.log("\tnpx hardhat verify --network " + networkName + " " + deployEscrow.address) + } + addresses.Escrow = deployEscrow.address; + if (sleepAmount > 0) await sleep(sleepAmount) //DF contracts if (shouldDeployDF) { diff --git a/scripts/deploy_escrow.js b/scripts/deploy_escrow.js new file mode 100644 index 00000000..aa787349 --- /dev/null +++ b/scripts/deploy_escrow.js @@ -0,0 +1,105 @@ +// We require the Hardhat Runtime Environment explicitly here. This is optional +// but useful for running the script in a standalone fashion through `node