Skip to content

Commit df5bec0

Browse files
authored
Create, fund and setup escrow in 1 transaction (#3641)
1 parent e901900 commit df5bec0

File tree

7 files changed

+1115
-83
lines changed

7 files changed

+1115
-83
lines changed

packages/core/contracts/Escrow.sol

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ contract Escrow is IEscrow, ReentrancyGuard {
150150
uint8 _exchangeOracleFeePercentage,
151151
string calldata _url,
152152
string calldata _hash
153-
) external override adminOrLauncher notExpired {
153+
) external override adminLauncherOrFactory notExpired {
154154
require(_reputationOracle != address(0), 'Invalid reputation oracle');
155155
require(_recordingOracle != address(0), 'Invalid recording oracle');
156156
require(_exchangeOracle != address(0), 'Invalid exchange oracle');
@@ -495,6 +495,16 @@ contract Escrow is IEscrow, ReentrancyGuard {
495495
_;
496496
}
497497

498+
modifier adminLauncherOrFactory() {
499+
require(
500+
msg.sender == admin ||
501+
msg.sender == launcher ||
502+
msg.sender == escrowFactory,
503+
'Unauthorised'
504+
);
505+
_;
506+
}
507+
498508
modifier adminOrReputationOracle() {
499509
require(
500510
msg.sender == admin || msg.sender == reputationOracle,

packages/core/contracts/EscrowFactory.sol

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ pragma solidity ^0.8.0;
44

55
import '@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol';
66
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
7-
7+
import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
8+
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
89
import './interfaces/IStaking.sol';
910
import './Escrow.sol';
1011

@@ -20,6 +21,8 @@ contract EscrowFactory is OwnableUpgradeable, UUPSUpgradeable {
2021
uint256 public minimumStake;
2122
address public admin;
2223

24+
using SafeERC20 for IERC20;
25+
2326
event Launched(address token, address escrow);
2427
event LaunchedV2(address token, address escrow, string jobRequesterId);
2528
event SetStakingAddress(address indexed stakingAddress);
@@ -41,30 +44,93 @@ contract EscrowFactory is OwnableUpgradeable, UUPSUpgradeable {
4144
_setEscrowAdmin(msg.sender);
4245
}
4346

44-
/**
45-
* @dev Creates a new Escrow contract.
46-
*
47-
* @param token Token address to be associated with the Escrow contract.
48-
* @param jobRequesterId String identifier for the job requester, used for tracking purposes.
49-
*
50-
* @return The address of the newly created Escrow contract.
51-
*/
52-
function createEscrow(
53-
address token,
54-
string memory jobRequesterId
55-
) external returns (address) {
47+
function _launchEscrow(
48+
address _token,
49+
string calldata _jobRequesterId
50+
) private {
5651
uint256 availableStake = IStaking(staking).getAvailableStake(
5752
msg.sender
5853
);
5954
require(availableStake >= minimumStake, 'Insufficient stake');
6055
require(admin != address(0), ERROR_ZERO_ADDRESS);
6156

62-
Escrow escrow = new Escrow(token, msg.sender, admin, STANDARD_DURATION);
57+
Escrow escrow = new Escrow(
58+
_token,
59+
msg.sender,
60+
admin,
61+
STANDARD_DURATION
62+
);
6363
counter++;
6464
escrowCounters[address(escrow)] = counter;
6565
lastEscrow = address(escrow);
6666

67-
emit LaunchedV2(token, lastEscrow, jobRequesterId);
67+
emit LaunchedV2(_token, lastEscrow, _jobRequesterId);
68+
}
69+
70+
/**
71+
* @dev Creates a new Escrow contract.
72+
*
73+
* @param _token Token address to be associated with the Escrow contract.
74+
* @param _jobRequesterId String identifier for the job requester, used for tracking purposes.
75+
*
76+
* @return The address of the newly created Escrow contract.
77+
*/
78+
function createEscrow(
79+
address _token,
80+
string calldata _jobRequesterId
81+
) external returns (address) {
82+
_launchEscrow(_token, _jobRequesterId);
83+
return lastEscrow;
84+
}
85+
86+
/**
87+
* @dev Creates a new Escrow contract and funds it in one transaction.
88+
* Requires the caller to have approved the factory for the token and amount.
89+
* @param _token Token address to be associated with the Escrow contract.
90+
* @param _amount Amount of tokens to fund the Escrow with.
91+
* @param _jobRequesterId String identifier for the job requester, used for tracking purposes.
92+
* @param _reputationOracle Address of the reputation oracle.
93+
* @param _recordingOracle Address of the recording oracle.
94+
* @param _exchangeOracle Address of the exchange oracle.
95+
* @param _reputationOracleFeePercentage Fee percentage for the reputation oracle.
96+
* @param _recordingOracleFeePercentage Fee percentage for the recording oracle.
97+
* @param _exchangeOracleFeePercentage Fee percentage for the exchange oracle.
98+
* @param _url URL for the escrow manifest.
99+
* @param _hash Hash of the escrow manifest.
100+
* @return The address of the newly created Escrow contract.
101+
*/
102+
function createFundAndSetupEscrow(
103+
address _token,
104+
uint256 _amount,
105+
string calldata _jobRequesterId,
106+
address _reputationOracle,
107+
address _recordingOracle,
108+
address _exchangeOracle,
109+
uint8 _reputationOracleFeePercentage,
110+
uint8 _recordingOracleFeePercentage,
111+
uint8 _exchangeOracleFeePercentage,
112+
string calldata _url,
113+
string calldata _hash
114+
) external returns (address) {
115+
require(_amount > 0, 'Amount is 0');
116+
117+
_launchEscrow(_token, _jobRequesterId);
118+
IERC20(_token).safeTransferFrom(
119+
msg.sender,
120+
address(lastEscrow),
121+
_amount
122+
);
123+
Escrow(lastEscrow).setup(
124+
_reputationOracle,
125+
_recordingOracle,
126+
_exchangeOracle,
127+
_reputationOracleFeePercentage,
128+
_recordingOracleFeePercentage,
129+
_exchangeOracleFeePercentage,
130+
_url,
131+
_hash
132+
);
133+
68134
return lastEscrow;
69135
}
70136

packages/core/test/EscrowFactory.ts

Lines changed: 165 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ import { ethers, upgrades } from 'hardhat';
55
import { EscrowFactory, HMToken, Staking } from '../typechain-types';
66
import { faker } from '@faker-js/faker';
77

8-
let owner: Signer, launcher: Signer, admin: Signer;
8+
let owner: Signer,
9+
launcher1: Signer,
10+
admin: Signer,
11+
launcher2: Signer,
12+
exchangeOracle: Signer,
13+
recordingOracle: Signer,
14+
reputationOracle: Signer;
15+
let exchangeOracleAddress: string,
16+
recordingOracleAddress: string,
17+
reputationOracleAddress: string;
918

1019
let token: HMToken, escrowFactory: EscrowFactory, staking: Staking;
1120
let stakingAddress: string, tokenAddress: string;
@@ -24,7 +33,18 @@ async function stake(staker: Signer, amount: bigint = FIXTURE_STAKE_AMOUNT) {
2433

2534
describe('EscrowFactory', function () {
2635
before(async () => {
27-
[owner, launcher, admin] = await ethers.getSigners();
36+
[
37+
owner,
38+
launcher1,
39+
admin,
40+
launcher2,
41+
exchangeOracle,
42+
recordingOracle,
43+
reputationOracle,
44+
] = await ethers.getSigners();
45+
exchangeOracleAddress = await exchangeOracle.getAddress();
46+
recordingOracleAddress = await recordingOracle.getAddress();
47+
reputationOracleAddress = await reputationOracle.getAddress();
2848

2949
const HMToken = await ethers.getContractFactory(
3050
'contracts/HMToken.sol:HMToken'
@@ -39,7 +59,10 @@ describe('EscrowFactory', function () {
3959

4060
await token
4161
.connect(owner)
42-
.transfer(await launcher.getAddress(), ethers.parseEther('100000'));
62+
.transfer(await launcher1.getAddress(), ethers.parseEther('100000'));
63+
await token
64+
.connect(owner)
65+
.transfer(await launcher2.getAddress(), ethers.parseEther('100000'));
4366

4467
const Staking = await ethers.getContractFactory('Staking');
4568
staking = await Staking.deploy(
@@ -50,8 +73,6 @@ describe('EscrowFactory', function () {
5073
);
5174
stakingAddress = await staking.getAddress();
5275

53-
await token.connect(launcher).approve(await staking.getAddress(), 1000);
54-
5576
const EscrowFactory = await ethers.getContractFactory(
5677
'contracts/EscrowFactory.sol:EscrowFactory'
5778
);
@@ -109,7 +130,7 @@ describe('EscrowFactory', function () {
109130

110131
it('reverts when caller is not the owner', async () => {
111132
await expect(
112-
escrowFactory.connect(launcher).setStakingAddress(stakingAddress)
133+
escrowFactory.connect(launcher1).setStakingAddress(stakingAddress)
113134
).to.be.revertedWith('Ownable: caller is not the owner');
114135
});
115136
});
@@ -147,7 +168,7 @@ describe('EscrowFactory', function () {
147168

148169
it('reverts when caller is not the owner', async () => {
149170
await expect(
150-
escrowFactory.connect(launcher).setMinimumStake(0)
171+
escrowFactory.connect(launcher1).setMinimumStake(0)
151172
).to.be.revertedWith('Ownable: caller is not the owner');
152173
});
153174
});
@@ -176,7 +197,7 @@ describe('EscrowFactory', function () {
176197

177198
it('reverts when caller is not the owner', async () => {
178199
await expect(
179-
escrowFactory.connect(launcher).setAdmin(await admin.getAddress())
200+
escrowFactory.connect(launcher1).setAdmin(await admin.getAddress())
180201
).to.be.revertedWith('Ownable: caller is not the owner');
181202
});
182203
});
@@ -197,18 +218,18 @@ describe('EscrowFactory', function () {
197218
it('reverts when launcher has insufficient stake', async () => {
198219
await expect(
199220
escrowFactory
200-
.connect(launcher)
221+
.connect(launcher1)
201222
.createEscrow(tokenAddress, FIXTURE_REQUESTER_ID)
202223
).to.be.revertedWith('Insufficient stake');
203224
});
204225
});
205226

206227
describe('succeeds', () => {
207228
it('creates an escrow successfully', async () => {
208-
await stake(launcher);
229+
await stake(launcher1);
209230

210231
const tx = await escrowFactory
211-
.connect(launcher)
232+
.connect(launcher1)
212233
.createEscrow(tokenAddress, FIXTURE_REQUESTER_ID);
213234

214235
await expect(tx)
@@ -229,4 +250,137 @@ describe('EscrowFactory', function () {
229250
});
230251
});
231252
});
253+
254+
describe('createFundAndSetupEscrow()', () => {
255+
const fee = faker.number.int({ min: 1, max: 5 });
256+
const manifestUrl = faker.internet.url();
257+
const manifestHash = faker.string.alphanumeric(46);
258+
const fundAmount = ethers.parseEther(
259+
faker.finance.amount({ min: 1, max: 100 })
260+
);
261+
describe('reverts', () => {
262+
it('reverts when fund amount is 0', async () => {
263+
await expect(
264+
escrowFactory
265+
.connect(launcher2)
266+
.createFundAndSetupEscrow(
267+
tokenAddress,
268+
0,
269+
FIXTURE_REQUESTER_ID,
270+
reputationOracleAddress,
271+
recordingOracleAddress,
272+
exchangeOracleAddress,
273+
fee,
274+
fee,
275+
fee,
276+
manifestUrl,
277+
manifestHash
278+
)
279+
).to.be.revertedWith('Amount is 0');
280+
});
281+
282+
it('reverts when launcher has insufficient stake', async () => {
283+
await expect(
284+
escrowFactory
285+
.connect(launcher2)
286+
.createFundAndSetupEscrow(
287+
tokenAddress,
288+
fundAmount,
289+
FIXTURE_REQUESTER_ID,
290+
reputationOracleAddress,
291+
recordingOracleAddress,
292+
exchangeOracleAddress,
293+
fee,
294+
fee,
295+
fee,
296+
manifestUrl,
297+
manifestHash
298+
)
299+
).to.be.revertedWith('Insufficient stake');
300+
});
301+
302+
it('reverts when allowance is too low', async () => {
303+
await stake(launcher2);
304+
await token
305+
.connect(launcher2)
306+
.approve(await escrowFactory.getAddress(), fundAmount / 2n);
307+
308+
await expect(
309+
escrowFactory
310+
.connect(launcher2)
311+
.createFundAndSetupEscrow(
312+
tokenAddress,
313+
fundAmount,
314+
FIXTURE_REQUESTER_ID,
315+
reputationOracleAddress,
316+
recordingOracleAddress,
317+
exchangeOracleAddress,
318+
fee,
319+
fee,
320+
fee,
321+
manifestUrl,
322+
manifestHash
323+
)
324+
).to.be.revertedWith('Spender allowance too low');
325+
});
326+
});
327+
328+
describe('succeeds', () => {
329+
it('creates an escrow successfully', async () => {
330+
await stake(launcher2);
331+
332+
await token
333+
.connect(launcher2)
334+
.approve(await escrowFactory.getAddress(), fundAmount);
335+
336+
const tx = await escrowFactory
337+
.connect(launcher2)
338+
.createFundAndSetupEscrow(
339+
tokenAddress,
340+
fundAmount,
341+
FIXTURE_REQUESTER_ID,
342+
reputationOracleAddress,
343+
recordingOracleAddress,
344+
exchangeOracleAddress,
345+
fee,
346+
fee,
347+
fee,
348+
manifestUrl,
349+
manifestHash
350+
);
351+
352+
const receipt = await tx.wait();
353+
const event = (
354+
receipt?.logs?.find(({ topics }) =>
355+
topics.includes(ethers.id('LaunchedV2(address,address,string)'))
356+
) as EventLog
357+
)?.args;
358+
359+
expect(event).to.not.be.undefined;
360+
const escrowAddress = event[1];
361+
362+
const escrow = await ethers.getContractAt(
363+
'contracts/Escrow.sol:Escrow',
364+
escrowAddress
365+
);
366+
367+
await expect(tx)
368+
.to.emit(escrowFactory, 'LaunchedV2')
369+
.withArgs(tokenAddress, escrowAddress, FIXTURE_REQUESTER_ID)
370+
.to.emit(escrow, 'PendingV2')
371+
.withArgs(
372+
manifestUrl,
373+
manifestHash,
374+
reputationOracleAddress,
375+
recordingOracleAddress,
376+
exchangeOracleAddress
377+
)
378+
.to.emit(escrow, 'Fund')
379+
.withArgs(fundAmount);
380+
381+
expect(await escrowFactory.hasEscrow(escrowAddress)).to.be.true;
382+
expect(await escrowFactory.lastEscrow()).to.equal(escrowAddress);
383+
});
384+
});
385+
});
232386
});

0 commit comments

Comments
 (0)