Skip to content

Commit

Permalink
feat(contracts): make opportunity adapter upgradable (#30)
Browse files Browse the repository at this point in the history
* feat(contracts): make opportunity adapter upgradable

* Improve scripts for upgaradble adapter

* Bump auction server
  • Loading branch information
m30m authored Mar 15, 2024
1 parent 3c28e6a commit 6b232f9
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 22 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci-pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
- name: Install forge dependencies 2
working-directory: per_multicall
run: forge install OpenZeppelin/openzeppelin-contracts --no-git --no-commit
- name: Install forge dependencies 3
working-directory: per_multicall
run: forge install OpenZeppelin/[email protected] --no-git --no-commit
- uses: pre-commit/[email protected]
if: ${{ github.event_name == 'pull_request' }}
with:
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ COPY --from=npm_build /src/per_multicall/node_modules/ /src/per_multicall/node_m
WORKDIR /src/per_multicall
RUN forge install foundry-rs/forge-std --no-git --no-commit
RUN forge install OpenZeppelin/openzeppelin-contracts --no-git --no-commit
RUN forge install OpenZeppelin/[email protected] --no-git --no-commit

# Build auction-server
WORKDIR /src
Expand Down
2 changes: 1 addition & 1 deletion auction-server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion auction-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "auction-server"
version = "0.2.0"
version = "0.2.1"
edition = "2021"
license-file = "license.txt"

Expand Down
1 change: 1 addition & 0 deletions per_multicall/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Run the following commands to install necessary libraries:
$ npm install
$ forge install foundry-rs/forge-std --no-git --no-commit
$ forge install OpenZeppelin/openzeppelin-contracts --no-git --no-commit
$ forge install OpenZeppelin/[email protected] --no-git --no-commit
```

## Repo contracts
Expand Down
2 changes: 1 addition & 1 deletion per_multicall/remappings.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
ds-test/=lib/forge-std/lib/ds-test/src/
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
forge-std/=lib/forge-std/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/
openzeppelin-contracts-upgradable/=lib/openzeppelin-contracts-upgradable/
@pythnetwork/pyth-sdk-solidity=node_modules/@pythnetwork/pyth-sdk-solidity/
57 changes: 46 additions & 11 deletions per_multicall/script/Vault.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,23 @@ import "@pythnetwork/pyth-sdk-solidity/MockPyth.sol";

import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import {WETH9} from "../src/WETH9.sol";

import "openzeppelin-contracts/contracts/utils/Strings.sol";

import "../src/Errors.sol";
import {OpportunityAdapterUpgradable} from "../src/OpportunityAdapterUpgradable.sol";

contract VaultScript is Script {
string public latestEnvironmentPath = "latestEnvironment.json";

function getDeployer() public view returns (address, uint256) {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
if (deployerPrivateKey == 0) {
revert("PRIVATE_KEY env variable is empty");
}
address deployerAddress = vm.addr(deployerPrivateKey);
return (deployerAddress, deployerPrivateKey);
}
Expand All @@ -39,9 +44,35 @@ contract VaultScript is Script {
return address(weth);
}

function deployExpressRelay(
function deployOpportunityAdapter(
address owner,
address admin,
address expressRelay,
address wethAddress
) public returns (address, address) {
) public returns (address) {
OpportunityAdapterUpgradable _opportunityAdapter = new OpportunityAdapterUpgradable();
// deploy proxy contract and point it to implementation
ERC1967Proxy proxy = new ERC1967Proxy(address(_opportunityAdapter), "");
// wrap in ABI to support easier calls
OpportunityAdapterUpgradable opportunityAdapter = OpportunityAdapterUpgradable(
payable(proxy)
);
opportunityAdapter.initialize(owner, admin, expressRelay, wethAddress);
return address(opportunityAdapter);
}

function upgradeOpportunityAdapter(address currentImplementation) public {
(address deployer, uint256 skDeployer) = getDeployer();
vm.startBroadcast(skDeployer);
OpportunityAdapterUpgradable _newImplementation = new OpportunityAdapterUpgradable();
OpportunityAdapterUpgradable proxy = OpportunityAdapterUpgradable(
payable(currentImplementation)
);
proxy.upgradeTo(address(_newImplementation));
vm.stopBroadcast();
}

function deployExpressRelay() public returns (address) {
(address operatorAddress, uint256 operatorSk) = makeAddrAndKey(
"perOperator"
);
Expand All @@ -50,11 +81,7 @@ contract VaultScript is Script {
payable(operatorAddress).transfer(0.01 ether);
ExpressRelay multicall = new ExpressRelay(operatorAddress, 0);
console.log("deployed ExpressRelay contract at", address(multicall));
OpportunityAdapter opportunityAdapter = new OpportunityAdapter(
address(multicall),
wethAddress
);
return (address(multicall), address(opportunityAdapter));
return address(multicall);
}

function deployVault(
Expand All @@ -79,10 +106,14 @@ contract VaultScript is Script {
public
returns (address, address, address, address, address)
{
(, uint256 skDeployer) = getDeployer();
(address deployer, uint256 skDeployer) = getDeployer();
vm.startBroadcast(skDeployer);
address weth = deployWeth();
(address expressRelay, address opportunityAdapter) = deployExpressRelay(
address expressRelay = deployExpressRelay();
address opportunityAdapter = deployOpportunityAdapter(
deployer,
deployer,
expressRelay,
weth
);
address mockPyth = deployMockPyth();
Expand All @@ -98,11 +129,15 @@ contract VaultScript is Script {
@param pyth The address of the already deployed pyth contract to use
*/
function setupTestnet(address pyth, address weth) public {
(, uint256 skDeployer) = getDeployer();
(address deployer, uint256 skDeployer) = getDeployer();
vm.startBroadcast(skDeployer);
if (pyth == address(0)) pyth = deployMockPyth();
if (weth == address(0)) weth = deployWeth();
(address expressRelay, address opportunityAdapter) = deployExpressRelay(
address expressRelay = deployExpressRelay();
address opportunityAdapter = deployOpportunityAdapter(
deployer,
deployer,
expressRelay,
weth
);
address vault = deployVault(expressRelay, pyth);
Expand Down
6 changes: 6 additions & 0 deletions per_multicall/src/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ error Unauthorized();
// Signature: 0x868a64de
error InvalidPermission();

// Signature: 0x8f4450b5
error InvalidExecutorSignature();

// Signature: 0xf136a5b7
error InvalidSearcherSignature();

Expand Down Expand Up @@ -36,3 +39,6 @@ error InsufficientTokenReceived();

// Signature: 0x4be6321b
error InvalidSignatureLength();
// The new contract does not have the same magic value as the old one.
// Signature: 0x4ed848c1
error InvalidMagicValue();
13 changes: 9 additions & 4 deletions per_multicall/src/OpportunityAdapter.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved
pragma solidity ^0.8.13;

import "./Errors.sol";
import "./Structs.sol";
import "./ExpressRelayFeeReceiver.sol";
import "./SigVerify.sol";
Expand All @@ -12,7 +11,8 @@ import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "forge-std/console.sol";
import "openzeppelin-contracts/contracts/utils/Strings.sol";

contract OpportunityAdapter is SigVerify {
abstract contract OpportunityAdapter is SigVerify {
address _admin;
address _expressRelay;
address _weth;
mapping(bytes => bool) _signatureUsed;
Expand All @@ -23,7 +23,12 @@ contract OpportunityAdapter is SigVerify {
* @param expressRelay: address of express relay
* @param weth: address of WETH contract
*/
constructor(address expressRelay, address weth) {
function _initialize(
address admin,
address expressRelay,
address weth
) internal {
_admin = admin;
_expressRelay = expressRelay;
_weth = weth;
}
Expand Down Expand Up @@ -74,7 +79,7 @@ contract OpportunityAdapter is SigVerify {
params.signature
);
if (!validSignature) {
revert InvalidSearcherSignature();
revert InvalidExecutorSignature();
}
if (block.timestamp > params.validUntil) {
revert ExpiredSignature();
Expand Down
102 changes: 102 additions & 0 deletions per_multicall/src/OpportunityAdapterUpgradable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved
pragma solidity ^0.8.13;

import "./Errors.sol";
import "./Structs.sol";
import "./ExpressRelayFeeReceiver.sol";
import "./SigVerify.sol";
import "./ExpressRelay.sol";
import "./WETH9.sol";

import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "forge-std/console.sol";
import "openzeppelin-contracts/contracts/utils/Strings.sol";
import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
import "openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol";
import {OpportunityAdapter} from "./OpportunityAdapter.sol";

contract OpportunityAdapterUpgradable is
Initializable,
Ownable2StepUpgradeable,
UUPSUpgradeable,
OpportunityAdapter
{
event ContractUpgraded(
address oldImplementation,
address newImplementation
);

// The contract will have an owner and an admin
// The owner will have all the power over it.
// The admin can set some config parameters only.
function initialize(
address owner,
address admin,
address expressRelay,
address weth
) public initializer {
require(owner != address(0), "owner is zero address");
require(admin != address(0), "admin is zero address");
require(expressRelay != address(0), "expressRelay is zero address");
require(weth != address(0), "weth is zero address");

__Ownable_init();
__UUPSUpgradeable_init();

OpportunityAdapter._initialize(admin, expressRelay, weth);

// We need to transfer the ownership from deployer to the new owner
_transferOwnership(owner);
}

/// Ensures the contract cannot be uninitialized and taken over.
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {}

// Only allow the owner to upgrade the proxy to a new implementation.
function _authorizeUpgrade(address) internal override onlyOwner {}

// We have not overridden these methods in Pyth contracts implementation.
// But we are overriding them here because there was no owner before and
// `_authorizeUpgrade` would cause a revert for these. Now we have an owner, and
// because we want to test for the magic. We are overriding these methods.
function upgradeTo(address newImplementation) external override onlyProxy {
address oldImplementation = _getImplementation();
_authorizeUpgrade(newImplementation);
_upgradeToAndCallUUPS(newImplementation, new bytes(0), false);

magicCheck();

emit ContractUpgraded(oldImplementation, _getImplementation());
}

function upgradeToAndCall(
address newImplementation,
bytes memory data
) external payable override onlyProxy {
address oldImplementation = _getImplementation();
_authorizeUpgrade(newImplementation);
_upgradeToAndCallUUPS(newImplementation, data, true);

magicCheck();

emit ContractUpgraded(oldImplementation, _getImplementation());
}

function magicCheck() internal view {
// Calling a method using `this.<method>` will cause a contract call that will use
// the new contract. This call will fail if the method does not exists or the magic
// is different.
if (this.opportunityAdapterUpgradableMagic() != 0x12d9987e)
revert InvalidMagicValue();
}

function opportunityAdapterUpgradableMagic() public pure returns (uint32) {
return 0x12d9987e;
}

function version() public pure returns (string memory) {
return "0.1.0";
}
}
16 changes: 12 additions & 4 deletions per_multicall/test/ExpressRelayIntegration.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import "@pythnetwork/pyth-sdk-solidity/MockPyth.sol";

import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import "openzeppelin-contracts/contracts/utils/Strings.sol";

import "./helpers/Signatures.sol";
import "./helpers/PriceHelpers.sol";
import "./helpers/TestParsingHelpers.sol";
import "./helpers/MulticallHelpers.sol";
import "../src/OpportunityAdapterUpgradable.sol";

/**
* @title ExpressRelayIntegrationTest
Expand All @@ -49,7 +51,7 @@ contract ExpressRelayIntegrationTest is
SearcherVault public searcherB;
ExpressRelay public expressRelay;
WETH9 public weth;
OpportunityAdapter public opportunityAdapter;
OpportunityAdapterUpgradable public opportunityAdapter;
MockPyth public mockPyth;

MyToken public token1;
Expand Down Expand Up @@ -156,7 +158,13 @@ contract ExpressRelayIntegrationTest is
weth = new WETH9();

vm.prank(perOperatorAddress, perOperatorAddress);
opportunityAdapter = new OpportunityAdapter(
OpportunityAdapterUpgradable _opportunityAdapter = new OpportunityAdapterUpgradable();
// deploy proxy contract and point it to implementation
ERC1967Proxy proxy = new ERC1967Proxy(address(_opportunityAdapter), "");
opportunityAdapter = OpportunityAdapterUpgradable(payable(proxy));
opportunityAdapter.initialize(
perOperatorAddress,
perOperatorAddress,
address(expressRelay),
address(weth)
);
Expand Down Expand Up @@ -1059,7 +1067,7 @@ contract ExpressRelayIntegrationTest is

assertFailedExternal(
multicallStatuses[0],
"InvalidSearcherSignature()"
"InvalidExecutorSignature()"
);
}

Expand Down Expand Up @@ -1167,6 +1175,6 @@ contract ExpressRelayIntegrationTest is
assertEqBalances(balancesBPost, balancesBPre);

assertEq(multicallStatuses[0].externalSuccess, true);
assertFailedExternal(multicallStatuses[1], "ExecutionFailed(string)");
assertFailedExternal(multicallStatuses[1], "TargetCallFailed(string)");
}
}
1 change: 1 addition & 0 deletions per_sdk/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ COPY --from=npm_build /src/per_multicall/node_modules/ /src/per_multicall/node_m
WORKDIR /src/per_multicall
RUN forge install foundry-rs/forge-std --no-git --no-commit
RUN forge install OpenZeppelin/openzeppelin-contracts --no-git --no-commit
RUN forge install OpenZeppelin/[email protected] --no-git --no-commit
RUN forge build --via-ir


Expand Down

0 comments on commit 6b232f9

Please sign in to comment.