Skip to content

Commit

Permalink
Merge pull request #61 from orochi-network/feature/redeem_beta_token_…
Browse files Browse the repository at this point in the history
…with_signature

Feature: Redeem beta token with signature
  • Loading branch information
dqtkien authored Sep 3, 2024
2 parents 25a061d + f02052c commit 1fd30ef
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 36 deletions.
38 changes: 35 additions & 3 deletions contracts/token/XORO.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ pragma solidity 0.8.19;

import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/security/ReentrancyGuard.sol';
import '../libraries/Operatable.sol';
import './XOROECDSA.sol';

contract XORO is ERC20, Ownable, Operatable {
contract XORO is ERC20, Ownable, Operatable, XOROECDSA, ReentrancyGuard {
// Error: Access denied
error AccessDenied();

Expand Down Expand Up @@ -44,7 +46,7 @@ contract XORO is ERC20, Ownable, Operatable {
//====================[ Operator ]====================

// Mint token in packed data
function batchMint(uint256[] calldata packedData) external onlyOperator returns(uint256) {
function batchMint(uint256[] calldata packedData) external onlyOperator returns (uint256) {
for (uint i = 0; i < packedData.length; i += 1) {
(uint96 amount, address to) = _unpack(packedData[i]);
_mint(to, amount);
Expand All @@ -53,14 +55,44 @@ contract XORO is ERC20, Ownable, Operatable {
}

// Burn token in packed data
function batchBurn(uint256[] calldata packedData) external onlyOperator returns(uint256) {
function batchBurn(uint256[] calldata packedData) external onlyOperator returns (uint256) {
for (uint i = 0; i < packedData.length; i += 1) {
(uint96 amount, address from) = _unpack(packedData[i]);
_burn(from, amount);
}
return packedData.length;
}

//====================[ User ]====================

// Every one can claim token with a valid ECDSA proof by Orochi Network
function redeem(bytes memory proof) external nonReentrant returns (uint256) {
XOROECDSAProof memory ecdsa = _decodeProof(proof);

// Signer must be operator
if (!_isOperator(ecdsa.signer)) {
revert InvalidSignature(ecdsa.signer);
}

// Nonce must be valid
if (!_verifyNonce(ecdsa.beneficiary, ecdsa.nonce)) {
revert InvalidNonce(ecdsa.beneficiary, ecdsa.nonce);
}

// Chain Id must be correct
if (uint(ecdsa.chainId) != block.chainid) {
revert InvalidChain(ecdsa.chainId);
}

// Mint the token for beneficiary
_mint(ecdsa.beneficiary, ecdsa.value);

// Increase his nonce
_increaseNonce(ecdsa.beneficiary);

return ecdsa.value;
}

//====================[ Disabled ]====================

/**
Expand Down
67 changes: 67 additions & 0 deletions contracts/token/XOROECDSA.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;
import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';
import '../libraries/Bytes.sol';

error InvalidSignature(address signer);
error InvalidNonce(address receiverAddress, uint64 nonce);
error InvalidChain(uint256 chainId);

contract XOROECDSA {
// Using Bytes for bytes
using Bytes for bytes;

// Verifiy digital signature
using ECDSA for bytes;
using ECDSA for bytes32;

// Storage of recent epoch's result
mapping(address => uint256) private nonceStorage;

// Structure of ECDSA proof of XORO
struct XOROECDSAProof {
address signer;
uint96 chainId;
address beneficiary;
uint64 nonce;
uint192 value;
}

// Verify proof of operator
// uint96 chainId;
// address beneficiary;
// uint64 nonce;
// uint192 value;
function _decodeProof(bytes memory proof) internal pure returns (XOROECDSAProof memory ecdsaProof) {
bytes memory signature = proof.readBytes(0, 65);
bytes memory message = proof.readBytes(65, 64);
uint256 uin256Value = message.readUint256(0);
ecdsaProof.chainId = uint96(uin256Value >> 160);
ecdsaProof.beneficiary = address(uint160(uin256Value));
uin256Value = message.readUint256(32);
ecdsaProof.nonce = uint64(uin256Value >> 192);
ecdsaProof.value = uint192(uin256Value);
ecdsaProof.signer = message.toEthSignedMessageHash().recover(signature);
return ecdsaProof;
}

// Verify nonce
function _getNonce(address singerAddress) internal view returns (uint256) {
return nonceStorage[singerAddress];
}

// Verify nonce
function _verifyNonce(address receiverAddress, uint64 nonce) internal view returns (bool) {
return nonceStorage[receiverAddress] == nonce;
}

// Increase nonce
function _increaseNonce(address receiverAddress) internal {
nonceStorage[receiverAddress] += 1;
}

// Get nonce of a given address
function getNonce(address receiverAddress) external view returns (uint256) {
return _getNonce(receiverAddress);
}
}
36 changes: 18 additions & 18 deletions test/006-orocle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,43 +44,43 @@ describe('OrocleV1', function () {
const data = OrocleEncoding.encodeTokenPrice([
{
symbol: 'BTC',
price: 42000n * 10n ** 9n,
price: 42000n * 10n ** 18n,
},
{
symbol: 'ETH',
price: 2000n * 10n ** 9n,
price: 2000n * 10n ** 18n,
},
{
symbol: 'DOT',
price: 6n * 10n ** 9n,
price: 6n * 10n ** 18n,
},
{
symbol: 'MINA',
price: 1n * 10n ** 9n,
price: 1n * 10n ** 18n,
},
{
symbol: 'USDT',
price: 1n * 10n ** 9n,
price: 1n * 10n ** 18n,
},
{
symbol: 'USDC',
price: 1n * 10n ** 9n,
price: 1n * 10n ** 18n,
},
{
symbol: 'USA',
price: 1n * 10n ** 9n,
price: 1n * 10n ** 18n,
},
{
symbol: 'USB',
price: 1n * 10n ** 9n,
price: 1n * 10n ** 18n,
},
{
symbol: 'USG',
price: 1n * 10n ** 9n,
price: 1n * 10n ** 18n,
},
{
symbol: 'USS',
price: 1n * 10n ** 9n,
price: 1n * 10n ** 18n,
},
]);

Expand All @@ -93,19 +93,19 @@ describe('OrocleV1', function () {
const data = OrocleEncoding.encodeTokenPrice([
{
symbol: 'BTC',
price: 42000n * 10n ** 9n,
price: 42000n * 10n ** 18n,
},
{
symbol: 'ETH',
price: 2000n * 10n ** 9n,
price: 2000n * 10n ** 18n,
},
{
symbol: 'DOT',
price: 6n * 10n ** 9n,
price: 6n * 10n ** 18n,
},
{
symbol: 'MINA',
price: 1n * 10n ** 9n,
price: 1n * 10n ** 18n,
},
]);

Expand All @@ -126,20 +126,20 @@ describe('OrocleV1', function () {
const data = OrocleEncoding.encodeTokenPrice([
{
symbol: 'BTC',
price: 42000n * 10n ** 9n,
price: 42000n * 10n ** 18n,
},
{
symbol: 'ETH',
price: 2000n * 10n ** 9n,
price: 2000n * 10n ** 18n,
},
{
symbol: 'DOT',
price: 6n * 10n ** 9n,
price: 6n * 10n ** 18n,
},

{
symbol: 'MINA',
price: 1n * 10n ** 9n,
price: 1n * 10n ** 18n,
},
...tokens,
]);
Expand Down
19 changes: 11 additions & 8 deletions test/008-orand-provider-v3.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,35 +395,38 @@ describe('OrandProviderV3', function () {
it('OrandProviderV3 should be upgradeable', async () => {
const { ethers } = hre;
const accounts = await ethers.getSigners();
const orocleV1Factory = await ethers.getContractFactory('OrocleV1');
const orocleV2Factory = await ethers.getContractFactory('OrocleV2');
const orandECVRFV3Factory = await ethers.getContractFactory('OrandECVRFV3');
const orand1Factory = await ethers.getContractFactory('OrandProviderTest');
const orand2Factory = await ethers.getContractFactory('OrandProviderV3');
const orandV3Factory1 = await ethers.getContractFactory('OrandProviderTest');
const orandV3Factory2 = await ethers.getContractFactory('OrandProviderV3');

const orocleV1 = await orocleV1Factory.deploy([accounts[0]]);
console.log('Orocle V1', await orocleV1.getAddress());
const orocleV2 = await upgrades.deployProxy(orocleV2Factory, [[accounts[0].address]]);
console.log('Orocle V2', await orocleV2.getAddress());
let correspondingAddress = getAddress(`0x${keccak256(`0x${pk.substring(2, 130)}`).substring(26, 66)}`);
const orandECVRFV3 = await orandECVRFV3Factory.deploy();
console.log('Orand ECVRF V3', await orandECVRFV3.getAddress());

const instance = await upgrades.deployProxy(orand1Factory, [
const instance = await upgrades.deployProxy(orandV3Factory1, [
// uint256[2] memory publicKey
OrandEncoding.pubKeyToAffine(HexString.hexPrefixAdd(pk)),
// address operator
correspondingAddress,
// address ecvrfAddress
await orandECVRFV3.getAddress(),
// address oracleAddress
await orocleV1.getAddress(),
await orocleV2.getAddress(),
// uint256 maxBatchingLimit
100,
]);

console.log('Instance:', await instance.getOracle(), await instance.getAddress());

const upgraded = await upgrades.upgradeProxy(await instance.getAddress(), orand2Factory);
let upgraded = await upgrades.upgradeProxy(await instance.getAddress(), orandV3Factory1);

console.log('Upgrade', await upgraded.getOracle(), await upgraded.getAddress());

upgraded = await upgrades.upgradeProxy(await instance.getAddress(), orandV3Factory2);

orandProviderV3 = upgraded as any;
});

Expand Down
71 changes: 64 additions & 7 deletions test/009-orocle-v2.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,75 @@
import hre, { upgrades } from 'hardhat';
import { OrocleV2 } from '../typechain-types';
import { FixedFloat, OrocleEncoding } from '@orochi-network/utilities';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';

describe('OrocleV1', function () {
it('OrocleV1 should be upgradeable', async () => {
let orocleV2: OrocleV2;
let operator: HardhatEthersSigner;

describe('OrocleV2', function () {
it('OrocleV2 should be upgradeable', async () => {
const { ethers } = hre;
const accounts = await ethers.getSigners();
const orocleV1Factory = await ethers.getContractFactory('OrocleTest');
const orocleV2Factory = await ethers.getContractFactory('OrocleV1');
[operator] = await ethers.getSigners();
const orocleV2Factory1 = await ethers.getContractFactory('OrocleTest');
const orocleV2Factory2 = await ethers.getContractFactory('OrocleV2');

const instance = await upgrades.deployProxy(orocleV1Factory, [accounts[0]]);
const instance = await upgrades.deployProxy(orocleV2Factory1, [[operator.address]]);

console.log('Instance:', await instance.owner(), await instance.getAddress());

const upgraded = await upgrades.upgradeProxy(await instance.getAddress(), orocleV2Factory);
const upgraded = await upgrades.upgradeProxy(await instance.getAddress(), orocleV2Factory2);

console.log('Upgrade', await upgraded.owner(), await upgraded.getAddress());
orocleV2 = upgraded as any;
});

it('OrocleV2 should be upgradeable', async () => {
await orocleV2.connect(operator).publishPrice(
OrocleEncoding.encodeTokenPrice([
{
symbol: 'BTC',
price: 42000n * 10n ** 18n,
},
]),
);
const [round, lastUpdate, price] = await orocleV2.getLatestRound(1, OrocleEncoding.toIdentifier('BTC'));

console.log('Round data:', { round, lastUpdate, price });

console.log(
'BTC/USDT',
FixedFloat.fromFixedFloat({
basedValue: BigInt(price),
decimals: 18,
}).pretty('en-us', 2),
);
});

it('OrocleV2 should be upgradeable', async () => {
await orocleV2.connect(operator).publishPrice(
OrocleEncoding.encodeTokenPrice([
{
symbol: 'BTC',
price: 42000n * 10n ** 18n,
},
{
symbol: 'ETH',
price: 3821n * 10n ** 18n,
},
]),
);
const [round, lastUpdate, price] = await orocleV2.getLatestRound(1, OrocleEncoding.toIdentifier('ETH'));

console.log('Round data:', { round, lastUpdate, price });

console.log(
'ETH/USDT',
FixedFloat.fromFixedFloat({
basedValue: BigInt(price),
decimals: 18,
}).pretty('en-us', 2),
);

console.log(await orocleV2.getLatestRound(1, OrocleEncoding.toIdentifier('BTC')));
});
});
Loading

0 comments on commit 1fd30ef

Please sign in to comment.