Skip to content

Commit

Permalink
feat: introduce UUPSExtUpgradeable base contract (#32)
Browse files Browse the repository at this point in the history
* feat: introduce the UUPSExtUpgradeable base contract
* chore: fix comments
* chore: remove empty line
* fix: move errors, add tests
* fix: eslint
* feat: update version
  • Loading branch information
ihoroleksiienko authored Nov 18, 2024
1 parent 880dba4 commit 9b28e54
Show file tree
Hide file tree
Showing 27 changed files with 263 additions and 20 deletions.
3 changes: 3 additions & 0 deletions contracts/LendingMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -747,4 +747,7 @@ contract LendingMarket is
function _blockTimestamp() internal view virtual returns (uint256) {
return block.timestamp - Constants.NEGATIVE_TIME_OFFSET;
}

/// @inheritdoc ILendingMarket
function proveLendingMarket() external pure {}
}
16 changes: 12 additions & 4 deletions contracts/LendingMarketUUPS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@

pragma solidity 0.8.24;

import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { UUPSExtUpgradeable } from "./common/UUPSExtUpgradeable.sol";
import { LendingMarket } from "./LendingMarket.sol";
import { Error } from "./common/libraries/Error.sol";

/// @title LendingMarketUUPS contract
/// @author CloudWalk Inc. (See https://cloudwalk.io)
/// @dev Upgradeable version of the lending market contract.
contract LendingMarketUUPS is LendingMarket, UUPSUpgradeable {
contract LendingMarketUUPS is LendingMarket, UUPSExtUpgradeable {
/// @dev Constructor that prohibits the initialization of the implementation of the upgradable contract.
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address newImplementation) internal override onlyRole(OWNER_ROLE) {}
/**
* @dev The upgrade validation function for the UUPSExtUpgradeable contract.
* @param newImplementation The address of the new implementation.
*/
function _validateUpgrade(address newImplementation) internal view override onlyRole(OWNER_ROLE) {
try LendingMarketUUPS(newImplementation).proveLendingMarket() {} catch {
revert Error.ImplementationAddressInvalid();
}
}
}
48 changes: 48 additions & 0 deletions contracts/common/UUPSExtUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

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

/**
* @title UUPSExtUpgradeable base contract
* @author CloudWalk Inc. (See https://www.cloudwalk.io)
* @dev Extends the OpenZeppelin's {UUPSUpgradeable} contract by adding additional checks for
* the new implementation address.
*
* This contract is used through inheritance. It introduces the virtual `_validateUpgrade()` function that must be
* implemented in the parent contract.
*/
abstract contract UUPSExtUpgradeable is UUPSUpgradeable {
// ------------------ Errors ---------------------------------- //

/// @dev Thrown if the provided new implementation address is not a contract.
error UUPSExtUpgradeable_ImplementationAddressNotContract();

/// @dev Thrown if the provided new implementation contract address is zero.
error UUPSExtUpgradeable_ImplementationAddressZero();

// ------------------ Internal functions ---------------------- //

/**
* @dev The upgrade authorization function for UUPSProxy.
* @param newImplementation The address of the new implementation.
*/
function _authorizeUpgrade(address newImplementation) internal override {
if (newImplementation == address(0)) {
revert UUPSExtUpgradeable_ImplementationAddressZero();
}

if (newImplementation.code.length == 0) {
revert UUPSExtUpgradeable_ImplementationAddressNotContract();
}

_validateUpgrade(newImplementation);
}

/**
* @dev Executes further validation steps of the upgrade including authorization and implementation address checks.
* @param newImplementation The address of the new implementation.
*/
function _validateUpgrade(address newImplementation) internal virtual;
}
2 changes: 1 addition & 1 deletion contracts/common/Versionable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ abstract contract Versionable is IVersionable {
* @inheritdoc IVersionable
*/
function $__VERSION() external pure returns (Version memory) {
return Version(1, 0, 0);
return Version(1, 1, 0);
}
}
3 changes: 3 additions & 0 deletions contracts/common/interfaces/core/ICreditLine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ interface ICreditLine {

/// @dev Returns the address of the credit line token.
function token() external view returns (address);

/// @dev Proves the contract is the credit line one. A marker function.
function proveCreditLine() external pure;
}
3 changes: 3 additions & 0 deletions contracts/common/interfaces/core/ILendingMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -281,4 +281,7 @@ interface ILendingMarket {

/// @dev Returns the total number of loans taken.
function loanCounter() external view returns (uint256);

/// @dev Proves the contract is the lending market one. A marker function.
function proveLendingMarket() external pure;
}
3 changes: 3 additions & 0 deletions contracts/common/interfaces/core/ILiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ interface ILiquidityPool {

/// @dev Returns the address of the liquidity pool token.
function token() external view returns (address);

/// @dev Proves the contract is the liquidity pool one. A marker function.
function proveLiquidityPool() external pure;
}
3 changes: 3 additions & 0 deletions contracts/common/libraries/Error.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ library Error {

/// @dev Thrown when the called function is not implemented.
error NotImplemented();

/// @dev Thrown if the provided new implementation address is not of a contract.
error ImplementationAddressInvalid();
}
3 changes: 3 additions & 0 deletions contracts/credit-lines/CreditLineConfigurable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -569,4 +569,7 @@ contract CreditLineConfigurable is
function migrationState() external view returns (MigrationState memory) {
return _migrationState;
}

