Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 221 additions & 24 deletions contracts/escrow/EnterpriseEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@
* The payer flow looks like:
* - payer deposits token
* - payer sets limit for payees (max amount, max process time)
*
*
* - payer can bundle multiple deposits (plain or via ERC20Permit) and authorizations
* in a single transaction using bundle()
*
*
* 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
* - while a lock is still active, the payee can reLock() it to change the amount and/or
* expiry (up or down). Every createLock check is re-evaluated against the new values, and
* the total lock lifetime (measured from the original creation) cannot exceed the
* authorization's maxLockSeconds.
*/
contract EnterpriseEscrow is
ReentrancyGuard
Expand Down Expand Up @@ -68,18 +74,31 @@
uint256 jobId;
address payer;
uint256 amount;
uint256 expiry;
uint256 expiry; // absolute timestamp: block.timestamp + duration
address token;
uint256 startTime; // block.timestamp at original createLock; preserved across reLock
}

mapping(address => lock[]) private locks; // locks by payee

/* structs used to bundle multiple payer actions in a single call */
struct DepositData { address token; uint256 amount; }
struct PermitData { address token; uint256 amount; uint256 deadline; uint8 v; bytes32 r; bytes32 s; }
struct AuthData { address token; address payee; uint256 maxLockedAmount; uint256 maxLockSeconds; uint256 maxLockCounts; }

/* structs used to bundle multiple payee (job) actions in a single call */
struct LockData { uint256 jobId; address token; address payer; uint256 amount; uint256 expiry; } // createLock & reLock
struct ClaimData { uint256 jobId; address token; address payer; uint256 amount; bytes proof; }
struct CancelData { uint256 jobId; address token; address payer; address payee; }

// 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,
event Auth(address indexed payer,address indexed payee,address token,uint256 maxLockedAmount,
uint256 maxLockSeconds,uint256 maxLockCounts);
event Lock(address payer,address payee,uint256 jobId,uint256 amount,uint256 expiry,address token);
event ReLock(address payer,address payee,uint256 jobId,uint256 oldAmount,uint256 newAmount,
uint256 newExpiry,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);

