diff --git a/contracts/Oracle.sol b/contracts/Oracle.sol index 37d1849..a778fca 100644 --- a/contracts/Oracle.sol +++ b/contracts/Oracle.sol @@ -3,10 +3,9 @@ pragma solidity 0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; import "./interfaces/BakiOracleInterface.sol"; -contract BakiOracle is AccessControl, BakiOracleInterface, Ownable { +contract BakiOracle is AccessControl, BakiOracleInterface { bytes32 public constant DATA_FEED = keccak256("DATA_FEED"); @@ -15,8 +14,10 @@ contract BakiOracle is AccessControl, BakiOracleInterface, Ownable { string[] public zTokenList; uint256 public collateralUSD; + address public authorizedSigner; + uint256 private nonce; - constructor (address admin, address _datafeed, address _zusd, address _zngn, address _zzar, address _zxaf) Ownable(admin) { + constructor (address admin, address _datafeed, address _zusd, address _zngn, address _zzar, address _zxaf) { string[4] memory default_currencies = ["zusd", "zngn", "zzar", "zxaf"]; _grantRole(DATA_FEED, _datafeed); _grantRole(DEFAULT_ADMIN_ROLE, admin); @@ -34,6 +35,18 @@ contract BakiOracle is AccessControl, BakiOracleInterface, Ownable { event SetZTokenUSDValue(string indexed _name, uint256 _value); event SetZCollateralUSD(uint256 _value); + function updateAuthorizedSigner(address _newSigner) external onlyRole(DEFAULT_ADMIN_ROLE) { + authorizedSigner = _newSigner; + } + + function getAuthorizedSigner() external view returns(address){ + return authorizedSigner; + } + + function getNonce() external view returns(uint256){ + return nonce; + } + function addZToken(string calldata _name, address _address) external onlyRole(DEFAULT_ADMIN_ROLE) { require(_address != address(0), "Address is invalid"); @@ -91,16 +104,20 @@ contract BakiOracle is AccessControl, BakiOracleInterface, Ownable { return false; } - function setZTokenUSDValue( string calldata _name, - uint256 _value + uint256 _value, + uint256 _nonce, + bytes memory _signature ) external onlyRole(DATA_FEED) { address zToken = getZToken(_name); require(_value >= 1, "Invalid value"); + require(recoverSigner(_value, _nonce, _signature) == authorizedSigner, "Invalid or unauthorized signature"); + require(_nonce == nonce + 1,"invalid nonce"); zTokenUSDValue[zToken] = _value; + nonce = _nonce; emit SetZTokenUSDValue(_name, _value); } @@ -113,14 +130,57 @@ contract BakiOracle is AccessControl, BakiOracleInterface, Ownable { return zTokenUSDValue[zToken]; } - function setZCollateralUSD(uint256 _value) external onlyRole(DATA_FEED){ + function setZCollateralUSD(uint256 _value, uint256 _nonce, bytes memory _signature) external onlyRole(DATA_FEED){ + + require(recoverSigner(_value, _nonce, _signature) == authorizedSigner, "Invalid or unauthorized signature"); + require(_nonce == nonce + 1,"invalid nonce"); if (_value > 1000) { collateralUSD = 1000; } else { collateralUSD = _value; } + nonce = _nonce; emit SetZCollateralUSD(_value); } + + function recoverSigner(uint256 _value, uint256 _nonce, bytes memory _signature) internal pure returns (address) { + bytes32 dataHash = keccak256(abi.encodePacked(_value, _nonce)); + bytes32 messageHash = prefixed(dataHash); + + // Split the signature into r, s, and v variables + (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); + + // Perform the ecrecover operation to determine the signer + return ecrecover(messageHash, v, r, s); + } + + // Internal function to split a signature into r, s, and v + function splitSignature(bytes memory sig) + internal + pure + returns (bytes32 r, bytes32 s, uint8 v) + { + require(sig.length == 65, "Invalid signature length"); + + assembly { + // First 32 bytes, after the length prefix + r := mload(add(sig, 32)) + // Second 32 bytes + s := mload(add(sig, 64)) + // Final byte (first byte of the next 32 bytes) + v := byte(0, mload(add(sig, 96))) + } + + if (v < 27) { + v += 27; + } + } + + + function prefixed(bytes32 hash) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } } + diff --git a/contracts/Test.sol b/contracts/Test.sol new file mode 100644 index 0000000..13785f3 --- /dev/null +++ b/contracts/Test.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract PriceOracle { + uint256 public currentPrice; + address private owner; + address public authorizedSigner; + + event PriceUpdated(uint256 newPrice); + + constructor(address _authorizedSigner) { + owner = msg.sender; + authorizedSigner = _authorizedSigner; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Only owner can call this."); + _; + } + + + function updateAuthorizedSigner(address _newSigner) public onlyOwner { + authorizedSigner = _newSigner; + } + + + function updatePrice(uint256 _newPrice, uint256 _nonce, bytes memory _signature) public { + require(recoverSigner(_newPrice, _nonce, _signature) == authorizedSigner, "Invalid or unauthorized signature"); + currentPrice = _newPrice; + emit PriceUpdated(_newPrice); + } + + + function recoverSigner(uint256 _newPrice, uint256 _nonce, bytes memory _signature) internal pure returns (address) { + bytes32 dataHash = keccak256(abi.encodePacked(_newPrice, _nonce)); + bytes32 messageHash = prefixed(dataHash); + + // Split the signature into r, s, and v variables + (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); + + // Perform the ecrecover operation to determine the signer + return ecrecover(messageHash, v, r, s); + } + + // Internal function to split a signature into r, s, and v + function splitSignature(bytes memory sig) + internal + pure + returns (bytes32 r, bytes32 s, uint8 v) + { + require(sig.length == 65, "Invalid signature length"); + + assembly { + // First 32 bytes, after the length prefix + r := mload(add(sig, 32)) + // Second 32 bytes + s := mload(add(sig, 64)) + // Final byte (first byte of the next 32 bytes) + v := byte(0, mload(add(sig, 96))) + } + + if (v < 27) { + v += 27; + } + } + + + function prefixed(bytes32 hash) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } +} \ No newline at end of file diff --git a/contracts/abis/Oracle.json b/contracts/abis/Oracle.json index 6c26a44..ac68760 100644 --- a/contracts/abis/Oracle.json +++ b/contracts/abis/Oracle.json @@ -56,28 +56,6 @@ "name": "AccessControlUnauthorizedAccount", "type": "error" }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - } - ], - "name": "OwnableInvalidOwner", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "OwnableUnauthorizedAccount", - "type": "error" - }, { "anonymous": false, "inputs": [ @@ -97,25 +75,6 @@ "name": "AddZToken", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -280,6 +239,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "authorizedSigner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -312,6 +284,32 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "getAuthorizedSigner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNonce", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -426,12 +424,12 @@ }, { "inputs": [], - "name": "owner", + "name": "nonce", "outputs": [ { - "internalType": "address", + "internalType": "uint256", "name": "", - "type": "address" + "type": "uint256" } ], "stateMutability": "view", @@ -450,13 +448,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -499,6 +490,16 @@ "internalType": "uint256", "name": "_value", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_nonce", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" } ], "name": "setZCollateralUSD", @@ -517,6 +518,16 @@ "internalType": "uint256", "name": "_value", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_nonce", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" } ], "name": "setZTokenUSDValue", @@ -547,11 +558,11 @@ "inputs": [ { "internalType": "address", - "name": "newOwner", + "name": "_newSigner", "type": "address" } ], - "name": "transferOwnership", + "name": "updateAuthorizedSigner", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -575,4 +586,4 @@ "stateMutability": "view", "type": "function" } -] +] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 79ba71c..ff0c33b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "@openzeppelin/hardhat-upgrades": "3.0.5", "dotenv": "^16.4.5", "ethers": "6.11.1", - "hardhat": "2.22.2" + "hardhat": "2.22.2", + "mocha": "^10.4.0" } }, "node_modules/@adraffy/ens-normalize": { diff --git a/package.json b/package.json index 2378352..95b9473 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "transfer-ownership": "hardhat run --network fuji scripts/transfer-ownership.js", "propose-upgrade": "hardhat run --network fuji scripts/propose_upgrade.js", "propose-upgrade-mainnet": "hardhat run --network mainnet scripts/propose_upgrade.js", - "bootstrap-deployment": "node scripts/aws_store.js" + "bootstrap-deployment": "node scripts/aws_store.js", + "bootstrap-testnet": "node scripts/aws_store_testnet.js", + "test": "node ./node_modules/mocha/bin/mocha" }, "dependencies": { "@aws-sdk/client-secrets-manager": "^3.556.0", @@ -25,6 +27,7 @@ "@openzeppelin/hardhat-upgrades": "3.0.5", "dotenv": "^16.4.5", "ethers": "6.11.1", - "hardhat": "2.22.2" + "hardhat": "2.22.2", + "mocha": "^10.4.0" } } diff --git a/scripts/aws_store_testnet.js b/scripts/aws_store_testnet.js new file mode 100644 index 0000000..218b189 --- /dev/null +++ b/scripts/aws_store_testnet.js @@ -0,0 +1,36 @@ +const { + SecretsManagerClient, + GetSecretValueCommand, + } = require("@aws-sdk/client-secrets-manager"); + const fs = require('fs'); + + async function getStore() { + + const secret_name = "TESTNET"; + + const client = new SecretsManagerClient({ + region: "us-east-1", + }); + + let response; + + try { + response = await client.send( + new GetSecretValueCommand({ + SecretId: secret_name, + VersionStage: "AWSCURRENT", + }) + ); + } catch (error) { + throw error; + } + + const secret = response.SecretString; + const keystore = JSON.parse(secret); + + fs.writeFileSync('keystore.json', JSON.stringify(keystore, null, 2)); + + return keystore; + } + + getStore() \ No newline at end of file diff --git a/scripts/deploy_vault_v1.js b/scripts/deploy_vault_v1.js index 6735156..e597152 100644 --- a/scripts/deploy_vault_v1.js +++ b/scripts/deploy_vault_v1.js @@ -12,7 +12,7 @@ let USDCAbi = require('../contracts/abis/USDC.json'); const Datafeed = MAINNET_DATAFEED; -const MULTISIG = MAINNET_MULTISIG; +const MULTISIG = "0x2fb53058044b43E34016f5e7934B6E9B06a40294"; let tokens = { zUSD: null, @@ -71,7 +71,7 @@ async function deployCollateral() { async function setVaultAddress(_name, _vaultAddress) { const zToken = await hre.ethers.getContractAt("ZToken", tokens[_name]); - const txn = await zToken.addVaultAddress(_vaultAddress); + const txn = await zToken.addVault(_vaultAddress); console.log(`Set Vault address on ${_name}:`, txn.hash); } @@ -124,18 +124,18 @@ async function main() { await vault.waitForDeployment(); console.log("Vault deployed to:", vault.target); - console.log('Transferring ownership of ProxyAdmin...'); + // console.log('Transferring ownership of ProxyAdmin...'); - await upgrades.admin.transferProxyAdminOwnership(vault.target, MULTISIG); - console.log('Transferred ownership of ProxyAdmin to:', MULTISIG); + // await upgrades.admin.transferProxyAdminOwnership(vault.target, MULTISIG); + // console.log('Transferred ownership of ProxyAdmin to:', MULTISIG); - // addContract(zUSD, 'zUSD', JSON.stringify(ZTokenAbi)); - // addContract(zNGN, 'zNGN', JSON.stringify(ZTokenAbi)); - // addContract(zZAR, 'zZAR', JSON.stringify(ZTokenAbi)); - // addContract(zXAF, 'zXAF', JSON.stringify(ZTokenAbi)); - // addContract(Oracle, 'Oracle', JSON.stringify(OracleAbi)); - // addContract(vault.target, 'Vault', JSON.stringify(VaultAbi)); - // addContract(collateral, 'USDC', JSON.stringify(USDCAbi)); + addContract(zUSD, 'zUSD-02', JSON.stringify(ZTokenAbi)); + addContract(zNGN, 'zNGN-02', JSON.stringify(ZTokenAbi)); + addContract(zZAR, 'zZAR-02', JSON.stringify(ZTokenAbi)); + addContract(zXAF, 'zXAF-02', JSON.stringify(ZTokenAbi)); + addContract(Oracle, 'Oracle-02', JSON.stringify(OracleAbi)); + addContract(vault.target, 'Vault-02', JSON.stringify(VaultAbi)); + addContract(collateral, 'USDC-02', JSON.stringify(USDCAbi)); const filePath = 'keystore.json'; @@ -146,5 +146,10 @@ async function main() { } console.log('File deleted successfully'); }); + + await setVaultAddress("zUSD", vault.target); + await setVaultAddress("zNGN", vault.target); + await setVaultAddress("zXAF", vault.target); + await setVaultAddress("zZAR", vault.target); } main() diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..eca16fa --- /dev/null +++ b/test/test.js @@ -0,0 +1,53 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); + +describe("PriceOracle", function () { + let PriceOracle; + let priceOracle; + let owner; + let addr1; + let addr2; + let addr3; + let addr4; + let addr5; + let addr6; + + beforeEach(async function () { + PriceOracle = await ethers.getContractFactory("BakiOracle"); + [owner, addr1, addr2, addr3, addr4, addr5, addr6] = await ethers.getSigners(); + priceOracle = await PriceOracle.deploy(addr1.address, addr2.address, addr3.address, addr4.address, addr5.address, addr6.address); + }); + + describe("Deployment", function () { + it("Should set the right owner", async function () { + await priceOracle.updateAuthorizedSigner(addr1.address); + expect(await priceOracle.authorizedSigner()).to.equal(addr1.address); + }); + }); + + describe("Transactions", function () { + it("Should update the price with the correct signature", async function () { + const newPrice = 200; + // const nonce = 1; + const messageHash = ethers.solidityPackedKeccak256(['uint256'], [newPrice]); + // const messageHashBytes = ethers.arrayify(messageHash); + const signature = await addr1.signMessage(messageHash); + console.log(signature) + await expect(priceOracle.setZCollateralUSD(newPrice, nonce, signature)) + .to.emit(priceOracle, 'SetZCollateralUSD') // Assuming there is an event emitted on price update + .withArgs(newPrice); + }); + + it("Should fail to update the price with an incorrect signature", async function () { + const newPrice = 300; + const nonce = 2; + const messageHash = ethers.solidityPackedKeccak256(['uint256', 'uint256'], [newPrice, nonce]); + // const messageHashBytes = ethers.utils.arrayify(messageHash); + const signature = await addr1.signMessage(messageHash); + + // Attempt to update with the wrong signer + await expect(priceOracle.connect(addr5).setZCollateralUSD(newPrice, nonce, signature)) + .to.be.revertedWith("Invalid or unauthorized signature"); + }); + }); +}); \ No newline at end of file