/// @inheritdoc ICreditLine
function proveCreditLine() external pure {}
}
16 changes: 12 additions & 4 deletions contracts/credit-lines/CreditLineConfigurableUUPS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@

pragma solidity 0.8.24;

import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { UUPSExtUpgradeable } from "../common/UUPSExtUpgradeable.sol";
import { CreditLineConfigurable } from "./CreditLineConfigurable.sol";
import { Error } from "../common/libraries/Error.sol";

/// @title CreditLineConfigurableUUPS contract
/// @author CloudWalk Inc. (See https://cloudwalk.io)
/// @dev Upgradeable version of the configurable credit line contract.
contract CreditLineConfigurableUUPS is CreditLineConfigurable, UUPSUpgradeable {
contract CreditLineConfigurableUUPS is CreditLineConfigurable, UUPSExtUpgradeable {
/// @dev Constructor that prohibits the initialization of the implementation of the upgradable contract.
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address newImplementation) internal override onlyRole(OWNER_ROLE) {}
/**
* @dev The upgrade validation function for the UUPSExtUpgradeable contract.
* @param newImplementation The address of the new implementation.
*/
function _validateUpgrade(address newImplementation) internal view override onlyRole(OWNER_ROLE) {
try CreditLineConfigurableUUPS(newImplementation).proveCreditLine() {} catch {
revert Error.ImplementationAddressInvalid();
}
}
}
3 changes: 3 additions & 0 deletions contracts/liquidity-pools/LiquidityPoolAccountable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,7 @@ contract LiquidityPoolAccountable is
function token() external view returns (address) {
return _token;
}

/// @inheritdoc ILiquidityPool
function proveLiquidityPool() external pure {}
}
16 changes: 12 additions & 4 deletions contracts/liquidity-pools/LiquidityPoolAccountableUUPS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@

pragma solidity 0.8.24;

import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { UUPSExtUpgradeable } from "../common/UUPSExtUpgradeable.sol";
import { LiquidityPoolAccountable } from "./LiquidityPoolAccountable.sol";
import { Error } from "../common/libraries/Error.sol";

