Skip to content

Commit ee21c4d

Browse files
portuu3flopez7
andauthored
Fix escrow payouts (#3261)
* fix payouts when fee calculation is truncated * fix tests and use faker in the root of the repository * document escrow contract and improve tests * Delete unnecessary check for BULK_MAX_VALUE * remove unused modifier * Undo faker package changes for updating yarn --------- Co-authored-by: Francisco López <[email protected]>
1 parent 76e79c3 commit ee21c4d

File tree

6 files changed

+440
-357
lines changed

6 files changed

+440
-357
lines changed

packages/apps/job-launcher/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
"zxcvbn": "^4.4.2"
7171
},
7272
"devDependencies": {
73-
"@faker-js/faker": "^9.5.0",
73+
"@faker-js/faker": "^9.8.0",
7474
"@golevelup/ts-jest": "^0.6.1",
7575
"@nestjs/cli": "^10.3.2",
7676
"@nestjs/schematics": "^11.0.2",

packages/apps/reputation-oracle/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"zxcvbn": "^4.4.2"
7373
},
7474
"devDependencies": {
75-
"@faker-js/faker": "^9.4.0",
75+
"@faker-js/faker": "^9.8.0",
7676
"@golevelup/ts-jest": "^0.6.1",
7777
"@nestjs/cli": "^10.3.2",
7878
"@nestjs/schematics": "^11.0.2",

packages/core/contracts/Escrow.sol

Lines changed: 133 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
88

99
import './interfaces/IEscrow.sol';
1010

11+
/**
12+
* @title Escrow Contract
13+
* @dev This contract manages the lifecycle of an escrow, including funding,
14+
* setup, payouts, and completion. It supports trusted handlers and oracles
15+
* for managing the escrow process.
16+
*/
1117
contract Escrow is IEscrow, ReentrancyGuard {
1218
bytes4 private constant FUNC_SELECTOR_BALANCE_OF =
1319
bytes4(keccak256('balanceOf(address)'));
1420

1521
string constant ERROR_ZERO_ADDRESS = 'Escrow: zero address';
1622

17-
uint256 private constant BULK_MAX_VALUE = 1e9 * (10 ** 18);
1823
uint32 private constant BULK_MAX_COUNT = 100;
1924

2025
event TrustedHandlerAdded(address _handler);
@@ -73,9 +78,16 @@ contract Escrow is IEscrow, ReentrancyGuard {
7378
mapping(address => bool) public areTrustedHandlers;
7479

7580
uint256 public remainingFunds;
76-
7781
uint256 public reservedFunds;
7882

83+
/**
84+
* @dev Constructor to initialize the escrow contract.
85+
* @param _token Address of the token used in the escrow.
86+
* @param _launcher Address of the launcher (creator) of the escrow.
87+
* @param _canceler Address of the canceler who can cancel the escrow.
88+
* @param _duration Duration of the escrow in seconds.
89+
* @param _handlers Array of trusted handler addresses.
90+
*/
7991
constructor(
8092
address _token,
8193
address _launcher,
@@ -97,23 +109,38 @@ contract Escrow is IEscrow, ReentrancyGuard {
97109
_addTrustedHandlers(_handlers);
98110
}
99111

112+
/**
113+
* @dev Returns the balance of the escrow contract for the main token.
114+
*/
100115
function getBalance() public view returns (uint256) {
101116
return getTokenBalance(token);
102117
}
103118

119+
/**
120+
* @dev Returns the balance of the escrow contract for a specific token.
121+
* @param _token Address of the token to check the balance for.
122+
*/
104123
function getTokenBalance(address _token) public view returns (uint256) {
105124
(bool success, bytes memory returnData) = _token.staticcall(
106125
abi.encodeWithSelector(FUNC_SELECTOR_BALANCE_OF, address(this))
107126
);
108127
return success ? abi.decode(returnData, (uint256)) : 0;
109128
}
110129

130+
/**
131+
* @dev Adds trusted handlers to the contract.
132+
* @param _handlers Array of addresses to be added as trusted handlers.
133+
*/
111134
function addTrustedHandlers(
112135
address[] memory _handlers
113136
) public override trusted {
114137
_addTrustedHandlers(_handlers);
115138
}
116139

140+
/**
141+
* @dev Internal function to add trusted handlers.
142+
* @param _handlers Array of addresses to be added as trusted handlers.
143+
*/
117144
function _addTrustedHandlers(address[] memory _handlers) internal {
118145
for (uint256 i = 0; i < _handlers.length; i++) {
119146
require(_handlers[i] != address(0), ERROR_ZERO_ADDRESS);
@@ -122,9 +149,17 @@ contract Escrow is IEscrow, ReentrancyGuard {
122149
}
123150
}
124151

125-
// The escrower puts the Token in the contract without an agentless
126-
// and assigsn a reputation oracle to payout the bounty of size of the
127-
// amount specified
152+
/**
153+
* @dev Sets up the escrow with oracles and manifest details.
154+
* @param _reputationOracle Address of the reputation oracle.
155+
* @param _recordingOracle Address of the recording oracle.
156+
* @param _exchangeOracle Address of the exchange oracle.
157+
* @param _reputationOracleFeePercentage Fee percentage for the reputation oracle.
158+
* @param _recordingOracleFeePercentage Fee percentage for the recording oracle.
159+
* @param _exchangeOracleFeePercentage Fee percentage for the exchange oracle.
160+
* @param _url URL of the manifest.
161+
* @param _hash Hash of the manifest.
162+
*/
128163
function setup(
129164
address _reputationOracle,
130165
address _recordingOracle,
@@ -182,6 +217,10 @@ contract Escrow is IEscrow, ReentrancyGuard {
182217
emit Fund(remainingFunds);
183218
}
184219

220+
/**
221+
* @dev Cancels the escrow and transfers remaining funds to the canceler.
222+
* @return bool indicating success of the cancellation.
223+
*/
185224
function cancel()
186225
public
187226
override
@@ -195,6 +234,11 @@ contract Escrow is IEscrow, ReentrancyGuard {
195234
return true;
196235
}
197236

237+
/**
238+
* @dev Withdraws excess funds from the escrow for a specific token.
239+
* @param _token Address of the token to withdraw.
240+
* @return bool indicating success of the withdrawal.
241+
*/
198242
function withdraw(
199243
address _token
200244
) public override trusted nonReentrant returns (bool) {
@@ -213,15 +257,18 @@ contract Escrow is IEscrow, ReentrancyGuard {
213257
return true;
214258
}
215259

260+
/**
261+
* @dev Completes the escrow, transferring remaining funds to the launcher.
262+
*/
216263
function complete() external override notExpired trustedOrReputationOracle {
217264
require(
218265
status == EscrowStatuses.Paid || status == EscrowStatuses.Partial,
219266
'Escrow not in Paid or Partial state'
220267
);
221-
_complete();
268+
_finalize();
222269
}
223270

224-
function _complete() private {
271+
function _finalize() private {
225272
if (remainingFunds > 0) {
226273
_safeTransfer(token, launcher, remainingFunds);
227274
remainingFunds = 0;
@@ -236,6 +283,11 @@ contract Escrow is IEscrow, ReentrancyGuard {
236283
}
237284
}
238285

286+
/**
287+
* @dev Stores intermediate results during the escrow process.
288+
* @param _url URL of the intermediate results.
289+
* @param _hash Hash of the intermediate results.
290+
*/
239291
function storeResults(
240292
string memory _url,
241293
string memory _hash,
@@ -270,22 +322,13 @@ contract Escrow is IEscrow, ReentrancyGuard {
270322
}
271323

272324
/**
273-
* @dev Performs bulk payout to multiple workers
274-
* Escrow needs to be completed / cancelled, so that it can be paid out.
275-
* Every recipient is paid with the amount after reputation and recording oracle fees taken out.
276-
* If the amount is less than the fee, the recipient is not paid.
277-
* If the fee is zero, reputation, and recording oracle are not paid.
278-
* Payout will fail if any of the transaction fails.
279-
* If the escrow is fully paid out, meaning that the balance of the escrow is 0, it'll set as Paid.
280-
* If the escrow is partially paid out, meaning that the escrow still has remaining balance, it'll set as Partial.
281-
* This contract is only callable if the contract is not broke, not launched, not paid, not expired, by trusted parties.
282-
*
283-
* @param _recipients Array of recipients
325+
* @dev Performs bulk payout to multiple recipients with oracle fees deducted.
326+
* @param _recipients Array of recipient addresses.
284327
* @param _amounts Array of amounts to be paid to each recipient.
285-
* @param _url URL storing results as transaction details
286-
* @param _hash Hash of the results
287-
* @param _txId Transaction ID
288-
* @param forceComplete Boolean parameter indicating if remaining balance should be transferred to the escrow creator
328+
* @param _url URL storing results as transaction details.
329+
* @param _hash Hash of the results.
330+
* @param _txId Transaction ID.
331+
* @param forceComplete Boolean indicating if remaining balance should be transferred to the launcher.
289332
*/
290333
function bulkPayOut(
291334
address[] memory _recipients,
@@ -314,60 +357,80 @@ contract Escrow is IEscrow, ReentrancyGuard {
314357
status != EscrowStatuses.Cancelled,
315358
'Invalid status'
316359
);
317-
318-
uint256 aggregatedBulkAmount = 0;
319-
for (uint256 i = 0; i < _amounts.length; i++) {
320-
uint256 amount = _amounts[i];
321-
require(amount > 0, 'Amount should be greater than zero');
322-
aggregatedBulkAmount += amount;
323-
}
324-
require(aggregatedBulkAmount < BULK_MAX_VALUE, 'Bulk value too high');
325360
require(
326-
aggregatedBulkAmount <= reservedFunds,
327-
'Not enough reserved funds'
361+
bytes(_url).length != 0 && bytes(_hash).length != 0,
362+
'URL or hash is empty'
328363
);
329364

330-
reservedFunds -= aggregatedBulkAmount;
331-
remainingFunds -= aggregatedBulkAmount;
332-
333-
require(bytes(_url).length != 0, "URL can't be empty");
334-
require(bytes(_hash).length != 0, "Hash can't be empty");
365+
uint256 totalBulkAmount = 0;
366+
uint256 totalReputationOracleFee = 0;
367+
uint256 totalRecordingOracleFee = 0;
368+
uint256 totalExchangeOracleFee = 0;
335369

336-
finalResultsUrl = _url;
337-
finalResultsHash = _hash;
370+
for (uint256 i = 0; i < _recipients.length; i++) {
371+
uint256 amount = _amounts[i];
372+
require(amount > 0, 'Amount should be greater than zero');
373+
totalBulkAmount += amount;
374+
totalReputationOracleFee +=
375+
(reputationOracleFeePercentage * amount) /
376+
100;
377+
totalRecordingOracleFee +=
378+
(recordingOracleFeePercentage * amount) /
379+
100;
380+
totalExchangeOracleFee +=
381+
(exchangeOracleFeePercentage * amount) /
382+
100;
383+
}
384+
require(totalBulkAmount <= reservedFunds, 'Not enough reserved funds');
338385

339-
uint256 totalFeePercentage = reputationOracleFeePercentage +
340-
recordingOracleFeePercentage +
341-
exchangeOracleFeePercentage;
386+
uint256 paidReputation = 0;
387+
uint256 paidRecording = 0;
388+
uint256 paidExchange = 0;
342389

343390
for (uint256 i = 0; i < _recipients.length; i++) {
344391
uint256 amount = _amounts[i];
345-
uint256 amountFee = (totalFeePercentage * amount) / 100;
346-
_safeTransfer(token, _recipients[i], amount - amountFee);
347-
}
392+
uint256 reputationOracleFee = (reputationOracleFeePercentage *
393+
amount) / 100;
394+
uint256 recordingOracleFee = (recordingOracleFeePercentage *
395+
amount) / 100;
396+
uint256 exchangeOracleFee = (exchangeOracleFeePercentage * amount) /
397+
100;
398+
399+
if (i == _recipients.length - 1) {
400+
reputationOracleFee = totalReputationOracleFee - paidReputation;
401+
recordingOracleFee = totalRecordingOracleFee - paidRecording;
402+
exchangeOracleFee = totalExchangeOracleFee - paidExchange;
403+
}
404+
405+
paidReputation += reputationOracleFee;
406+
paidRecording += recordingOracleFee;
407+
paidExchange += exchangeOracleFee;
348408

349-
// Transfer oracle fees
350-
if (reputationOracleFeePercentage > 0) {
351409
_safeTransfer(
352410
token,
353-
reputationOracle,
354-
(reputationOracleFeePercentage * aggregatedBulkAmount) / 100
411+
_recipients[i],
412+
amount -
413+
reputationOracleFee -
414+
recordingOracleFee -
415+
exchangeOracleFee
355416
);
356417
}
418+
419+
// Transfer oracle fees
420+
if (reputationOracleFeePercentage > 0) {
421+
_safeTransfer(token, reputationOracle, totalReputationOracleFee);
422+
}
357423
if (recordingOracleFeePercentage > 0) {
358-
_safeTransfer(
359-
token,
360-
recordingOracle,
361-
(recordingOracleFeePercentage * aggregatedBulkAmount) / 100
362-
);
424+
_safeTransfer(token, recordingOracle, totalRecordingOracleFee);
363425
}
364426
if (exchangeOracleFeePercentage > 0) {
365-
_safeTransfer(
366-
token,
367-
exchangeOracle,
368-
(exchangeOracleFeePercentage * aggregatedBulkAmount) / 100
369-
);
427+
_safeTransfer(token, exchangeOracle, totalExchangeOracleFee);
370428
}
429+
remainingFunds -= totalBulkAmount;
430+
reservedFunds -= totalBulkAmount;
431+
432+
finalResultsUrl = _url;
433+
finalResultsHash = _hash;
371434

372435
if (remainingFunds == 0 || forceComplete) {
373436
emit BulkTransferV2(
@@ -377,7 +440,7 @@ contract Escrow is IEscrow, ReentrancyGuard {
377440
false,
378441
finalResultsUrl
379442
);
380-
_complete();
443+
_finalize();
381444
} else {
382445
if (status != EscrowStatuses.ToCancel) {
383446
status = EscrowStatuses.Partial;
@@ -394,13 +457,11 @@ contract Escrow is IEscrow, ReentrancyGuard {
394457

395458
/**
396459
* @dev Overloaded function to perform bulk payout with default forceComplete set to false.
397-
* Calls the main bulkPayout function with forceComplete as false.
398-
*
399-
* @param _recipients Array of recipients
460+
* @param _recipients Array of recipient addresses.
400461
* @param _amounts Array of amounts to be paid to each recipient.
401-
* @param _url URL storing results as transaction details
402-
* @param _hash Hash of the results
403-
* @param _txId Transaction ID
462+
* @param _url URL storing results as transaction details.
463+
* @param _hash Hash of the results.
464+
* @param _txId Transaction ID.
404465
*/
405466
function bulkPayOut(
406467
address[] memory _recipients,
@@ -412,6 +473,12 @@ contract Escrow is IEscrow, ReentrancyGuard {
412473
bulkPayOut(_recipients, _amounts, _url, _hash, _txId, false);
413474
}
414475

476+
/**
477+
* @dev Internal function to safely transfer tokens.
478+
* @param _token Address of the token to transfer.
479+
* @param to Address of the recipient.
480+
* @param value Amount to transfer.
481+
*/
415482
function _safeTransfer(address _token, address to, uint256 value) internal {
416483
SafeERC20.safeTransfer(IERC20(_token), to, value);
417484
}

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
],
5252
"license": "MIT",
5353
"devDependencies": {
54+
"@faker-js/faker": "^9.8.0",
5455
"@nomicfoundation/hardhat-chai-matchers": "^2.0.7",
5556
"@nomicfoundation/hardhat-ethers": "^3.0.5",
5657
"@nomicfoundation/hardhat-network-helpers": "^1.0.12",

0 commit comments

Comments
 (0)