Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 11 additions & 1 deletion packages/core/contracts/Escrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ contract Escrow is IEscrow, ReentrancyGuard {
uint8 _exchangeOracleFeePercentage,
string calldata _url,
string calldata _hash
) external override adminOrLauncher notExpired {
) external override adminLauncherOrFactory notExpired {
require(_reputationOracle != address(0), 'Invalid reputation oracle');
require(_recordingOracle != address(0), 'Invalid recording oracle');
require(_exchangeOracle != address(0), 'Invalid exchange oracle');
Expand Down Expand Up @@ -495,6 +495,16 @@ contract Escrow is IEscrow, ReentrancyGuard {
_;
}

modifier adminLauncherOrFactory() {
require(
msg.sender == admin ||
msg.sender == launcher ||
msg.sender == escrowFactory,
'Unauthorised'
);
_;
}

modifier adminOrReputationOracle() {
require(
msg.sender == admin || msg.sender == reputationOracle,
Expand Down
96 changes: 81 additions & 15 deletions packages/core/contracts/EscrowFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ pragma solidity ^0.8.0;

import '@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';

import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import './interfaces/IStaking.sol';
import './Escrow.sol';

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

using SafeERC20 for IERC20;

event Launched(address token, address escrow);
event LaunchedV2(address token, address escrow, string jobRequesterId);
event SetStakingAddress(address indexed stakingAddress);
Expand All @@ -41,30 +44,93 @@ contract EscrowFactory is OwnableUpgradeable, UUPSUpgradeable {
_setEscrowAdmin(msg.sender);
}

/**
* @dev Creates a new Escrow contract.
*
* @param token Token address to be associated with the Escrow contract.
* @param jobRequesterId String identifier for the job requester, used for tracking purposes.
*
* @return The address of the newly created Escrow contract.
*/
function createEscrow(
address token,
string memory jobRequesterId
) external returns (address) {
function _launchEscrow(
address _token,
string calldata _jobRequesterId
) private {
uint256 availableStake = IStaking(staking).getAvailableStake(
msg.sender
);
require(availableStake >= minimumStake, 'Insufficient stake');
require(admin != address(0), ERROR_ZERO_ADDRESS);

Escrow escrow = new Escrow(token, msg.sender, admin, STANDARD_DURATION);
Escrow escrow = new Escrow(
_token,
msg.sender,
admin,
STANDARD_DURATION
);
counter++;
escrowCounters[address(escrow)] = counter;
lastEscrow = address(escrow);

emit LaunchedV2(token, lastEscrow, jobRequesterId);
emit LaunchedV2(_token, lastEscrow, _jobRequesterId);
}

/**
* @dev Creates a new Escrow contract.
*
* @param _token Token address to be associated with the Escrow contract.
* @param _jobRequesterId String identifier for the job requester, used for tracking purposes.
*
* @return The address of the newly created Escrow contract.
*/
function createEscrow(
address _token,
string calldata _jobRequesterId
) external returns (address) {
_launchEscrow(_token, _jobRequesterId);
return lastEscrow;
}

/**
* @dev Creates a new Escrow contract and funds it in one transaction.
* Requires the caller to have approved the factory for the token and amount.
* @param _token Token address to be associated with the Escrow contract.
* @param _amount Amount of tokens to fund the Escrow with.
* @param _jobRequesterId String identifier for the job requester, used for tracking purposes.
* @param _reputationOracle Address of the reputation oracle.
* @param _recordingOracle Address of the recording oracle.
* @param _exchangeOracle Address of the exchange oracle.
* @param _reputationOracleFeePercentage Fee percentage for the reputation oracle.
* @param _recordingOracleFeePercentage Fee percentage for the recording oracle.
* @param _exchangeOracleFeePercentage Fee percentage for the exchange oracle.
* @param _url URL for the escrow manifest.
* @param _hash Hash of the escrow manifest.
* @return The address of the newly created Escrow contract.
*/
function createFundAndSetupEscrow(
address _token,
uint256 _amount,
string calldata _jobRequesterId,
address _reputationOracle,
address _recordingOracle,
address _exchangeOracle,
uint8 _reputationOracleFeePercentage,
uint8 _recordingOracleFeePercentage,
uint8 _exchangeOracleFeePercentage,
string calldata _url,
string calldata _hash
) external returns (address) {
require(_amount > 0, 'Amount is 0');

_launchEscrow(_token, _jobRequesterId);
IERC20(_token).safeTransferFrom(
msg.sender,
address(lastEscrow),
_amount
);
Escrow(lastEscrow).setup(
_reputationOracle,
_recordingOracle,
_exchangeOracle,
_reputationOracleFeePercentage,
_recordingOracleFeePercentage,
_exchangeOracleFeePercentage,
_url,
_hash
);

return lastEscrow;
}

Expand Down
176 changes: 165 additions & 11 deletions packages/core/test/EscrowFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ import { ethers, upgrades } from 'hardhat';
import { EscrowFactory, HMToken, Staking } from '../typechain-types';
import { faker } from '@faker-js/faker';

let owner: Signer, launcher: Signer, admin: Signer;
let owner: Signer,
launcher1: Signer,
admin: Signer,
launcher2: Signer,
exchangeOracle: Signer,
recordingOracle: Signer,
reputationOracle: Signer;
let exchangeOracleAddress: string,
recordingOracleAddress: string,
reputationOracleAddress: string;

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

describe('EscrowFactory', function () {
before(async () => {
[owner, launcher, admin] = await ethers.getSigners();
[
owner,
launcher1,
admin,
launcher2,
exchangeOracle,
recordingOracle,
reputationOracle,
] = await ethers.getSigners();
exchangeOracleAddress = await exchangeOracle.getAddress();
recordingOracleAddress = await recordingOracle.getAddress();
reputationOracleAddress = await reputationOracle.getAddress();

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

await token
.connect(owner)
.transfer(await launcher.getAddress(), ethers.parseEther('100000'));
.transfer(await launcher1.getAddress(), ethers.parseEther('100000'));
await token
.connect(owner)
.transfer(await launcher2.getAddress(), ethers.parseEther('100000'));

const Staking = await ethers.getContractFactory('Staking');
staking = await Staking.deploy(
Expand All @@ -50,8 +73,6 @@ describe('EscrowFactory', function () {
);
stakingAddress = await staking.getAddress();

await token.connect(launcher).approve(await staking.getAddress(), 1000);

const EscrowFactory = await ethers.getContractFactory(
'contracts/EscrowFactory.sol:EscrowFactory'
);
Expand Down Expand Up @@ -109,7 +130,7 @@ describe('EscrowFactory', function () {

it('reverts when caller is not the owner', async () => {
await expect(
escrowFactory.connect(launcher).setStakingAddress(stakingAddress)
escrowFactory.connect(launcher1).setStakingAddress(stakingAddress)
).to.be.revertedWith('Ownable: caller is not the owner');
});
});
Expand Down Expand Up @@ -147,7 +168,7 @@ describe('EscrowFactory', function () {

it('reverts when caller is not the owner', async () => {
await expect(
escrowFactory.connect(launcher).setMinimumStake(0)
escrowFactory.connect(launcher1).setMinimumStake(0)
).to.be.revertedWith('Ownable: caller is not the owner');
});
});
Expand Down Expand Up @@ -176,7 +197,7 @@ describe('EscrowFactory', function () {

it('reverts when caller is not the owner', async () => {
await expect(
escrowFactory.connect(launcher).setAdmin(await admin.getAddress())
escrowFactory.connect(launcher1).setAdmin(await admin.getAddress())
).to.be.revertedWith('Ownable: caller is not the owner');
});
});
Expand All @@ -197,18 +218,18 @@ describe('EscrowFactory', function () {
it('reverts when launcher has insufficient stake', async () => {
await expect(
escrowFactory
.connect(launcher)
.connect(launcher1)
.createEscrow(tokenAddress, FIXTURE_REQUESTER_ID)
).to.be.revertedWith('Insufficient stake');
});
});

describe('succeeds', () => {
it('creates an escrow successfully', async () => {
await stake(launcher);
await stake(launcher1);

const tx = await escrowFactory
.connect(launcher)
.connect(launcher1)
.createEscrow(tokenAddress, FIXTURE_REQUESTER_ID);

await expect(tx)
Expand All @@ -229,4 +250,137 @@ describe('EscrowFactory', function () {
});
});
});

describe('createFundAndSetupEscrow()', () => {
const fee = faker.number.int({ min: 1, max: 5 });
const manifestUrl = faker.internet.url();
const manifestHash = faker.string.alphanumeric(46);
const fundAmount = ethers.parseEther(
faker.finance.amount({ min: 1, max: 100 })
);
describe('reverts', () => {
it('reverts when fund amount is 0', async () => {
await expect(
escrowFactory
.connect(launcher2)
.createFundAndSetupEscrow(
tokenAddress,
0,
FIXTURE_REQUESTER_ID,
reputationOracleAddress,
recordingOracleAddress,
exchangeOracleAddress,
fee,
fee,
fee,
manifestUrl,
manifestHash
)
).to.be.revertedWith('Amount is 0');
});

it('reverts when launcher has insufficient stake', async () => {
await expect(
escrowFactory
.connect(launcher2)
.createFundAndSetupEscrow(
tokenAddress,
fundAmount,
FIXTURE_REQUESTER_ID,
reputationOracleAddress,
recordingOracleAddress,
exchangeOracleAddress,
fee,
fee,
fee,
manifestUrl,
manifestHash
)
).to.be.revertedWith('Insufficient stake');
});

it('reverts when allowance is too low', async () => {
await stake(launcher2);
await token
.connect(launcher2)
.approve(await escrowFactory.getAddress(), fundAmount / 2n);

await expect(
escrowFactory
.connect(launcher2)
.createFundAndSetupEscrow(
tokenAddress,
fundAmount,
FIXTURE_REQUESTER_ID,
reputationOracleAddress,
recordingOracleAddress,
exchangeOracleAddress,
fee,
fee,
fee,
manifestUrl,
manifestHash
)
).to.be.revertedWith('Spender allowance too low');
});
});

describe('succeeds', () => {
it('creates an escrow successfully', async () => {
await stake(launcher2);

await token
.connect(launcher2)
.approve(await escrowFactory.getAddress(), fundAmount);

const tx = await escrowFactory
.connect(launcher2)
.createFundAndSetupEscrow(
tokenAddress,
fundAmount,
FIXTURE_REQUESTER_ID,
reputationOracleAddress,
recordingOracleAddress,
exchangeOracleAddress,
fee,
fee,
fee,
manifestUrl,
manifestHash
);

const receipt = await tx.wait();
const event = (
receipt?.logs?.find(({ topics }) =>
topics.includes(ethers.id('LaunchedV2(address,address,string)'))
) as EventLog
)?.args;

expect(event).to.not.be.undefined;
const escrowAddress = event[1];

const escrow = await ethers.getContractAt(
'contracts/Escrow.sol:Escrow',
escrowAddress
);

await expect(tx)
.to.emit(escrowFactory, 'LaunchedV2')
.withArgs(tokenAddress, escrowAddress, FIXTURE_REQUESTER_ID)
.to.emit(escrow, 'PendingV2')
.withArgs(
manifestUrl,
manifestHash,
reputationOracleAddress,
recordingOracleAddress,
exchangeOracleAddress
)
.to.emit(escrow, 'Fund')
.withArgs(fundAmount);

expect(await escrowFactory.hasEscrow(escrowAddress)).to.be.true;
expect(await escrowFactory.lastEscrow()).to.equal(escrowAddress);
});
});
});
});
Loading
Loading