/// @title LiquidityPoolAccountableUUPS contract
/// @author CloudWalk Inc. (See https://cloudwalk.io)
/// @dev Upgradeable version of the accountable liquidity pool contract.
contract LiquidityPoolAccountableUUPS is LiquidityPoolAccountable, UUPSUpgradeable {
contract LiquidityPoolAccountableUUPS is LiquidityPoolAccountable, UUPSExtUpgradeable {
/// @dev Constructor that prohibits the initialization of the implementation of the upgradable contract.
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address newImplementation) internal override onlyRole(OWNER_ROLE) {}
/**
* @dev The upgrade validation function for the UUPSExtUpgradeable contract.
* @param newImplementation The address of the new implementation.
*/
function _validateUpgrade(address newImplementation) internal view override onlyRole(OWNER_ROLE) {
try LiquidityPoolAccountableUUPS(newImplementation).proveLiquidityPool() {} catch {
revert Error.ImplementationAddressInvalid();
}
}
}
2 changes: 2 additions & 0 deletions contracts/mocks/CreditLineMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,6 @@ contract CreditLineMock is ICreditLine {
function mockLoanTerms(address borrower, uint256 amount, Loan.Terms memory terms) external {
_loanTerms[borrower][amount] = terms;
}

function proveCreditLine() external pure {}
}
2 changes: 2 additions & 0 deletions contracts/mocks/LendingMarketMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,6 @@ contract LendingMarketMock is ILendingMarket {
function callOnAfterLoanRevocationCreditLine(address creditLine, uint256 loanId) external {
emit HookCallResult(ICreditLine(creditLine).onAfterLoanRevocation(loanId));
}

function proveLendingMarket() external pure {}
}
2 changes: 2 additions & 0 deletions contracts/mocks/LiquidityPoolMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,6 @@ contract LiquidityPoolMock is ILiquidityPool {
function approveMarket(address _market, address token_) external {
IERC20(token_).approve(_market, type(uint56).max);
}

function proveLiquidityPool() external pure {}
}
23 changes: 23 additions & 0 deletions contracts/mocks/UUPSExtUpgradableMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import { UUPSExtUpgradeable } from "../common/UUPSExtUpgradeable.sol";

/**
* @title UUPSExtUpgradableMock contract
* @author CloudWalk Inc. (See https://www.cloudwalk.io)
* @dev An implementation of the {UUPSExtUpgradable} contract for test purposes.
*/
contract UUPSExtUpgradeableMock is UUPSExtUpgradeable {
/// @dev Emitted when the internal `_validateUpgrade()` function is called with the parameters of the function.
event MockValidateUpgradeCall(address newImplementation);

/**
* @dev Executes further validation steps of the upgrade including authorization and implementation address checks.
* @param newImplementation The address of the new implementation.
*/
function _validateUpgrade(address newImplementation) internal virtual override {
emit MockValidateUpgradeCall(newImplementation);
}
}
2 changes: 1 addition & 1 deletion test/LendingMarket.base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const ACCURACY_FACTOR = 10000;
const COOLDOWN_IN_PERIODS = 3;
const EXPECTED_VERSION: Version = {
major: 1,
minor: 0,
minor: 1,
patch: 0
};

Expand Down
1 change: 0 additions & 1 deletion test/LendingMarket.complex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,6 @@ describe("Contract 'LendingMarket': complex tests", async () => {

expect(actualPrecision).to.lessThanOrEqual(scenario.precision, errorMessageBefore);
expect(actualTrackedBalanceAfter).to.eq(expectedTrackedBalanceAfter, errorMessageAfter);

}
}

Expand Down
13 changes: 12 additions & 1 deletion test/LendingMarketUUPS.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { checkContractUupsUpgrading, connect } from "../test-utils/eth";
import { setUpFixture } from "../test-utils/common";

const ERROR_NAME_ACCESS_CONTROL_UNAUTHORIZED = "AccessControlUnauthorizedAccount";
const ERROR_NAME_IMPLEMENTATION_ADDRESS_INVALID = "ImplementationAddressInvalid";

const OWNER_ROLE = ethers.id("OWNER_ROLE");

