Skip to content

Commit

Permalink
add the factory
Browse files Browse the repository at this point in the history
  • Loading branch information
FedokDL committed Dec 10, 2024
1 parent 30df442 commit 16df1e1
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 1 deletion.
88 changes: 88 additions & 0 deletions smart-contracts/contracts/delegate/DelegatorFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";

import {IProvidersDelegator} from "../interfaces/delegate/IProvidersDelegator.sol";
import {IOwnable} from "../interfaces/utils/IOwnable.sol";

contract DelegatorFactory is OwnableUpgradeable, PausableUpgradeable, UUPSUpgradeable {
address public lumerinDiamond;
address public beacon;
mapping(address => address[]) public proxies;

event ProxyDeployed(address indexed proxyAddress);
event ImplementationUpdated(address indexed newImplementation);

constructor() {
_disableInitializers();
}

function DelegatorFactory_init(address _lumerinDiamond, address _implementation) external initializer {
__Pausable_init();
__Ownable_init();
__UUPSUpgradeable_init();

lumerinDiamond = _lumerinDiamond;

beacon = address(new UpgradeableBeacon(_implementation));
}

function pause() external onlyOwner {
_pause();
}

function unpause() external onlyOwner {
_unpause();
}

function deployProxy(
address feeTreasury_,
uint256 fee_,
string memory name_,
string memory endpoint_
) external whenNotPaused returns (address) {
bytes32 salt_ = _calculatePoolSalt(_msgSender());
address proxy_ = address(new BeaconProxy{salt: salt_}(beacon, bytes("")));

proxies[_msgSender()].push(address(proxy_));

IProvidersDelegator(proxy_).ProvidersDelegator_init(lumerinDiamond, feeTreasury_, fee_, name_, endpoint_);
IOwnable(proxy_).transferOwnership(_msgSender());

emit ProxyDeployed(address(proxy_));

return address(proxy_);
}

function predictProxyAddress(address _deployer) external view returns (address) {
bytes32 salt_ = _calculatePoolSalt(_deployer);

bytes32 bytecodeHash_ = keccak256(
abi.encodePacked(type(BeaconProxy).creationCode, abi.encode(address(beacon), bytes("")))
);

return Create2.computeAddress(salt_, bytecodeHash_);
}

function updateImplementation(address _newImplementation) external onlyOwner {
UpgradeableBeacon(beacon).upgradeTo(_newImplementation);

emit ImplementationUpdated(_newImplementation);
}

function version() external pure returns (uint256) {
return 1;
}

function _calculatePoolSalt(address sender_) internal view returns (bytes32) {
return keccak256(abi.encodePacked(sender_, proxies[sender_].length));
}

function _authorizeUpgrade(address) internal view override onlyOwner {}
}
15 changes: 15 additions & 0 deletions smart-contracts/contracts/interfaces/utils/IOwnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IOwnable {
/**
* @dev Returns the address of the current owner.
*/
function owner() external view returns (address);

/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner_) external;
}
12 changes: 12 additions & 0 deletions smart-contracts/contracts/mock/UUPSMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract UUPSMock is UUPSUpgradeable {
function version() external pure returns (uint256) {
return 999;
}

function _authorizeUpgrade(address) internal view override {}
}
200 changes: 200 additions & 0 deletions smart-contracts/test/delegate/DelegatorFactory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import {
DelegatorFactory,
LumerinDiamond,
MorpheusToken,
ProvidersDelegator,
ProvidersDelegator__factory,
UUPSMock,
} from '@ethers-v6';
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
import { expect } from 'chai';
import { ethers } from 'hardhat';

import { wei } from '@/scripts/utils/utils';
import {
deployDelegatorFactory,
deployFacetMarketplace,
deployFacetProviderRegistry,
deployLumerinDiamond,
deployMORToken,
} from '@/test/helpers/deployers';
import { Reverter } from '@/test/helpers/reverter';

