Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VAL-117 Make Pool/WithdrawControllers upgradeable + Loan #131

Merged
merged 5 commits into from
Dec 7, 2022
Merged
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
31 changes: 16 additions & 15 deletions contracts/Loan.sol
Original file line number Diff line number Diff line change
@@ -8,28 +8,29 @@ import "./interfaces/IServiceConfiguration.sol";
import "./libraries/LoanLib.sol";
import "./CollateralVault.sol";
import "./FundingVault.sol";
import "./upgrades/interfaces/IBeaconImplementation.sol";

/**
* @title Loan
*
* Empty Loan contract.
*/
contract Loan is ILoan {
contract Loan is ILoan, IBeaconImplementation {
using SafeMath for uint256;
uint256 constant RAY = 10**27;

IServiceConfiguration private immutable _serviceConfiguration;
address private immutable _factory;
IServiceConfiguration private _serviceConfiguration;
address private _factory;
ILoanLifeCycleState private _state = ILoanLifeCycleState.Requested;
address private immutable _borrower;
address private immutable _pool;
CollateralVault public immutable _collateralVault;
FundingVault public immutable fundingVault;
address private _borrower;
address private _pool;
CollateralVault public _collateralVault;
FundingVault public fundingVault;
address[] private _fungibleCollateral;
ILoanNonFungibleCollateral[] private _nonFungibleCollateral;
uint256 public immutable createdAt;
address public immutable liquidityAsset;
uint256 public immutable payment;
uint256 public createdAt;
address public liquidityAsset;
uint256 public payment;
uint256 public outstandingPrincipal;
uint256 public paymentsRemaining;
uint256 public paymentDueDate;
@@ -111,15 +112,15 @@ contract Loan is ILoan {
_;
}

constructor(
IServiceConfiguration serviceConfiguration,
function initialize(
address serviceConfiguration,
address factory_,
address borrower_,
address pool_,
address liquidityAsset_,
ILoanSettings memory settings_
) {
_serviceConfiguration = serviceConfiguration;
) public virtual initializer {
_serviceConfiguration = IServiceConfiguration(serviceConfiguration);
_factory = factory_;
_borrower = borrower_;
_pool = pool_;
@@ -130,7 +131,7 @@ contract Loan is ILoan {
settings = settings_;

LoanLib.validateLoan(
serviceConfiguration,
_serviceConfiguration,
settings.duration,
settings.paymentPeriod,
settings.principal,
38 changes: 18 additions & 20 deletions contracts/LoanFactory.sol
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import "./Loan.sol";
import "./interfaces/IServiceConfiguration.sol";
import "./interfaces/ILoanFactory.sol";
import "./Loan.sol";
import "./upgrades/BeaconProxyFactory.sol";
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";

/**
* @title LoanFactory
*/
contract LoanFactory {
/**
* @dev Reference to the ServiceConfiguration contract
*/
IServiceConfiguration internal _serviceConfiguration;

contract LoanFactory is ILoanFactory, BeaconProxyFactory {
/**
* @dev Mapping of created loans
*/
mapping(address => bool) internal _isLoan;

/**
* @dev Emitted when a Loan is created.
*/
event LoanCreated(address indexed addr);

constructor(address serviceConfiguration) {
_serviceConfiguration = IServiceConfiguration(serviceConfiguration);
}
@@ -41,30 +34,35 @@ contract LoanFactory {
_serviceConfiguration.paused() == false,
"LoanFactory: Protocol paused"
);
require(implementation != address(0), "LoanFactory: no implementation");
address addr = initializeLoan(borrower, pool, liquidityAsset, settings);
emit LoanCreated(addr);
_isLoan[addr] = true;
return addr;
}

/**
* @dev Internal initialization of Loan contract
* @dev Internal initialization of Beacon proxy for Loans
*/
function initializeLoan(
address borrower,
address pool,
address liquidityAsset,
ILoanSettings memory settings
) internal virtual returns (address) {
Loan loan = new Loan(
_serviceConfiguration,
BeaconProxy proxy = new BeaconProxy(
address(this),
borrower,
pool,
liquidityAsset,
settings
abi.encodeWithSelector(
Loan.initialize.selector,
address(_serviceConfiguration),
address(this),
borrower,
pool,
liquidityAsset,
settings
)
);
return address(loan);
return address(proxy);
}

/**
49 changes: 6 additions & 43 deletions contracts/PoolFactory.sol
Original file line number Diff line number Diff line change
@@ -5,17 +5,12 @@ import "./Pool.sol";
import "./interfaces/IServiceConfiguration.sol";
import "./interfaces/IPoolFactory.sol";
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import "./upgrades/interfaces/IBeacon.sol";
import "./upgrades/BeaconProxyFactory.sol";

/**
* @title PoolFactory
*/
contract PoolFactory is IPoolFactory, IBeacon {
/**
* @dev Reference to the ServiceConfiguration contract
*/
address internal _serviceConfiguration;

contract PoolFactory is IPoolFactory, BeaconProxyFactory {
/**
* @dev Reference to the WithdrawControllerFactory contract
*/
@@ -26,44 +21,16 @@ contract PoolFactory is IPoolFactory, IBeacon {
*/
address internal _poolControllerFactory;

/**
* @inheritdoc IBeacon
*/
address public implementation;

/**
* @dev Modifier that requires that the sender is registered as a protocol deployer.
*/
modifier onlyDeployer() {
require(
IServiceConfiguration(_serviceConfiguration).isDeployer(msg.sender),
"Upgrade: unauthorized"
);
_;
}

constructor(
address serviceConfiguration,
address withdrawControllerFactory,
address poolControllerFactory
) {
_serviceConfiguration = serviceConfiguration;
_serviceConfiguration = IServiceConfiguration(serviceConfiguration);
_withdrawControllerFactory = withdrawControllerFactory;
_poolControllerFactory = poolControllerFactory;
}

/**
* @inheritdoc IBeacon
*/
function setImplementation(address newImplementation)
external
override
onlyDeployer
{
implementation = newImplementation;
emit ImplementationSet(newImplementation);
}

/**
* @dev Creates a pool
* @dev Emits `PoolCreated` event.
@@ -77,7 +44,7 @@ contract PoolFactory is IPoolFactory, IBeacon {
"PoolFactory: no implementation set"
);
require(
IServiceConfiguration(_serviceConfiguration).paused() == false,
_serviceConfiguration.paused() == false,
"PoolFactory: Protocol paused"
);
require(
@@ -92,9 +59,7 @@ contract PoolFactory is IPoolFactory, IBeacon {
}
require(
settings.firstLossInitialMinimum >=
IServiceConfiguration(_serviceConfiguration).firstLossMinimum(
liquidityAsset
),
_serviceConfiguration.firstLossMinimum(liquidityAsset),
"PoolFactory: Invalid first loss minimum"
);
require(
@@ -110,9 +75,7 @@ contract PoolFactory is IPoolFactory, IBeacon {
"PoolFactory: Invalid request cancellation fee"
);
require(
IServiceConfiguration(_serviceConfiguration).isLiquidityAsset(
liquidityAsset
),
_serviceConfiguration.isLiquidityAsset(liquidityAsset),
"PoolFactory: invalid asset"
);

8 changes: 1 addition & 7 deletions contracts/ServiceConfiguration.sol
Original file line number Diff line number Diff line change
@@ -100,6 +100,7 @@ contract ServiceConfiguration is
paused = false;
firstLossFeeBps = 500;
protocolFeeBps = 0;
_serviceConfiguration = IServiceConfiguration(address(this));

_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
@@ -181,11 +182,4 @@ contract ServiceConfiguration is
firstLossFeeBps = value;
emit ParameterSet("firstLossFeeBps", value);
}

/**
* @inheritdoc IServiceConfigurable
*/
function serviceConfiguration() external view override returns (address) {
return address(this);
}
}
7 changes: 4 additions & 3 deletions contracts/controllers/PoolController.sol
Original file line number Diff line number Diff line change
@@ -9,11 +9,12 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet
import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../upgrades/interfaces/IBeaconImplementation.sol";

/**
* @title WithdrawState
*/
contract PoolController is IPoolController {
contract PoolController is IPoolController, IBeaconImplementation {
using SafeERC20 for IERC20;

IPool public pool;
@@ -91,13 +92,13 @@ contract PoolController is IPoolController {
_;
}

constructor(
function initialize(
address pool_,
address serviceConfiguration_,
address admin_,
address liquidityAsset_,
IPoolConfigurableSettings memory poolSettings_
) {
) public initializer {
_serviceConfiguration = serviceConfiguration_;
pool = IPool(pool_);

7 changes: 4 additions & 3 deletions contracts/controllers/WithdrawController.sol
Original file line number Diff line number Diff line change
@@ -7,11 +7,12 @@ import "./interfaces/IPoolController.sol";
import "../libraries/PoolLib.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "../upgrades/interfaces/IBeaconImplementation.sol";

/**
* @title WithdrawState
*/
contract WithdrawController is IWithdrawController {
contract WithdrawController is IWithdrawController, IBeaconImplementation {
using SafeMath for uint256;
using EnumerableSet for EnumerableSet.AddressSet;

@@ -44,9 +45,9 @@ contract WithdrawController is IWithdrawController {
}

/**
* @dev Constructor for a Pool's withdraw state
* @dev Initializer for a Pool's withdraw state
*/
constructor(address pool) {
function initialize(address pool) public initializer {
_pool = IPool(pool);
}

34 changes: 18 additions & 16 deletions contracts/factories/PoolControllerFactory.sol
Original file line number Diff line number Diff line change
@@ -3,19 +3,16 @@ pragma solidity ^0.8.16;

import "../controllers/PoolController.sol";
import "../interfaces/IServiceConfiguration.sol";
import "./interfaces/IPoolControllerFactory.sol";
import "../factories/interfaces/IPoolControllerFactory.sol";
import "../upgrades/BeaconProxyFactory.sol";
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";

/**
* @title PoolAdmin controller Factory
*/
contract PoolControllerFactory is IPoolControllerFactory {
/**
* @dev Reference to the ServiceConfiguration contract
*/
address private _serviceConfiguration;

contract PoolControllerFactory is IPoolControllerFactory, BeaconProxyFactory {
constructor(address serviceConfiguration) {
_serviceConfiguration = serviceConfiguration;
_serviceConfiguration = IServiceConfiguration(serviceConfiguration);
}

/**
@@ -29,18 +26,23 @@ contract PoolControllerFactory is IPoolControllerFactory {
IPoolConfigurableSettings memory poolSettings
) public virtual returns (address addr) {
require(
IServiceConfiguration(_serviceConfiguration).paused() == false,
_serviceConfiguration.paused() == false,
"PoolControllerFactory: Protocol paused"
);
require(implementation != address(0), "PoolControllerFactory: no impl");

PoolController controller = new PoolController(
pool,
serviceConfiguration,
admin,
liquidityAsset,
poolSettings
BeaconProxy proxy = new BeaconProxy(
address(this),
abi.encodeWithSelector(
PoolController.initialize.selector,
pool,
serviceConfiguration,
admin,
liquidityAsset,
poolSettings
)
);
addr = address(controller);
addr = address(proxy);
emit PoolControllerCreated(addr, admin);
}
}
24 changes: 14 additions & 10 deletions contracts/factories/WithdrawControllerFactory.sol
Original file line number Diff line number Diff line change
@@ -4,18 +4,18 @@ pragma solidity ^0.8.16;
import "../controllers/WithdrawController.sol";
import "../interfaces/IServiceConfiguration.sol";
import "./interfaces/IWithdrawControllerFactory.sol";
import "../upgrades/BeaconProxyFactory.sol";
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";

/**
* @title WithdrawController Factory
*/
contract WithdrawControllerFactory is IWithdrawControllerFactory {
/**
* @dev Reference to the ServiceConfiguration contract
*/
address private _serviceConfiguration;

contract WithdrawControllerFactory is
IWithdrawControllerFactory,
BeaconProxyFactory
{
constructor(address serviceConfiguration) {
_serviceConfiguration = serviceConfiguration;
_serviceConfiguration = IServiceConfiguration(serviceConfiguration);
}

/**
@@ -27,12 +27,16 @@ contract WithdrawControllerFactory is IWithdrawControllerFactory {
returns (address addr)
{
require(
IServiceConfiguration(_serviceConfiguration).paused() == false,
_serviceConfiguration.paused() == false,
"WithdrawControllerFactory: Protocol paused"
);

WithdrawController withdrawController = new WithdrawController(pool);
addr = address(withdrawController);
BeaconProxy proxy = new BeaconProxy(
address(this),
abi.encodeWithSelector(WithdrawController.initialize.selector, pool)
);

addr = address(proxy);
emit WithdrawControllerCreated(addr);
}
}
25 changes: 25 additions & 0 deletions contracts/interfaces/ILoanFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import "../interfaces/ILoan.sol";

/**
* @title ILoanFactory
*/
interface ILoanFactory {
/**
* @dev Emitted when a loan is created.
*/
event LoanCreated(address indexed addr);

/**
* @dev Creates a loan
* @dev Emits `LoanCreated` event.
*/
function createLoan(
address borrower,
address pool,
address liquidityAsset,
ILoanSettings memory settings
) external returns (address);
}
12 changes: 12 additions & 0 deletions contracts/mocks/upgrades/LoanMockV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import "../../Loan.sol";
import "./MockUpgrade.sol";

/**
* @dev Simulated new Loan implementation
*/
contract LoanMockV2 is Loan, MockUpgrade {

}
12 changes: 12 additions & 0 deletions contracts/mocks/upgrades/PoolControllerMockV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import "../../controllers/PoolController.sol";
import "./MockUpgrade.sol";

/**
* @dev Simulated new ServiceConfiguration implementation
*/
contract PoolControllerMockV2 is PoolController, MockUpgrade {

}
12 changes: 12 additions & 0 deletions contracts/mocks/upgrades/WithdrawControllerMockV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import "../../controllers/WithdrawController.sol";
import "./MockUpgrade.sol";

/**
* @dev Simulated new ServiceConfiguration implementation
*/
contract WithdrawControllerMockV2 is WithdrawController, MockUpgrade {

}
30 changes: 12 additions & 18 deletions contracts/permissioned/PermissionedLoan.sol
Original file line number Diff line number Diff line change
@@ -26,28 +26,22 @@ contract PermissionedLoan is Loan {
_;
}

/**
* @dev The constructor for the PermissionedLoan contract. It holds a reference
* to the corresponding PermissionedPool's access control contract to enforce the
* same controls on borrowers.
*/
constructor(
IServiceConfiguration serviceConfiguration,
address factory,
address borrower,
address pool,
function initialize(
address serviceConfiguration,
address factory_,
address borrower_,
address pool_,
address liquidityAsset_,
ILoanSettings memory settings_
)
Loan(
) public override {
super.initialize(
serviceConfiguration,
factory,
borrower,
pool,
factory_,
borrower_,
pool_,
liquidityAsset_,
settings_
)
{
poolAccessControl = PermissionedPool(pool).poolAccessControl();
);
poolAccessControl = PermissionedPool(pool_).poolAccessControl();
}
}
20 changes: 12 additions & 8 deletions contracts/permissioned/PermissionedLoanFactory.sol
Original file line number Diff line number Diff line change
@@ -15,22 +15,26 @@ contract PermissionedLoanFactory is LoanFactory {

/**
* @inheritdoc LoanFactory
* @dev Deploys PermissionedLoan
* @dev Deploys BeaconProxy for PermissionedLoan
*/
function initializeLoan(
address borrower,
address pool,
address liquidityAsset,
ILoanSettings memory settings
) internal override returns (address) {
Loan loan = new PermissionedLoan(
_serviceConfiguration,
BeaconProxy proxy = new BeaconProxy(
address(this),
borrower,
pool,
liquidityAsset,
settings
abi.encodeWithSelector(
PermissionedLoan.initialize.selector,
address(_serviceConfiguration),
address(this),
borrower,
pool,
liquidityAsset,
settings
)
);
return address(loan);
return address(proxy);
}
}
2 changes: 1 addition & 1 deletion contracts/permissioned/PermissionedPoolFactory.sol
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ contract PermissionedPoolFactory is PoolFactory {
*/
modifier onlyVerifiedPoolAdmin() {
require(
IPermissionedServiceConfiguration(_serviceConfiguration)
IPermissionedServiceConfiguration(address(_serviceConfiguration))
.poolAdminAccessControl()
.isAllowed(msg.sender),
"CALLER_NOT_ADMIN"
43 changes: 43 additions & 0 deletions contracts/upgrades/BeaconProxyFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import "../interfaces/IServiceConfiguration.sol";
import "./interfaces/IBeacon.sol";

/**
* @title BeaconProxyFactory
* @dev Base contract for emitting new Beacon proxy contracts.
*/
abstract contract BeaconProxyFactory is IBeacon {
/**
* @dev Address of the protocol service configuration
*/
IServiceConfiguration internal _serviceConfiguration;

/**
* @dev Modifier that requires that the sender is registered as a protocol deployer.
*/
modifier onlyDeployer() {
require(
_serviceConfiguration.isDeployer(msg.sender),
"Upgrade: unauthorized"
);
_;
}

/**
* @inheritdoc IBeacon
*/
address public implementation;

/**
* @inheritdoc IBeacon
*/
function setImplementation(address newImplementation)
external
onlyDeployer
{
implementation = newImplementation;
emit ImplementationSet(newImplementation);
}
}
16 changes: 7 additions & 9 deletions contracts/upgrades/DeployerUUPSUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -4,26 +4,24 @@ pragma solidity ^0.8.16;
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "../interfaces/IServiceConfiguration.sol";
import "../interfaces/IServiceConfigurable.sol";

/**
* @title DeployerUUPSUpgradeable
* @dev Base upgradeable contract that ensures only the protocol Deployer can deploy
* upgrades.
*/
abstract contract DeployerUUPSUpgradeable is
IServiceConfigurable,
Initializable,
UUPSUpgradeable
{
abstract contract DeployerUUPSUpgradeable is Initializable, UUPSUpgradeable {
/**
* @dev Address of the protocol service configuration
*/
IServiceConfiguration internal _serviceConfiguration;

/**
* @dev Modifier that requires that the sender is registered as a protocol deployer.
*/
modifier onlyDeployer() {
require(
IServiceConfiguration(this.serviceConfiguration()).isDeployer(
msg.sender
),
_serviceConfiguration.isDeployer(msg.sender),
"Upgrade: unauthorized"
);
_;
44 changes: 38 additions & 6 deletions test/Loan.test.ts
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ describe("Loan", () => {
principal: 500_000
})
) {
const { admin, operator, poolAdmin, borrower, lender, other } =
const { admin, deployer, operator, poolAdmin, borrower, lender, other } =
await getCommonSigners();

// Create a pool
@@ -41,18 +41,25 @@ describe("Loan", () => {
const LoanLib = await ethers.getContractFactory("LoanLib");
const loanLib = await LoanLib.deploy();

const LoanFactory = await ethers.getContractFactory("LoanFactory", {
libraries: {
LoanLib: loanLib.address
}
});
const LoanFactory = await ethers.getContractFactory("LoanFactory");
const loanFactory = await LoanFactory.deploy(serviceConfiguration.address);
await loanFactory.deployed();

await serviceConfiguration
.connect(operator)
.setLoanFactory(loanFactory.address, true);

// Deploy Loan implementation contract
const LoanImpl = await ethers.getContractFactory("Loan", {
libraries: {
LoanLib: loanLib.address
}
});
const loanImpl = await LoanImpl.deploy();

// Set implementation on the LoanFactory
await loanFactory.connect(deployer).setImplementation(loanImpl.address);

const depositAmount = 1_000_000;
await liquidityAsset.mint(lender.address, 10_000_000);
await liquidityAsset.connect(lender).approve(pool.address, depositAmount);
@@ -92,8 +99,10 @@ describe("Loan", () => {

return {
pool,
deployer,
poolController,
loan,
loanLib,
loanFactory,
operator,
poolAdmin,
@@ -1466,4 +1475,27 @@ describe("Loan", () => {
);
});
});

describe("Upgrades", () => {
it("can be upgraded", async () => {
const { loan, loanFactory, deployer, loanLib } = await loadFixture(
deployFixture
);

// new implementation
const V2Impl = await ethers.getContractFactory("LoanMockV2", {
libraries: {
LoanLib: loanLib.address
}
});
const v2Impl = await V2Impl.deploy();
await expect(
loanFactory.connect(deployer).setImplementation(v2Impl.address)
).to.emit(loanFactory, "ImplementationSet");

// Check that it upgraded
const loanV2 = V2Impl.attach(loan.address);
expect(await loanV2.foo()).to.be.true;
});
});
});
2 changes: 1 addition & 1 deletion test/Pool.test.ts
Original file line number Diff line number Diff line change
@@ -1430,7 +1430,7 @@ describe("Pool", () => {
});
});

describe("Updates", () => {
describe("Upgrades", () => {
it("Can be upgraded through the beacon", async () => {
const { pool, poolFactory, deployer, poolLib } = await loadFixture(
loadPoolFixture
2 changes: 1 addition & 1 deletion test/PoolFactory.test.ts
Original file line number Diff line number Diff line change
@@ -186,7 +186,7 @@ describe("PoolFactory", () => {
).to.emit(poolFactory, "PoolCreated");
});

it("deployer can set new implementations", async () => {
it.only("deployer can set new implementations", async () => {
const {
poolFactory,
liquidityAsset: mockNewImplementation,
43 changes: 40 additions & 3 deletions test/controllers/PoolController.test.ts
Original file line number Diff line number Diff line change
@@ -14,20 +14,29 @@ import {
deployPool,
depositToPool
} from "../support/pool";
import { getCommonSigners } from "../support/utils";

describe("PoolController", () => {
async function loadPoolFixture() {
const [operator, poolAdmin, borrower, otherAccount, ...otherAccounts] =
await ethers.getSigners();
const {
operator,
deployer,
poolAdmin,
borrower,
otherAccount,
otherAccounts
} = await getCommonSigners();

const {
pool,
poolLib,
poolControllerFactory,
withdrawControllerFactory,
liquidityAsset,
poolController,
serviceConfiguration,
withdrawController
} = await deployPool({
operator,
poolAdmin: poolAdmin
});

@@ -57,11 +66,15 @@ describe("PoolController", () => {

return {
operator,
deployer,
poolAdmin,
borrower,
otherAccount,
otherAccounts,
pool,
poolLib,
poolControllerFactory,
withdrawControllerFactory,
loan,
otherLoan,
liquidityAsset,
@@ -1078,4 +1091,28 @@ describe("PoolController", () => {
await expect(tx2).to.changeTokenBalance(liquidityAsset, poolAdmin, 100);
});
});

describe("Upgrades", () => {
it("Can be upgraded", async () => {
const { poolController, poolLib, poolControllerFactory, deployer } =
await loadFixture(loadPoolFixture);

// new implementation
const V2Impl = await ethers.getContractFactory("PoolControllerMockV2", {
libraries: {
PoolLib: poolLib.address
}
});
const v2Impl = await V2Impl.deploy();
await expect(
poolControllerFactory
.connect(deployer)
.setImplementation(v2Impl.address)
).to.emit(poolControllerFactory, "ImplementationSet");

// Check that it upgraded
const poolControllerV2 = V2Impl.attach(poolController.address);
expect(await poolControllerV2.foo()).to.be.true;
});
});
});
56 changes: 51 additions & 5 deletions test/controllers/WithdrawController.test.ts
Original file line number Diff line number Diff line change
@@ -2,26 +2,41 @@ import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";
import { activatePool, deployPool, depositToPool } from "../support/pool";
import { getCommonSigners } from "../support/utils";

describe("WithdrawController", () => {
async function loadPoolFixture() {
const [operator, poolAdmin, borrower, otherAccount, ...otherAccounts] =
await ethers.getSigners();

const { pool, liquidityAsset, withdrawController } = await deployPool({
const {
operator,
deployer,
poolAdmin,
borrower,
otherAccount,
otherAccounts
} = await getCommonSigners();

const {
pool,
withdrawControllerFactory,
poolLib,
liquidityAsset,
withdrawController
} = await deployPool({
poolAdmin: poolAdmin
});

return {
operator,
deployer,
poolAdmin,
borrower,
otherAccount,
otherAccounts,
pool,
liquidityAsset,
withdrawController
withdrawController,
withdrawControllerFactory,
poolLib
};
}

@@ -256,4 +271,35 @@ describe("WithdrawController", () => {
expect(await withdrawController.totalRedeemableShares()).to.equal(1); // snapshot dust
});
});

describe("Upgrades", () => {
it("Can be upgraded", async () => {
const {
withdrawController,
withdrawControllerFactory,
poolLib,
deployer
} = await loadFixture(loadPoolFixture);

// new implementation
const V2Impl = await ethers.getContractFactory(
"WithdrawControllerMockV2",
{
libraries: {
PoolLib: poolLib.address
}
}
);
const v2Impl = await V2Impl.deploy();
await expect(
withdrawControllerFactory
.connect(deployer)
.setImplementation(v2Impl.address)
).to.emit(withdrawControllerFactory, "ImplementationSet");

// Check that it upgraded
const withdrawControllerV2 = V2Impl.attach(withdrawController.address);
expect(await withdrawControllerV2.foo()).to.be.true;
});
});
});
60 changes: 40 additions & 20 deletions test/permissioned/PermissionedLoanFactory.test.ts
Original file line number Diff line number Diff line change
@@ -2,35 +2,41 @@ import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";
import { deployMockERC20 } from "../support/erc20";
import { DEFAULT_LOAN_SETTINGS } from "../support/loan";
import { deployPermissionedPool, DEFAULT_POOL_SETTINGS } from "../support/pool";
import { DEFAULT_LOAN_SETTINGS, deployPermissionedLoan } from "../support/loan";
import { deployPermissionedServiceConfiguration } from "../support/serviceconfiguration";
import { deployToSAcceptanceRegistry } from "../support/tosacceptanceregistry";
import { findEventByName, getCommonSigners } from "../support/utils";

describe("PermissionedLoanFactory", () => {
async function deployFixture() {
// Contracts are deployed using the first signer/account by default
const { operator, borrower, otherAccounts } = await getCommonSigners();
const mockPool = otherAccounts[0];
const { operator, poolAdmin, borrower, otherAccounts, deployer } =
await getCommonSigners();

// Deploy the liquidity asset
const { mockERC20: liquidityAsset } = await deployMockERC20();

// Deploy the Service Configuration contract
const { serviceConfiguration: permissionedServiceConfiguration } =
await deployPermissionedServiceConfiguration();
// Deploy PermissionedPool
const {
pool,
tosAcceptanceRegistry,
serviceConfiguration: permissionedServiceConfiguration
} = await deployPermissionedPool({
poolAdmin: poolAdmin,
settings: DEFAULT_POOL_SETTINGS,
liquidityAsset: liquidityAsset
});

await permissionedServiceConfiguration
.connect(operator)
.setLiquidityAsset(liquidityAsset.address, true);

// Deploy ToS Registry
const { tosAcceptanceRegistry } = await deployToSAcceptanceRegistry(
permissionedServiceConfiguration
);

// Configure ToS
await permissionedServiceConfiguration
.connect(operator)
.setToSAcceptanceRegistry(tosAcceptanceRegistry.address);

await tosAcceptanceRegistry
.connect(operator)
.updateTermsOfService("https://terms.example");
@@ -39,22 +45,36 @@ describe("PermissionedLoanFactory", () => {
const LoanLib = await ethers.getContractFactory("LoanLib");
const loanLib = await LoanLib.deploy();

// Deploy the PermissionedPoolFactory
const LoanFactory = await ethers.getContractFactory("LoanFactory", {
libraries: {
LoanLib: loanLib.address
}
});
// Deploy the PermissionedLoanFactory
const LoanFactory = await ethers.getContractFactory(
"PermissionedLoanFactory"
);
const loanFactory = await LoanFactory.deploy(
permissionedServiceConfiguration.address
);
await loanFactory.deployed();

// Deployer PermissionedLoan implementation
const PermissionedLoan = await ethers.getContractFactory(
"PermissionedLoan",
{
libraries: {
LoanLib: loanLib.address
}
}
);
const permissionedLoan = await PermissionedLoan.deploy();

// Set implementation on factory
await loanFactory
.connect(deployer)
.setImplementation(permissionedLoan.address);

return {
loanFactory,
operator,
borrower,
mockPool,
pool,
liquidityAsset,
tosAcceptanceRegistry,
permissionedServiceConfiguration
@@ -65,7 +85,7 @@ describe("PermissionedLoanFactory", () => {
const {
loanFactory,
borrower,
mockPool,
pool,
liquidityAsset,
tosAcceptanceRegistry
} = await loadFixture(deployFixture);
@@ -76,7 +96,7 @@ describe("PermissionedLoanFactory", () => {
.connect(borrower)
.createLoan(
borrower.address,
mockPool.address,
pool.address,
liquidityAsset.address,
DEFAULT_LOAN_SETTINGS
);
27 changes: 18 additions & 9 deletions test/support/loan.ts
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ export async function deployLoan(
serviceConfiguration: existingServiceConfiguration
});

const { operator } = await getCommonSigners();
const { operator, deployer } = await getCommonSigners();

await serviceConfiguration
.connect(operator)
@@ -49,18 +49,23 @@ export async function deployLoan(
const LoanLib = await ethers.getContractFactory("LoanLib");
const loanLib = await LoanLib.deploy();

const LoanFactory = await ethers.getContractFactory("LoanFactory", {
const LoanImpl = await ethers.getContractFactory("Loan", {
libraries: {
LoanLib: loanLib.address
}
});
const loanImpl = await LoanImpl.deploy();

const LoanFactory = await ethers.getContractFactory("LoanFactory");
const loanFactory = await LoanFactory.deploy(serviceConfiguration.address);
await loanFactory.deployed();

await serviceConfiguration
.connect(operator)
.setLoanFactory(loanFactory.address, true);

await loanFactory.connect(deployer).setImplementation(loanImpl.address);

const txn = await loanFactory.createLoan(borrower, pool, liquidityAsset, {
loanType: loanSettings.loanType,
principal: loanSettings.principal,
@@ -90,7 +95,7 @@ export async function deployPermissionedLoan(
existingServiceConfiguration: any = null,
overriddenLoanTerms?: Partial<typeof DEFAULT_LOAN_SETTINGS>
) {
const { operator } = await getCommonSigners();
const { operator, deployer } = await getCommonSigners();
const { serviceConfiguration } = await (existingServiceConfiguration == null
? deployPermissionedServiceConfiguration()
: {
@@ -109,13 +114,15 @@ export async function deployPermissionedLoan(
const LoanLib = await ethers.getContractFactory("LoanLib");
const loanLib = await LoanLib.deploy();

const PermissionedLoanFactory = await ethers.getContractFactory(
"PermissionedLoanFactory",
{
libraries: {
LoanLib: loanLib.address
}
const LoanImpl = await ethers.getContractFactory("PermissionedLoan", {
libraries: {
LoanLib: loanLib.address
}
});
const loanImpl = await LoanImpl.deploy();

const PermissionedLoanFactory = await ethers.getContractFactory(
"PermissionedLoanFactory"
);

const loanFactory = await PermissionedLoanFactory.deploy(
@@ -127,6 +134,8 @@ export async function deployPermissionedLoan(
.connect(operator)
.setLoanFactory(loanFactory.address, true);

await loanFactory.connect(deployer).setImplementation(loanImpl.address);

const txn = await loanFactory.createLoan(borrower, pool, liquidityAsset, {
loanType: loanSettings.loanType,
principal: loanSettings.principal,
24 changes: 20 additions & 4 deletions test/support/pool.ts
Original file line number Diff line number Diff line change
@@ -108,6 +108,8 @@ export async function deployPool({

return {
pool,
poolControllerFactory,
withdrawControllerFactory,
poolLib,
poolFactory,
liquidityAsset,
@@ -320,25 +322,39 @@ export async function deployWithdrawControllerFactory(
poolLibAddress: string,
serviceConfigAddress: string
) {
const Factory = await ethers.getContractFactory("WithdrawControllerFactory", {
const { deployer } = await getCommonSigners();

const Factory = await ethers.getContractFactory("WithdrawControllerFactory");
const factory = await Factory.deploy(serviceConfigAddress);

const Impl = await ethers.getContractFactory("WithdrawController", {
libraries: {
PoolLib: poolLibAddress
}
});
const factory = await Factory.deploy(serviceConfigAddress);
const impl = await Impl.deploy();
factory.connect(deployer).setImplementation(impl.address);

return factory.deployed();
}

export async function deployPoolControllerFactory(
poolLibAddress: string,
serviceConfigAddress: string
) {
const Factory = await ethers.getContractFactory("PoolControllerFactory", {
const { deployer } = await getCommonSigners();
const Factory = await ethers.getContractFactory("PoolControllerFactory");
const factory = await Factory.deploy(serviceConfigAddress);

// Attach PoolController implementation
const Impl = await ethers.getContractFactory("PoolController", {
libraries: {
PoolLib: poolLibAddress
}
});
const factory = await Factory.deploy(serviceConfigAddress);
const impl = await Impl.deploy();
factory.connect(deployer).setImplementation(impl.address);

return factory.deployed();
}