Skip to content

Commit 0661934

Browse files
authored
Fix cancelation refunds (#3690)
1 parent b7ae4e0 commit 0661934

11 files changed

Lines changed: 1622 additions & 54 deletions

File tree

.changeset/tasty-shrimps-grow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@human-protocol/core": patch
3+
---
4+
5+
Add CancelationRefund event to any cancelation

packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,16 +1741,78 @@ describe('JobService', () => {
17411741
expect(mockJobRepository.updateOne).toHaveBeenCalledWith(jobEntity);
17421742
});
17431743

1744-
it('should throw ConflictError if no refund is found', async () => {
1744+
it('should not create a refund and set status to CANCELED when no refund is found', async () => {
17451745
const jobEntity = createJobEntity();
17461746
mockPaymentService.getJobPayments.mockResolvedValueOnce([]);
17471747
mockedEscrowUtils.getCancellationRefund.mockResolvedValueOnce(
17481748
null as any,
17491749
);
17501750

1751-
await expect(jobService.cancelJob(jobEntity)).rejects.toThrow(
1752-
new ConflictError(ErrorJob.NoRefundFound),
1751+
jest
1752+
.spyOn(jobService as any, 'getRefundAmount')
1753+
.mockResolvedValueOnce(0n);
1754+
1755+
mockJobRepository.updateOne.mockResolvedValueOnce(jobEntity);
1756+
1757+
await jobService.cancelJob(jobEntity);
1758+
1759+
expect(mockPaymentService.getJobPayments).toHaveBeenCalledWith(
1760+
jobEntity.id,
1761+
PaymentType.SLASH,
17531762
);
1763+
expect(EscrowUtils.getCancellationRefund).toHaveBeenCalledWith(
1764+
jobEntity.chainId,
1765+
jobEntity.escrowAddress,
1766+
);
1767+
expect(mockPaymentService.createRefundPayment).not.toHaveBeenCalled();
1768+
expect(jobEntity.status).toBe(JobStatus.CANCELED);
1769+
expect(mockJobRepository.updateOne).toHaveBeenCalledWith(jobEntity);
1770+
});
1771+
1772+
it('should create a refund when on-chain refund amount is greater than 0', async () => {
1773+
const jobEntity = createJobEntity();
1774+
const tokenDecimals = (TOKEN_ADDRESSES[jobEntity.chainId as ChainId] ??
1775+
{})[jobEntity.token as EscrowFundToken]?.decimals;
1776+
1777+
mockPaymentService.getJobPayments.mockResolvedValueOnce([]);
1778+
mockedEscrowUtils.getCancellationRefund.mockResolvedValueOnce(
1779+
null as any,
1780+
);
1781+
1782+
const refundAmount = faker.number.float({
1783+
min: 1,
1784+
max: 10,
1785+
fractionDigits: tokenDecimals,
1786+
});
1787+
1788+
// Mock on-chain refund lookup to return a positive amount
1789+
jest
1790+
.spyOn(jobService as any, 'getRefundAmount')
1791+
.mockResolvedValueOnce(
1792+
ethers.parseUnits(refundAmount.toString(), tokenDecimals),
1793+
);
1794+
1795+
mockPaymentService.createRefundPayment.mockResolvedValueOnce(undefined);
1796+
mockJobRepository.updateOne.mockResolvedValueOnce(jobEntity);
1797+
1798+
await jobService.cancelJob(jobEntity);
1799+
1800+
expect(mockPaymentService.getJobPayments).toHaveBeenCalledWith(
1801+
jobEntity.id,
1802+
PaymentType.SLASH,
1803+
);
1804+
expect(EscrowUtils.getCancellationRefund).toHaveBeenCalledWith(
1805+
jobEntity.chainId,
1806+
jobEntity.escrowAddress,
1807+
);
1808+
expect(mockPaymentService.createRefundPayment).toHaveBeenCalledWith({
1809+
refundAmount: refundAmount,
1810+
refundCurrency: jobEntity.token,
1811+
userId: jobEntity.userId,
1812+
jobId: jobEntity.id,
1813+
});
1814+
expect(jobEntity.status).toBe(JobStatus.CANCELED);
1815+
expect(mockJobRepository.updateOne).toHaveBeenCalledWith(jobEntity);
17541816
});
17551817

17561818
it('should throw ConflictError if refund.amount is empty', async () => {

packages/apps/job-launcher/server/src/modules/job/job.service.ts

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
EscrowClient,
44
EscrowStatus,
55
EscrowUtils,
6+
ICancellationRefund,
67
KVStoreKeys,
78
KVStoreUtils,
89
NETWORKS,
@@ -79,6 +80,7 @@ import {
7980
} from './job.dto';
8081
import { JobEntity } from './job.entity';
8182
import { JobRepository } from './job.repository';
83+
import { Escrow, Escrow__factory } from '@human-protocol/core/typechain-types';
8284

8385
@Injectable()
8486
export class JobService {
@@ -876,24 +878,95 @@ export class JobService {
876878
PaymentType.SLASH,
877879
);
878880
if (!slash?.length) {
879-
const refund = await EscrowUtils.getCancellationRefund(
880-
jobEntity.chainId,
881-
jobEntity.escrowAddress!,
882-
);
881+
let refund: ICancellationRefund | null = null;
882+
try {
883+
refund = await EscrowUtils.getCancellationRefund(
884+
jobEntity.chainId,
885+
jobEntity.escrowAddress!,
886+
);
887+
} catch {
888+
// Ignore error
889+
}
883890

884-
if (!refund || !refund.amount) {
885-
throw new ConflictError(ErrorJob.NoRefundFound);
891+
let amount = 0n;
892+
893+
if (!refund) {
894+
//Temp fix
895+
amount = await this.getRefundAmount(
896+
jobEntity.chainId,
897+
jobEntity.escrowAddress!,
898+
token.address,
899+
);
900+
} else {
901+
if (!refund.amount) {
902+
throw new ConflictError(ErrorJob.NoRefundFound);
903+
}
904+
amount = refund.amount;
886905
}
887906

888-
await this.paymentService.createRefundPayment({
889-
refundAmount: Number(ethers.formatUnits(refund.amount, token.decimals)),
890-
refundCurrency: jobEntity.token,
891-
userId: jobEntity.userId,
892-
jobId: jobEntity.id,
893-
});
907+
if (amount > 0n) {
908+
await this.paymentService.createRefundPayment({
909+
refundAmount: Number(ethers.formatUnits(amount, token.decimals)),
910+
refundCurrency: jobEntity.token,
911+
userId: jobEntity.userId,
912+
jobId: jobEntity.id,
913+
});
914+
}
894915
}
895916

896917
jobEntity.status = JobStatus.CANCELED;
897918
await this.jobRepository.updateOne(jobEntity);
898919
}
920+
921+
public async getRefundAmount(
922+
chainId: ChainId,
923+
escrowAddress: string,
924+
tokenAddress: string,
925+
): Promise<bigint> {
926+
const signer = this.web3Service.getSigner(chainId);
927+
const provider = signer.provider!;
928+
const contract: Escrow = Escrow__factory.connect(escrowAddress!, provider);
929+
const fromBlock = 79278120; //This issue started at this block
930+
const toBlock = 'latest';
931+
const cancelledFilter = contract.filters?.Cancelled?.();
932+
const cancelledLogs = await contract.queryFilter(
933+
cancelledFilter,
934+
fromBlock,
935+
toBlock,
936+
);
937+
938+
for (const log of cancelledLogs) {
939+
const erc20Interface = new ethers.Interface([
940+
'event Transfer(address indexed from, address indexed to, uint256 value)',
941+
]);
942+
943+
const transferTopic = erc20Interface.getEvent('Transfer')!.topicHash;
944+
const receipt = await provider.getTransactionReceipt(log.transactionHash);
945+
946+
const transferLogs = receipt!.logs.filter(
947+
(l) =>
948+
l.address.toLowerCase() === tokenAddress.toLowerCase() &&
949+
l.topics[0] === transferTopic,
950+
);
951+
for (const tlog of transferLogs) {
952+
const decoded = erc20Interface.decodeEventLog(
953+
'Transfer',
954+
tlog.data,
955+
tlog.topics,
956+
);
957+
958+
const from = decoded.from as string;
959+
const to = decoded.to as string;
960+
const value = decoded.value as bigint;
961+
if (
962+
from.toLowerCase() === escrowAddress.toLowerCase() &&
963+
to.toLowerCase() === signer.address.toLowerCase()
964+
) {
965+
return value;
966+
}
967+
}
968+
}
969+
970+
return 0n;
971+
}
899972
}

packages/core/.openzeppelin/bsc-testnet.json

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2956,6 +2956,163 @@
29562956
},
29572957
"namespaces": {}
29582958
}
2959+
},
2960+
"addfe6d6ad3f78f6ccf914624fcf7eba6c970f5ba3a20a52f0184991b3d172e1": {
2961+
"address": "0xa68c1566f23C2335c142D798f8094F460d487044",
2962+
"txHash": "0x69327fb1132a9f7f15e2550822477c921c531375a50dc429eef4ecfefefe841a",
2963+
"layout": {
2964+
"solcVersion": "0.8.23",
2965+
"storage": [
2966+
{
2967+
"label": "_initialized",
2968+
"offset": 0,
2969+
"slot": "0",
2970+
"type": "t_uint8",
2971+
"contract": "Initializable",
2972+
"src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63",
2973+
"retypedFrom": "bool"
2974+
},
2975+
{
2976+
"label": "_initializing",
2977+
"offset": 1,
2978+
"slot": "0",
2979+
"type": "t_bool",
2980+
"contract": "Initializable",
2981+
"src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68"
2982+
},
2983+
{
2984+
"label": "__gap",
2985+
"offset": 0,
2986+
"slot": "1",
2987+
"type": "t_array(t_uint256)50_storage",
2988+
"contract": "ContextUpgradeable",
2989+
"src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40"
2990+
},
2991+
{
2992+
"label": "_owner",
2993+
"offset": 0,
2994+
"slot": "51",
2995+
"type": "t_address",
2996+
"contract": "OwnableUpgradeable",
2997+
"src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22"
2998+
},
2999+
{
3000+
"label": "__gap",
3001+
"offset": 0,
3002+
"slot": "52",
3003+
"type": "t_array(t_uint256)49_storage",
3004+
"contract": "OwnableUpgradeable",
3005+
"src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94"
3006+
},
3007+
{
3008+
"label": "__gap",
3009+
"offset": 0,
3010+
"slot": "101",
3011+
"type": "t_array(t_uint256)50_storage",
3012+
"contract": "ERC1967UpgradeUpgradeable",
3013+
"src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169"
3014+
},
3015+
{
3016+
"label": "__gap",
3017+
"offset": 0,
3018+
"slot": "151",
3019+
"type": "t_array(t_uint256)50_storage",
3020+
"contract": "UUPSUpgradeable",
3021+
"src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111"
3022+
},
3023+
{
3024+
"label": "counter",
3025+
"offset": 0,
3026+
"slot": "201",
3027+
"type": "t_uint256",
3028+
"contract": "EscrowFactory",
3029+
"src": "contracts/EscrowFactory.sol:17"
3030+
},
3031+
{
3032+
"label": "escrowCounters",
3033+
"offset": 0,
3034+
"slot": "202",
3035+
"type": "t_mapping(t_address,t_uint256)",
3036+
"contract": "EscrowFactory",
3037+
"src": "contracts/EscrowFactory.sol:18"
3038+
},
3039+
{
3040+
"label": "lastEscrow",
3041+
"offset": 0,
3042+
"slot": "203",
3043+
"type": "t_address",
3044+
"contract": "EscrowFactory",
3045+
"src": "contracts/EscrowFactory.sol:19"
3046+
},
3047+
{
3048+
"label": "staking",
3049+
"offset": 0,
3050+
"slot": "204",
3051+
"type": "t_address",
3052+
"contract": "EscrowFactory",
3053+
"src": "contracts/EscrowFactory.sol:20"
3054+
},
3055+
{
3056+
"label": "minimumStake",
3057+
"offset": 0,
3058+
"slot": "205",
3059+
"type": "t_uint256",
3060+
"contract": "EscrowFactory",
3061+
"src": "contracts/EscrowFactory.sol:21"
3062+
},
3063+
{
3064+
"label": "admin",
3065+
"offset": 0,
3066+
"slot": "206",
3067+
"type": "t_address",
3068+
"contract": "EscrowFactory",
3069+
"src": "contracts/EscrowFactory.sol:22"
3070+
},
3071+
{
3072+
"label": "__gap",
3073+
"offset": 0,
3074+
"slot": "207",
3075+
"type": "t_array(t_uint256)44_storage",
3076+
"contract": "EscrowFactory",
3077+
"src": "contracts/EscrowFactory.sol:189"
3078+
}
3079+
],
3080+
"types": {
3081+
"t_address": {
3082+
"label": "address",
3083+
"numberOfBytes": "20"
3084+
},
3085+
"t_array(t_uint256)44_storage": {
3086+
"label": "uint256[44]",
3087+
"numberOfBytes": "1408"
3088+
},
3089+
"t_array(t_uint256)49_storage": {
3090+
"label": "uint256[49]",
3091+
"numberOfBytes": "1568"
3092+
},
3093+
"t_array(t_uint256)50_storage": {
3094+
"label": "uint256[50]",
3095+
"numberOfBytes": "1600"
3096+
},
3097+
"t_bool": {
3098+
"label": "bool",
3099+
"numberOfBytes": "1"
3100+
},
3101+
"t_mapping(t_address,t_uint256)": {
3102+
"label": "mapping(address => uint256)",
3103+
"numberOfBytes": "32"
3104+
},
3105+
"t_uint256": {
3106+
"label": "uint256",
3107+
"numberOfBytes": "32"
3108+
},
3109+
"t_uint8": {
3110+
"label": "uint8",
3111+
"numberOfBytes": "1"
3112+
}
3113+
},
3114+
"namespaces": {}
3115+
}
29593116
}
29603117
}
29613118
}

0 commit comments

Comments
 (0)