describe('DelegatorFactory', () => {
const reverter = new Reverter();

let OWNER: SignerWithAddress;
let KYLE: SignerWithAddress;
let SHEV: SignerWithAddress;

let diamond: LumerinDiamond;
let delegatorFactory: DelegatorFactory;

let token: MorpheusToken;

before(async () => {
[OWNER, KYLE, SHEV] = await ethers.getSigners();

[diamond, token] = await Promise.all([deployLumerinDiamond(), deployMORToken()]);
await Promise.all([
deployFacetProviderRegistry(diamond),
deployFacetMarketplace(diamond, token, wei(0.0001), wei(900)),
]);

delegatorFactory = await deployDelegatorFactory(diamond);

await reverter.snapshot();
});

afterEach(reverter.revert);

describe('UUPS', () => {
describe('#DelegatorFactory_init', () => {
it('should revert if try to call init function twice', async () => {
await expect(delegatorFactory.DelegatorFactory_init(OWNER, OWNER)).to.be.rejectedWith(
'Initializable: contract is already initialized',
);
});
});
describe('#version', () => {
it('should return correct version', async () => {
expect(await delegatorFactory.version()).to.eq(1);
});
});
describe('#upgradeTo', () => {
it('should upgrade to the new version', async () => {
const factory = await ethers.getContractFactory('UUPSMock');
const newImpl = await factory.deploy();

await delegatorFactory.upgradeTo(newImpl);
const newDelegatorFactory = newImpl.attach(delegatorFactory) as UUPSMock;

expect(await newDelegatorFactory.version()).to.eq(999);
});
it('should throw error when caller is not an owner', async () => {
await expect(delegatorFactory.connect(KYLE).upgradeTo(KYLE)).to.be.revertedWith(
'Ownable: caller is not the owner',
);
});
});
});

describe('#deployProxy', () => {
let providersDelegatorFactory: ProvidersDelegator__factory;

before(async () => {
providersDelegatorFactory = await ethers.getContractFactory('ProvidersDelegator');
});

it('should deploy a new proxy', async () => {
await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint');

const proxy = providersDelegatorFactory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegator;

expect(await proxy.owner()).to.eq(SHEV);
expect(await proxy.fee()).to.eq(wei(0.1, 25));
expect(await proxy.feeTreasury()).to.eq(KYLE);
expect(await proxy.name()).to.eq('name');
expect(await proxy.endpoint()).to.eq('endpoint');
});
it('should deploy new proxies', async () => {
await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1');
await delegatorFactory.connect(SHEV).deployProxy(SHEV, wei(0.2, 25), 'name2', 'endpoint2');
await delegatorFactory.connect(KYLE).deployProxy(SHEV, wei(0.3, 25), 'name3', 'endpoint3');

let proxy = providersDelegatorFactory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegator;
expect(await proxy.owner()).to.eq(SHEV);
expect(await proxy.fee()).to.eq(wei(0.2, 25));
expect(await proxy.feeTreasury()).to.eq(SHEV);
expect(await proxy.name()).to.eq('name2');
expect(await proxy.endpoint()).to.eq('endpoint2');

proxy = providersDelegatorFactory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegator;
expect(await proxy.owner()).to.eq(KYLE);
expect(await proxy.fee()).to.eq(wei(0.3, 25));
expect(await proxy.feeTreasury()).to.eq(SHEV);
expect(await proxy.name()).to.eq('name3');
expect(await proxy.endpoint()).to.eq('endpoint3');
});
describe('#pause, #unpause', () => {
it('should revert when paused and not after the unpause', async () => {
await delegatorFactory.pause();
await expect(
delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1'),
).to.be.rejectedWith('Pausable: paused');

await delegatorFactory.unpause();
await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1');
});
it('should throw error when caller is not an owner', async () => {
await expect(delegatorFactory.connect(KYLE).pause()).to.be.revertedWith('Ownable: caller is not the owner');
});
it('should throw error when caller is not an owner', async () => {
await expect(delegatorFactory.connect(KYLE).unpause()).to.be.revertedWith('Ownable: caller is not the owner');
});
});
});

describe('#predictProxyAddress', () => {
it('should predict a proxy address', async () => {
const predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV);
await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint');

const proxyAddress = await delegatorFactory.proxies(SHEV, 0);

expect(proxyAddress).to.eq(predictedProxyAddress);
});
it('should predict proxy addresses', async () => {
let predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV);
await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint');
expect(await delegatorFactory.proxies(SHEV, 0)).to.eq(predictedProxyAddress);

predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV);
await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint');
expect(await delegatorFactory.proxies(SHEV, 1)).to.eq(predictedProxyAddress);

predictedProxyAddress = await delegatorFactory.predictProxyAddress(KYLE);
await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint');
expect(await delegatorFactory.proxies(KYLE, 0)).to.eq(predictedProxyAddress);

predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV);
await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint');
expect(await delegatorFactory.proxies(SHEV, 2)).to.eq(predictedProxyAddress);
});
});