Expand Down Expand Up @@ -53,9 +54,19 @@ describe("Contract 'LendingMarketUUPS'", async () => {
it("Is reverted if the caller is not the owner", async () => {
const { lendingMarket } = await setUpFixture(deployLendingMarket);

await expect(connect(lendingMarket, attacker).upgradeToAndCall(attacker.address, "0x"))
await expect(connect(lendingMarket, attacker).upgradeToAndCall(lendingMarket, "0x"))
.to.be.revertedWithCustomError(lendingMarket, ERROR_NAME_ACCESS_CONTROL_UNAUTHORIZED)
.withArgs(attacker.address, OWNER_ROLE);
});

it("Is reverted if the provided implementation address is not a lending market contract", async () => {
const { lendingMarket } = await setUpFixture(deployLendingMarket);
const mockContractFactory = await ethers.getContractFactory("UUPSExtUpgradeableMock");
const mockContract = await mockContractFactory.deploy() as Contract;
await mockContract.waitForDeployment();

await expect(lendingMarket.upgradeToAndCall(mockContract, "0x"))
.to.be.revertedWithCustomError(lendingMarket, ERROR_NAME_IMPLEMENTATION_ADDRESS_INVALID);
});
});
});
70 changes: 70 additions & 0 deletions test/common/UUPSExtUbgradable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ethers, network, upgrades } from "hardhat";
import { expect } from "chai";
import { Contract, ContractFactory } from "ethers";
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { connect } from "../../test-utils/eth";

const ADDRESS_ZERO = ethers.ZeroAddress;

async function setUpFixture<T>(func: () => Promise<T>): Promise<T> {
if (network.name === "hardhat") {
return loadFixture(func);
} else {
return func();
}
}

describe("Contracts 'UUPSExtUpgradeable'", async () => {
// Errors of the lib contracts
const REVERT_ERROR_IMPLEMENTATION_ADDRESS_NOT_CONTRACT = "UUPSExtUpgradeable_ImplementationAddressNotContract";
const REVERT_ERROR_IMPLEMENTATION_ADDRESS_ZERO = "UUPSExtUpgradeable_ImplementationAddressZero";

// Events of the contracts under test
const EVENT_NAME_MOCK_VALIDATE_UPGRADE_CALL = "MockValidateUpgradeCall";

let uupsExtensionFactory: ContractFactory;
let deployer: HardhatEthersSigner;

before(async () => {
[deployer] = await ethers.getSigners();

// The contract factory with the explicitly specified deployer account
uupsExtensionFactory = await ethers.getContractFactory("UUPSExtUpgradeableMock");
uupsExtensionFactory = uupsExtensionFactory.connect(deployer);
});

async function deployContract(): Promise<{ uupsExtension: Contract }> {
let uupsExtension: Contract = await upgrades.deployProxy(uupsExtensionFactory, [], { initializer: false });
await uupsExtension.waitForDeployment();
uupsExtension = connect(uupsExtension, deployer); // Explicitly specifying the initial account

return { uupsExtension };
}

describe("Function 'upgradeToAndCall()'", async () => {
it("Executes as expected", async () => {
const { uupsExtension } = await setUpFixture(deployContract);

const newImplementation = await uupsExtensionFactory.deploy();
await newImplementation.waitForDeployment();
const newImplementationAddress = await newImplementation.getAddress();

await expect(uupsExtension.upgradeToAndCall(newImplementationAddress, "0x"))
.to.emit(uupsExtension, EVENT_NAME_MOCK_VALIDATE_UPGRADE_CALL)
.withArgs(newImplementationAddress);
});

it("Is reverted if the new implementation address is zero", async () => {
const { uupsExtension } = await setUpFixture(deployContract);
await expect(uupsExtension.upgradeToAndCall(ADDRESS_ZERO, "0x"))
.to.be.revertedWithCustomError(uupsExtension, REVERT_ERROR_IMPLEMENTATION_ADDRESS_ZERO);
});

it("Is reverted if the new implementation address is not a contract", async () => {
const { uupsExtension } = await setUpFixture(deployContract);
await expect(uupsExtension.upgradeToAndCall(deployer.address, "0x"))
.to.be.revertedWithCustomError(uupsExtension, REVERT_ERROR_IMPLEMENTATION_ADDRESS_NOT_CONTRACT);
});
});
});
2 changes: 1 addition & 1 deletion test/credit-lines/CreditLineConfigurable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const DEFAULT_ADDON_AMOUNT = 321;
const DEFAULT_REPAY_AMOUNT = 322;
const EXPECTED_VERSION: Version = {
major: 1,
minor: 0,
minor: 1,
patch: 0
};

Expand Down
Loading

0 comments on commit 9b28e54

Please # to comment.