Expand All @@ -106,7 +125,7 @@
* @param token array of tokens to deposit
* @param amount array of amounts in wei to deposit
*/
function depositMultiple(address[] memory token,uint256[] memory amount) external nonReentrant{
function depositMultiple(address[] calldata token,uint256[] calldata amount) external nonReentrant{
require(token.length==amount.length,"Invalid input");
for(uint256 i=0;i<token.length;i++){
_deposit(token[i],amount[i]);
Expand All @@ -133,19 +152,93 @@
bytes32 r,
bytes32 s
) external nonReentrant {
_depositWithPermit(token, amount, deadline, v, r, s);
}
function _depositWithPermit(
address token,
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
// 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);
}

Check notice

Code scanning / Slither

Calls inside a loop Low

EnterpriseEscrow._depositWithPermit(address,uint256,uint256,uint8,bytes32,bytes32) has external calls inside a loop: IERC20Permit(token).permit(msg.sender,address(this),amount,deadline,v,r,s)
Calls stack containing the loop:
EnterpriseEscrow.bundle(EnterpriseEscrow.DepositData[],EnterpriseEscrow.PermitData[],EnterpriseEscrow.AuthData[])

/**
* @dev bundle
* Called by payer to bundle multiple deposits, permit-based deposits and authorizations
* in a single transaction (e.g. onboarding). All operations execute as msg.sender; any
* sub-array may be empty.
*
* @param deposits array of {token, amount} for plain deposits (require prior approval)
* @param permits array of {token, amount, deadline, v, r, s} for ERC20Permit-based deposits
* @param auths array of {token, payee, maxLockedAmount, maxLockSeconds, maxLockCounts}
*/
// Reentrancy-safe: nonReentrant blocks re-entry into every state-changing entrypoint; only view
// getters (the caller's own balances, not an oracle) are reachable during a token transfer/permit.
// slither-disable-next-line reentrancy-eth,reentrancy-benign
function bundle(
DepositData[] calldata deposits,
PermitData[] calldata permits,
AuthData[] calldata auths
) external nonReentrant {
for(uint256 i=0;i<deposits.length;i++){
_deposit(deposits[i].token,deposits[i].amount);
}
for(uint256 i=0;i<permits.length;i++){
_depositWithPermit(permits[i].token,permits[i].amount,permits[i].deadline,permits[i].v,permits[i].r,permits[i].s);
}
for(uint256 i=0;i<auths.length;i++){
_authorize(auths[i].token,auths[i].payee,auths[i].maxLockedAmount,auths[i].maxLockSeconds,auths[i].maxLockCounts);
}
}
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

/**
* @dev bundleJobs
* Called by a payee (e.g. a node servicing many jobs) to batch payee-side lock operations
* in a single transaction. Operations run as msg.sender in a fixed order:
* claims -> cancels -> creates -> reLocks, so lock slots / locked amounts freed by claims and
* cancels are available before new locks are created. Any sub-array may be empty.
*
* @param claims array of {jobId, token, payer, amount, proof} passed to claimLock
* @param cancels array of {jobId, token, payer, payee} passed to cancelExpiredLock
* @param newLocks array of {jobId, token, payer, amount, expiry} passed to createLock
* @param reLockOps array of {jobId, token, payer, amount, expiry} passed to reLock
*/
// Reentrancy-safe: nonReentrant blocks re-entry into every state-changing entrypoint; only view
// getters (the caller's own balances, not an oracle) are reachable during the claim fee transfer.
// slither-disable-next-line reentrancy-eth
function bundleJobs(
ClaimData[] calldata claims,
CancelData[] calldata cancels,
LockData[] calldata newLocks,
LockData[] calldata reLockOps
) external nonReentrant {
for(uint256 i=0;i<claims.length;i++){
_claimLock(claims[i].jobId,claims[i].token,claims[i].payer,claims[i].amount,claims[i].proof);
}
for(uint256 i=0;i<cancels.length;i++){
_cancelExpiredLock(cancels[i].jobId,cancels[i].token,cancels[i].payer,cancels[i].payee);
}
for(uint256 i=0;i<newLocks.length;i++){
_createLock(newLocks[i].jobId,newLocks[i].token,newLocks[i].payer,newLocks[i].amount,newLocks[i].expiry);
}
for(uint256 i=0;i<reLockOps.length;i++){
_reLock(reLockOps[i].jobId,reLockOps[i].token,reLockOps[i].payer,reLockOps[i].amount,reLockOps[i].expiry);
}
}
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
function _deposit(address token,uint256 amount) internal{
require(token!=address(0),"Invalid token address");
funds[msg.sender][token].available+=amount;
Expand All @@ -171,7 +264,7 @@
* @param token array of tokens to withdraw
* @param amount array of amounts in wei to withdraw
*/
function withdraw(address[] memory token,uint256[] memory amount) external nonReentrant{
function withdraw(address[] calldata token,uint256[] calldata amount) external nonReentrant{
require(token.length==amount.length,"Invalid input");
for(uint256 i=0;i<token.length;i++){
_withdraw(token[i],amount[i]);
Expand Down Expand Up @@ -230,8 +323,8 @@
* @param maxLockSeconds array of maximum lock duration in seconds
* @param maxLockCounts array of maximum locks held by this payee
*/
function authorizeMultiple(address[] memory token,address[] memory payee,uint256[] memory maxLockedAmount,
uint256[] memory maxLockSeconds,uint256[] memory maxLockCounts) external nonReentrant{
function authorizeMultiple(address[] calldata token,address[] calldata payee,uint256[] calldata maxLockedAmount,
uint256[] calldata maxLockSeconds,uint256[] calldata maxLockCounts) external nonReentrant{
require(token.length==payee.length &&
token.length==maxLockedAmount.length &&
token.length==maxLockSeconds.length &&
Expand Down Expand Up @@ -259,7 +352,7 @@
if(i==length){ //not found
userAuths[msg.sender][token].push(auth(payee,maxLockedAmount,0,maxLockSeconds,maxLockCounts,0));
}
emit Auth(msg.sender,payee,maxLockedAmount,maxLockSeconds,maxLockCounts);
emit Auth(msg.sender,payee,token,maxLockedAmount,maxLockSeconds,maxLockCounts);

}

Expand Down Expand Up @@ -374,8 +467,8 @@
* @param amount array of amounts in wei to lock
* @param expiry array of expiry timestamps
*/
function createLocks(uint256[] memory jobId,address[] memory token,
address[] memory payer,uint256[] memory amount,uint256[] memory expiry) external nonReentrant{
function createLocks(uint256[] calldata jobId,address[] calldata token,
address[] calldata payer,uint256[] calldata amount,uint256[] calldata expiry) external nonReentrant{

require(jobId.length==token.length &&
jobId.length==payer.length &&
Expand All @@ -387,7 +480,7 @@
}
function _createLock(uint256 jobId,address token,address payer,uint256 amount,uint256 expiry) internal {
require(payer!=address(0),'Invalid payer');
require(payer!=msg.sender,'Payeer cannot be payee');
require(payer!=msg.sender,'Payer cannot be payee');
require(token!=address(0),'Invalid token');
require(amount>0,"Invalid amount");
require(jobId>0,"Invalid jobId");
Expand Down Expand Up @@ -430,10 +523,107 @@
funds[payer][token].available-=amount;
funds[payer][token].locked+=amount;
// create the lock
locks[msg.sender].push(lock(jobId,payer,amount,block.timestamp+expiry,token));
locks[msg.sender].push(lock(jobId,payer,amount,block.timestamp+expiry,token,block.timestamp));
emit Lock(payer,msg.sender,jobId,amount,expiry,token);
}

/**
* @dev reLock
* Called by payee to change the amount and/or expiry of an existing, still active lock.
* Both amount and expiry can go up or down. All createLock checks (including the enterprise
* fee gate) are re-evaluated against the new values, and the total lock lifetime (measured
* from the original creation) cannot exceed the authorization's maxLockSeconds. jobId and
* the original startTime are preserved.
*
* @param jobId jobId, required (identifies the lock together with token and payer)
* @param token token, required
* @param payer payer address
* @param amount new amount in wei to lock
* @param expiry new expiry, relative seconds from now
*/
function reLock(uint256 jobId,address token,address payer,uint256 amount,uint256 expiry) external nonReentrant{
_reLock(jobId,token,payer,amount,expiry);
}
/**
* @dev reLocks
* Called by payee to reLock multiple existing locks
*
* @param jobId array of jobIds
* @param token array of tokens
* @param payer array of payer addresses
* @param amount array of new amounts in wei to lock
* @param expiry array of new expiries, relative seconds from now
*/
function reLocks(uint256[] calldata jobId,address[] calldata token,
address[] calldata payer,uint256[] calldata amount,uint256[] calldata expiry) external nonReentrant{

require(jobId.length==token.length &&
jobId.length==payer.length &&
jobId.length==amount.length &&
jobId.length==expiry.length,"Invalid input");
for(uint256 i=0;i<jobId.length;i++){
_reLock(jobId[i],token[i],payer[i],amount[i],expiry[i]);
}
}
function _reLock(uint256 jobId,address token,address payer,uint256 amount,uint256 expiry) internal {
require(payer!=address(0),'Invalid payer');
require(payer!=msg.sender,'Payer cannot be payee');
require(token!=address(0),'Invalid token');
require(amount>0,"Invalid amount");
require(jobId>0,"Invalid jobId");
// find the existing lock
lock memory tempLock=lock(0,address(0),0,0,address(0),0);
uint256 lockIndex;
uint256 length=locks[msg.sender].length;
for(lockIndex=0;lockIndex<length;lockIndex++){
if(
payer==locks[msg.sender][lockIndex].payer &&
jobId==locks[msg.sender][lockIndex].jobId &&
token==locks[msg.sender][lockIndex].token
){
tempLock=locks[msg.sender][lockIndex];
break;
}
}
require(tempLock.payer==payer,"Lock not found");
require(tempLock.expiry>=block.timestamp,"Lock expired");
// find the auth
auth memory tempAuth=auth(address(0),0,0,0,0,0);
uint256 index;
length=userAuths[payer][token].length;
for(index=0;index<length;index++){
if(msg.sender==userAuths[payer][token][index].payee) {
tempAuth=userAuths[payer][token][index];
break;
}
}
require(tempAuth.payee==msg.sender,"No auth found");
// re-run createLock checks against the new values, accounting for the old lock being removed
require(funds[payer][token].available+tempLock.amount>=amount,"Payer does not have enough funds");
require(block.timestamp+expiry<=tempLock.startTime+tempAuth.maxLockSeconds,"Expiry too high");
require(tempAuth.currentLockedAmount-tempLock.amount+amount<=tempAuth.maxLockedAmount,"Exceeds maxLockedAmount");
// check enterprise fee with the new amount
if(opcCollector != address(0)){
require(
IEnterpriseFeeCollector(opcCollector).isTokenAllowed(token),
"This token is not allowed by enterprise fee collector"
);
uint256 enterpriseFee = IEnterpriseFeeCollector(opcCollector).calculateFee(token,amount);
require(
enterpriseFee<amount,"Amount must be higher than enterprise fee"
);
}
// update auths (currentLocks unchanged - same slot)
userAuths[payer][token][index].currentLockedAmount=tempAuth.currentLockedAmount-tempLock.amount+amount;
// update user funds
funds[payer][token].available=funds[payer][token].available+tempLock.amount-amount;
funds[payer][token].locked=funds[payer][token].locked-tempLock.amount+amount;
// update the lock in place (jobId and startTime preserved)
locks[msg.sender][lockIndex].amount=amount;
locks[msg.sender][lockIndex].expiry=block.timestamp+expiry;
emit ReLock(payer,msg.sender,jobId,tempLock.amount,amount,expiry,token);
}

Check notice

Code scanning / Slither

Calls inside a loop Low

EnterpriseEscrow._reLock(uint256,address,address,uint256,uint256) has external calls inside a loop: require(bool,string)(IEnterpriseFeeCollector(opcCollector).isTokenAllowed(token),This token is not allowed by enterprise fee collector)
Calls stack containing the loop:
EnterpriseEscrow.bundleJobs(EnterpriseEscrow.ClaimData[],EnterpriseEscrow.CancelData[],EnterpriseEscrow.LockData[],EnterpriseEscrow.LockData[])

Check notice

Code scanning / Slither

Calls inside a loop Low

EnterpriseEscrow._reLock(uint256,address,address,uint256,uint256) has external calls inside a loop: enterpriseFee = IEnterpriseFeeCollector(opcCollector).calculateFee(token,amount)
Calls stack containing the loop:
EnterpriseEscrow.reLocks(uint256[],address[],address[],uint256[],uint256[])

Check notice

Code scanning / Slither

Calls inside a loop Low

EnterpriseEscrow._reLock(uint256,address,address,uint256,uint256) has external calls inside a loop: require(bool,string)(IEnterpriseFeeCollector(opcCollector).isTokenAllowed(token),This token is not allowed by enterprise fee collector)
Calls stack containing the loop:
EnterpriseEscrow.reLocks(uint256[],address[],address[],uint256[],uint256[])

Check notice

Code scanning / Slither

Calls inside a loop Low

EnterpriseEscrow._reLock(uint256,address,address,uint256,uint256) has external calls inside a loop: enterpriseFee = IEnterpriseFeeCollector(opcCollector).calculateFee(token,amount)
Calls stack containing the loop:
EnterpriseEscrow.bundleJobs(EnterpriseEscrow.ClaimData[],EnterpriseEscrow.CancelData[],EnterpriseEscrow.LockData[],EnterpriseEscrow.LockData[])
Comment on lines 574 to 635
Comment on lines 574 to 635
Comment on lines 574 to 635
Comment on lines 574 to 635
Comment on lines 574 to 635

/**
* @dev claimLock
* Called by payee to claim a lock (fully or partial)
Expand All @@ -445,7 +635,7 @@
* @param amount amount in wei to claim
* @param proof job proof
*/
function claimLock(uint256 jobId,address token,address payer,uint256 amount,bytes memory proof)
function claimLock(uint256 jobId,address token,address payer,uint256 amount,bytes calldata proof)
external nonReentrant{
_claimLock(jobId,token,payer,amount,proof);
}
Expand All @@ -461,9 +651,9 @@
* @param amount array amounts in wei to claim
* @param proof array of job proofs
*/
function claimLocks(uint256[] memory jobId,address[] memory token,
address[] memory payer,uint256[] memory amount,
bytes[] memory proof) external nonReentrant{
function claimLocks(uint256[] calldata jobId,address[] calldata token,
address[] calldata payer,uint256[] calldata amount,
bytes[] calldata proof) external nonReentrant{

require(jobId.length==token.length &&
jobId.length==payer.length &&
Expand All @@ -484,8 +674,10 @@
* @param amount amount in wei to claim
* @param proof job proof
*/
// Reentrancy-safe: nonReentrant blocks re-entry; only view getters are reachable during the transfers.
// slither-disable-next-line reentrancy-eth
function claimLockAndWithdraw(uint256 jobId,address token,address payer,
uint256 amount,bytes memory proof) external nonReentrant{
uint256 amount,bytes calldata proof) external nonReentrant{
_claimLock(jobId,token,payer,amount,proof);
_withdraw(token,funds[msg.sender][token].available);

Expand All @@ -501,8 +693,10 @@
* @param amount array amounts in wei to claim
* @param proof array of job proofs
*/
function claimLocksAndWithdraw(uint256[] memory jobId,address[] memory token,
address[] memory payer,uint256[] memory amount,bytes[] memory proof) external nonReentrant{
// Reentrancy-safe: nonReentrant blocks re-entry; only view getters are reachable during the transfers.
// slither-disable-next-line reentrancy-eth
function claimLocksAndWithdraw(uint256[] calldata jobId,address[] calldata token,
address[] calldata payer,uint256[] calldata amount,bytes[] calldata proof) external nonReentrant{

require(jobId.length==token.length &&
jobId.length==payer.length &&
Expand All @@ -519,12 +713,15 @@
}
}

// Reentrancy-safe: reached only via nonReentrant entrypoints, so re-entry is impossible; the
// funds/userTokens writes after the fee transfer cannot be exploited.
// slither-disable-next-line reentrancy-no-eth,reentrancy-benign
function _claimLock(uint256 jobId,address token,address payer,uint256 amount,
bytes memory proof) internal {
bytes calldata proof) internal {
require(payer!=address(0),'Invalid payer');
require(token!=address(0),'Invalid token');
require(jobId>0,'Invalid jobId');
lock memory tempLock=lock(0,address(0),0,0,address(0));
lock memory tempLock=lock(0,address(0),0,0,address(0),0);
uint256 index;
uint256 length=locks[msg.sender].length;
for(index=0;index<length;index++){
Expand Down Expand Up @@ -603,8 +800,8 @@
* @param payer payer address (zero address means any)
* @param payee payee address (required)
*/
function cancelExpiredLocks(uint256[] memory jobId,address[] memory token,address[] memory payer,
address[] memory payee) external nonReentrant{
function cancelExpiredLocks(uint256[] calldata jobId,address[] calldata token,address[] calldata payer,
address[] calldata payee) external nonReentrant{
require(jobId.length==token.length &&
jobId.length==payer.length &&
jobId.length==payee.length,"Invalid input");
Expand Down
Loading
Loading