describe('#updateImplementation', () => {
it('should update proxies implementation', async () => {
await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint');
await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint');
await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint');

const factory = await ethers.getContractFactory('UUPSMock');
const newImpl = await factory.deploy();

let proxy = factory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegator;
expect(await proxy.version()).to.eq(1);
proxy = factory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegator;
expect(await proxy.version()).to.eq(1);
proxy = factory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegator;
expect(await proxy.version()).to.eq(1);

await delegatorFactory.updateImplementation(newImpl);

proxy = factory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegator;
expect(await proxy.version()).to.eq(999);
proxy = factory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegator;
expect(await proxy.version()).to.eq(999);
proxy = factory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegator;
expect(await proxy.version()).to.eq(999);
});
it('should throw error when caller is not an owner', async () => {
await expect(delegatorFactory.connect(KYLE).updateImplementation(KYLE)).to.be.revertedWith(
'Ownable: caller is not the owner',
);
});
});
});

// npm run generate-types && npx hardhat test "test/delegate/DelegatorFactory.test"
// npx hardhat coverage --solcoverjs ./.solcover.ts --testfiles "test/delegate/DelegatorFactory.test"
2 changes: 1 addition & 1 deletion smart-contracts/test/delegate/ProviderDelegator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ describe('ProvidersDelegator', () => {
);
});
it('should throw error when the stake closed', async () => {
providersDelegator.setIsStakeClosed(true);
await providersDelegator.setIsStakeClosed(true);
await expect(providersDelegator.connect(KYLE).stake(wei(1))).to.be.revertedWithCustomError(
providersDelegator,
'StakeClosed',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ethers } from 'hardhat';

import { DelegatorFactory, LumerinDiamond } from '@/generated-types/ethers';

export const deployDelegatorFactory = async (diamond: LumerinDiamond): Promise<DelegatorFactory> => {
const [providersDelegatorImplFactory, delegatorFactoryImplFactory, proxyFactory] = await Promise.all([
ethers.getContractFactory('ProvidersDelegator'),
ethers.getContractFactory('DelegatorFactory'),
ethers.getContractFactory('ERC1967Proxy'),
]);

const delegatorFactoryImpl = await delegatorFactoryImplFactory.deploy();
const proxy = await proxyFactory.deploy(delegatorFactoryImpl, '0x');
const delegatorFactory = delegatorFactoryImpl.attach(proxy) as DelegatorFactory;

const providersDelegatorImpl = await providersDelegatorImplFactory.deploy();
await delegatorFactory.DelegatorFactory_init(diamond, providersDelegatorImpl);

return delegatorFactory;
};
1 change: 1 addition & 0 deletions smart-contracts/test/helpers/deployers/delegate/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './delegator-factory';
export * from './providers-delegator';

0 comments on commit 16df1e1

Please sign in to comment.