Skip to content

Commit

Permalink
feat: Make Ojo Contract EIP-1967 proxy upgradable (#17)
Browse files Browse the repository at this point in the history
* feat: Make Ojo Contract EIP-1967 proxy upgradable

* comment

* implement upgradable util methods

* fix tests

* lint

* update deployMockOjo to use CREATE2

* Update contracts/Ojo.sol

Co-authored-by: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com>

* Update contracts/OjoProxy.sol

Co-authored-by: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com>

* key in deploy fix

---------

Co-authored-by: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com>
  • Loading branch information
rbajollari and adamewozniak authored Jan 28, 2024
1 parent 430663e commit accd1eb
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 229 deletions.
2 changes: 1 addition & 1 deletion contracts/MockOjo.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract MockOjo {
uint256 amount;
}

Balance[] public balances ;
Balance[] public balances;

constructor(address ojo_) {
ojo = IOjo(ojo_);
Expand Down
35 changes: 22 additions & 13 deletions contracts/Ojo.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecu
import "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";
import "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol";
import "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/Upgradable.sol";
import "./IOjo.sol";
import "./OjoTypes.sol";

contract Ojo is IOjo, AxelarExecutable, Ownable {
contract Ojo is IOjo, AxelarExecutable, Upgradable {
IAxelarGasService public immutable gasReceiver;

string public ojoChain;
Expand All @@ -20,18 +20,10 @@ contract Ojo is IOjo, AxelarExecutable, Ownable {

mapping(bytes32 => OjoTypes.PriceData) public priceData;

constructor(
address gateway_,
address gasReceiver_,
string memory ojoChain_,
string memory ojoAddress_,
address owner,
uint256 resolveWindow_
) AxelarExecutable(gateway_) Ownable(owner) {
error AlreadyInitialized();

constructor(address gateway_, address gasReceiver_) AxelarExecutable(gateway_) {
gasReceiver = IAxelarGasService(gasReceiver_);
ojoChain = ojoChain_;
ojoAddress = ojoAddress_;
resolveWindow = resolveWindow_;
}

function callContractMethodWithOjoPriceData(
Expand Down Expand Up @@ -86,6 +78,19 @@ contract Ojo is IOjo, AxelarExecutable, Ownable {
gateway.callContractWithToken(ojoChain, ojoAddress, payloadWithVersion, symbol, amount);
}

function _setup(bytes calldata data) internal override {
(string memory ojoChain_, string memory ojoAddress_, uint256 resolveWindow_) = abi.decode(
data,
(string, string, uint256)
);
if (bytes(ojoChain).length != 0) revert AlreadyInitialized();
if (bytes(ojoAddress).length != 0) revert AlreadyInitialized();
if (resolveWindow != 0) revert AlreadyInitialized();
ojoChain = ojoChain_;
ojoAddress = ojoAddress_;
resolveWindow = resolveWindow_;
}

function _execute(
string calldata,
string calldata,
Expand Down Expand Up @@ -193,4 +198,8 @@ contract Ojo is IOjo, AxelarExecutable, Ownable {
function updateResolveWindow(uint256 resolveWindow_) external onlyOwner {
resolveWindow = resolveWindow_;
}

function contractId() external pure returns (bytes32) {
return keccak256('ojo-v1');
}
}
15 changes: 15 additions & 0 deletions contracts/OjoProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import '@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/InitProxy.sol';

contract OjoProxy is InitProxy {
function contractId()
internal
pure
override
returns (bytes32)
{
return keccak256('ojo-v1');
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
"author": "rbajollari <rbajollari@gmail.com>",
"license": "MIT",
"dependencies": {
"@axelar-network/axelar-gmp-sdk-solidity": "^5.6.2",
"@axelar-network/axelar-gmp-sdk-solidity": "^5.6.4",
"@openzeppelin/contracts": "^5.0.0",
"@openzeppelin/contracts-upgradeable": "^5.0.1",
"dotenv": "^16.3.1",
"hardhat": "^2.19.0",
"hardhat-abi-exporter": "^2.10.1",
Expand Down
13 changes: 6 additions & 7 deletions scripts/deployMockOjo.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Wallet, AbiCoder, ethers } from "ethers";
import { Wallet, ethers } from "ethers";
import MockOjo from '../artifacts/contracts/MockOjo.sol/MockOjo.json';
import Create3Deployer from '@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/deploy/Create3Deployer.sol/Create3Deployer.json';
import Create2Deployer from '@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/deploy/Create2Deployer.sol/Create2Deployer.json';
import testnet_chains from '../testnet_chains.json';

async function main() {
const ojoContractddress = "0x1cAe21620070E54C17279faDb2Bd6eBd5654459a";
const create3DeployerAddress = "0x6513Aedb4D1593BA12e50644401D976aebDc90d8";
const ojoContractddress = "0x885C97650b85865A7b162179876585d1A8573D3E";
const create2DeployerAddress = "0x98b2920d53612483f91f12ed7754e51b4a77919e";

const privateKey = process.env.PRIVATE_KEY;

Expand All @@ -21,14 +21,13 @@ async function main() {
const balance = await provider.getBalance(wallet.address)
console.log(`${chain.name} wallet balance: ${ethers.formatEther(balance.toString())} ${chain.tokenSymbol}`);

const deployerContract = new ethers.Contract(create3DeployerAddress, Create3Deployer.abi, wallet);
const deployerContract = new ethers.Contract(create2DeployerAddress, Create2Deployer.abi, wallet);

const salt = ethers.zeroPadValue(ethers.toUtf8Bytes("MockOjo"), 32);

const coder = AbiCoder.defaultAbiCoder()
const creationCode = ethers.solidityPacked(
["bytes", "bytes"],
[MockOjo.bytecode, coder.encode(["address"], [ojoContractddress])]
[MockOjo.bytecode, ethers.AbiCoder.defaultAbiCoder().encode(["address"], [ojoContractddress])]
);

// perform static call to log address of the contract
Expand Down
51 changes: 24 additions & 27 deletions scripts/deployOjo.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,46 @@
import { Wallet, AbiCoder, ethers } from "ethers";
import { Wallet, ethers } from "ethers";
import Ojo from '../artifacts/contracts/Ojo.sol/Ojo.json';
import Create3Deployer from '@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/deploy/Create3Deployer.sol/Create3Deployer.json';
import OjoProxy from '../artifacts/contracts/OjoProxy.sol/OjoProxy.json';
import testnet_chains from '../testnet_chains.json';
import { deployCreate2InitUpgradable } from './utils/upgradable';

async function main() {
const axelarGasReceiverAddress = "0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6";
const create3DeployerAddress = "0x6513Aedb4D1593BA12e50644401D976aebDc90d8";
const create2DeployerAddress = "0x98b2920d53612483f91f12ed7754e51b4a77919e";
const ojoChain = "ojo";
const ojoAddress = "ojo1es9mhmnunh208ucwq8rlrl97hqulxrz8k37dcu";
const resolveWindow = 7200

const privateKey = process.env.PRIVATE_KEY;

if (!privateKey) {
throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.');
throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.');
}

const evmChains = testnet_chains.map((chain) => ({ ...chain }));
const key = Date.now();

for (const chain of evmChains) {
const provider = new ethers.JsonRpcProvider(chain.rpc)
const wallet = new Wallet(privateKey, provider);
const balance = await provider.getBalance(wallet.address)
console.log(`${chain.name} wallet balance: ${ethers.formatEther(balance.toString())} ${chain.tokenSymbol}`);

const deployerContract = new ethers.Contract(create3DeployerAddress, Create3Deployer.abi, wallet);

const salt = ethers.zeroPadValue(ethers.toUtf8Bytes("Ojo"), 32);

const coder = AbiCoder.defaultAbiCoder()
const creationCode = ethers.solidityPacked(
["bytes", "bytes"],
[Ojo.bytecode, coder.encode(["address", "address", "string", "string", "address", "uint256"],[chain.gateway, axelarGasReceiverAddress, ojoChain, ojoAddress, wallet.address, resolveWindow])]
);

// perform static call to log address of the contract
const deployedAddress = await deployerContract.deploy.staticCallResult(creationCode, salt);
console.log(`${chain.name}, address: ${deployedAddress}`);

// perform actual deploy tx
await deployerContract.deploy(creationCode, salt);
const provider = new ethers.JsonRpcProvider(chain.rpc)
const wallet = new Wallet(privateKey, provider);
const balance = await provider.getBalance(wallet.address)
console.log(`${chain.name} wallet balance: ${ethers.formatEther(balance.toString())} ${chain.tokenSymbol}`);

const deployedContract = await deployCreate2InitUpgradable(
create2DeployerAddress,
wallet,
Ojo,
OjoProxy,
[chain.gateway, axelarGasReceiverAddress],
ethers.AbiCoder.defaultAbiCoder().encode(["string", "string", "uint256"],[ojoChain, ojoAddress, resolveWindow]),
key
);

console.log(`${chain.name}, address: ${await deployedContract.getAddress()}`);
}
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
console.error(error);
process.exitCode = 1;
});
38 changes: 38 additions & 0 deletions scripts/upgradeOjo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Wallet, ethers } from "ethers";
import Ojo from '../artifacts/contracts/Ojo.sol/Ojo.json';
import testnet_chains from '../testnet_chains.json';
import { upgradeUpgradable } from './utils/upgradable';

async function main() {
const ojoProxyAddress = "0x4C49Bca23BB402e4938B59Af14f17FA8178c1BA3";
const axelarGasReceiverAddress = "0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6";

const privateKey = process.env.PRIVATE_KEY;

if (!privateKey) {
throw new Error('Invalid private key. Make sure the PRIVATE_KEY environment variable is set.');
}

const evmChains = testnet_chains.map((chain) => ({ ...chain }));

for (const chain of evmChains) {
const provider = new ethers.JsonRpcProvider(chain.rpc)
const wallet = new Wallet(privateKey, provider);
const balance = await provider.getBalance(wallet.address)
console.log(`${chain.name} wallet balance: ${ethers.formatEther(balance.toString())} ${chain.tokenSymbol}`);

const upgradeTx = await upgradeUpgradable(
ojoProxyAddress,
wallet,
Ojo,
[chain.gateway, axelarGasReceiverAddress]
);

console.log(`${chain.name}, upgrade tx: ${upgradeTx.hash}`);
}
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
71 changes: 71 additions & 0 deletions scripts/utils/upgradable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { AbiCoder, ethers } from "ethers";
import Create2Deployer from '@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/deploy/Create2Deployer.sol/Create2Deployer.json';
import IUpgradable from '@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/interfaces/IUpgradable.sol/IUpgradable.json';

export async function deployCreate2InitUpgradable(
create2DeployerAddress: any,
wallet: ethers.Wallet,
implementationJson: { abi: ethers.Interface | ethers.InterfaceAbi; bytecode: ethers.BytesLike | { object: string; }; },
proxyJson: { abi: ethers.Interface | ethers.InterfaceAbi; bytecode: ethers.BytesLike | { object: string; }; },
implementationConstructorArgs: any,
setupParams = '0x',
key = Date.now(),
) {
const implementationFactory = new ethers.ContractFactory(implementationJson.abi, implementationJson.bytecode, wallet);

const implementation = await implementationFactory.deploy(...implementationConstructorArgs);
await implementation.waitForDeployment();

const proxy = await create2DeployAndInitContract(
create2DeployerAddress,
wallet,
proxyJson,
key,
[await implementation.getAddress(), wallet.address, setupParams]
);

return new ethers.Contract(await proxy.getAddress(), implementationJson.abi, wallet);
}

export async function upgradeUpgradable(
proxyAddress: any,
wallet: ethers.Wallet,
contractJson: { bytecode: any | ethers.Overrides; abi: ethers.Interface | ethers.InterfaceAbi; },
implementationConstructorArgs: any,
setupParams = '0x'
) {
const proxy = new ethers.Contract(proxyAddress, IUpgradable.abi, wallet);

const implementationFactory = new ethers.ContractFactory(contractJson.abi, contractJson.bytecode, wallet);

const implementation = await implementationFactory.deploy(...implementationConstructorArgs);
await implementation.waitForDeployment();

const implementationCode = await wallet.provider!.getCode(await implementation.getAddress());
const implementationCodeHash = ethers.keccak256(implementationCode);

const tx = await proxy.upgrade(await implementation.getAddress(), implementationCodeHash, setupParams);
await tx.wait();

return tx;
}

async function create2DeployAndInitContract (
deployerAddress: any,
wallet: ethers.Wallet,
contractJson: { bytecode: any | ethers.Overrides; abi: ethers.Interface | ethers.InterfaceAbi; },
key: number,
initArgs: any,
confirmations = null,
) {
const deployer = new ethers.Contract(deployerAddress, Create2Deployer.abi, wallet);
const salt = ethers.keccak256(AbiCoder.defaultAbiCoder().encode(['string'], [key.toString()]));
const address = await deployer.deployedAddress(contractJson.bytecode, wallet.address, salt);
const contract = new ethers.Contract(address, contractJson.abi, wallet);
const initData = (await contract.init.populateTransaction(...initArgs)).data;

const tx = await deployer.deployAndInit(contractJson.bytecode, salt, initData);
await tx.wait(confirmations);

return contract;
};
32 changes: 20 additions & 12 deletions test/MockOjo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,37 @@ import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";


describe("Deploy", function () {
async function deployMockOjoContract() {
// Contracts are deployed using the first signer/account by default
const [deployer, axelarGateway, axelarGasReceiver] = await ethers.getSigners();
// Contracts are deployed using the first signer/account by default
const [deployer, axelarGateway, axelarGasReceiver] = await ethers.getSigners();

const Ojo = await ethers.getContractFactory("Ojo");
const ojoImplementation = await Ojo.deploy(axelarGateway.address, axelarGasReceiver.address);

const ojoChain = "ojoChain";
const ojoAddress = "ojoAddress";
const resolveWindow = 100;
const initParams = ethers.AbiCoder.defaultAbiCoder().encode(["string", "string", "uint256"],[ojoChain, ojoAddress, resolveWindow]);

const OjoProxy = await ethers.getContractFactory("OjoProxy");
const ojoProxy = await OjoProxy.deploy();

const ojoChain = "ojoChain";
const ojoAddress = "ojoAddress";
const resolveWindow = 100;
ojoProxy.init(await ojoImplementation.getAddress(), await deployer.getAddress(), initParams);

const Ojo = await ethers.getContractFactory("Ojo");
const ojo = await Ojo.deploy(axelarGateway.address, axelarGasReceiver.address, ojoChain, ojoAddress, deployer, resolveWindow);
const ojoContract = ojoImplementation.attach(await ojoProxy.getAddress());
const ojo = await ethers.getContractAt("Ojo", await ojoContract.getAddress())

const MockOjo = await ethers.getContractFactory("MockOjo");
const mockOjo = await MockOjo.deploy(await ojo.getAddress())
const MockOjo = await ethers.getContractFactory("MockOjo");
const mockOjo = await MockOjo.deploy(await ojo.getAddress());

return {deployer, mockOjo, ojo}
return {deployer, mockOjo, ojo}
}


it("sets address of ojo contract correctly", async function(){
const { mockOjo, ojo} = await loadFixture(deployMockOjoContract);

expect(await mockOjo.ojo()).eq(await ojo.getAddress())
expect(await mockOjo.ojo()).eq(await ojo.getAddress());
})
})
Loading

0 comments on commit accd1eb

Please sign in to comment.