diff --git a/contract/bridge/contracts/ExchangeRateOracle.sol b/contract/bridge/contracts/ExchangeRateOracle.sol index 41153da..f55856a 100644 --- a/contract/bridge/contracts/ExchangeRateOracle.sol +++ b/contract/bridge/contracts/ExchangeRateOracle.sol @@ -7,39 +7,45 @@ https://onebtc-dev.web.app/spec/oracle.html The Exchange Rate Oracle receives a continuous data feed on the exchange rate between BTC and ONE. */ -pragma solidity 0.6.12; - -import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/math/MathUpgradeable.sol"; - -contract ExchangeRateOracle is Initializable { - using SafeMathUpgradeable for uint256; +pragma solidity ^0.6.12; +contract ExchangeRateOracle { uint256 constant MAX_DELAY = 1000; - uint256 public lastExchangeRateTime; uint256 exchangeRate; - - mapping(address => bool) authorizedOracles; - - event SetExchangeRate(address oracle, uint256 rate); - - event recoverFromORACLEOFFLINE(address oracle, uint256 rate); - - function initialize(address provider) public initializer { - lastExchangeRateTime = now; - authorizedOracles[provider] = true; + uint256 satoshiPerBytes; + mapping (address => bool) authorizedOracles; + + event SetExchangeRate( + address oracle, + uint256 rate + ); + + event SetSatoshiPerByte( + uint256 fee, + uint256 inclusionEstimate + ); + + event recoverFromORACLEOFFLINE( + address oracle, + uint256 rate + ); + + constructor() public { + authorizedOracles[0x5B38Da6a701c568545dCfcB03FcB875f56beddC4] = true; } - function setExchangeRate(uint256 btcPrice, uint256 onePrice) public { - address oracle = msg.sender; - require(authorizedOracles[oracle], "Sender is not authorized"); + /** + @notice Set the latest (aggregate) BTC/ONE exchange rate. This function invokes a check of vault collateral rates in the Vault Registry component. + @param oracle the oracle account calling this function. Must be pre-authorized and tracked in this component! + @param rate the u128 BTC/ONE exchange rate. + */ + function setExchangeRate(address oracle, uint256 rate) public { + require(authorizedOracles[oracle], "ERR_INVALID_ORACLE_SOURCE"); - uint256 rate = btcPrice.div(onePrice); exchangeRate = rate; - if (now - lastExchangeRateTime > MAX_DELAY) { + if (lastExchangeRateTime - now > MAX_DELAY) { emit recoverFromORACLEOFFLINE(oracle, rate); } @@ -48,32 +54,49 @@ contract ExchangeRateOracle is Initializable { emit SetExchangeRate(oracle, rate); } - function getExchangeRate() private view returns (uint256) { - require( - now - lastExchangeRateTime <= MAX_DELAY, - "Exchange rate avaialble is too old" - ); + /** + @notice Set the Satoshi per bytes fee + @param fee the Satoshi per byte fee. + @param inclusionEstimate the estimated inclusion time. + */ + function setSatoshiPerBytes(uint256 fee, uint256 inclusionEstimate) public { + // 1. The BTC Bridge status in the Security component MUST be set to RUNNING:0. + // TODO require() + + require(authorizedOracles[msg.sender], "ERR_INVALID_ORACLE_SOURCE"); + + satoshiPerBytes = inclusionEstimate; + + emit SetSatoshiPerByte(fee, inclusionEstimate); + } + + /** + @notice Returns the latest BTC/ONE exchange rate, as received from the external data sources. + @return uint256 (aggregate) exchange rate value + */ + function getExchangeRate() public view returns (uint256) { + require (now - lastExchangeRateTime > MAX_DELAY, "ERR_MISSING_EXCHANGE_RATE"); return exchangeRate; } /** - * @notice Get BTC amount by ONE. - * @param amount collateral(ONE) amount - * @return BTC amount - */ - function collateralToWrapped(uint256 amount) public view returns (uint256) { + * @notice Get BTC amount by ONE. + * @param amount collateral(ONE) amount + * @return BTC amount + */ + function collateralToWrapped(uint256 amount) public view returns(uint256) { uint256 rate = getExchangeRate(); - return amount.div(rate).mul(10**8).div(10**18); + return amount/rate; } /** - * @notice Get ONE amount by BTC. - * @param amount BTC amount - * @return ONE amount - */ - function wrappedToCollateral(uint256 amount) public view returns (uint256) { + * @notice Get ONE amount by BTC. + * @param amount BTC amount + * @return ONE amount + */ + function wrappedToCollateral(uint256 amount) public view returns(uint256) { uint256 rate = getExchangeRate(); - return amount.mul(rate).mul(10**18).div(10**8); + return amount*rate; } } diff --git a/contract/bridge/contracts/SLA.sol b/contract/bridge/contracts/SLA.sol index 873e721..5ec7267 100644 --- a/contract/bridge/contracts/SLA.sol +++ b/contract/bridge/contracts/SLA.sol @@ -2,7 +2,12 @@ pragma solidity 0.6.12; +import "@openzeppelin/contracts/math/SafeMath.sol"; + contract SLA { + + using SafeMath for uint256; + event UpdateVaultSLA( address indexed vaultId, uint256 boundedNewSla, @@ -12,7 +17,7 @@ contract SLA { event UpdateRelayerSLA( address indexed relayerId, uint256 newSla, - uint256 deltaSla + int256 deltaSla ); enum VaultEvent { @@ -44,7 +49,6 @@ contract SLA { uint256 withdraw; uint256 liquidate; uint256 sla; - uint256 vaultTargetSla; } uint256 public VaultSLATarget = 100; @@ -52,8 +56,39 @@ contract SLA { mapping(address => SlaData) VaultSLA; mapping(address => SlaData) StakedRelayerSLA; + mapping(address => bool) VaultTrue; + + + /* + - adjust stake + - liquidate stake + - calculate slashed amount + - fixed point unsigned to signed + - wrapper to uint128 + - currency to fixed point + */ + + constructor( uint256 _TotalIssueCount, + uint256 _LifetimeIssued, + uint256 _VaultExecuteIssueMaxSlaChange, + uint256 _VaultDepositMaxSlaChange, + uint256 _VaultWithdrawMaxSlaChange, + uint256 _AverageDepositCount, + uint256 _AverageDeposit, + uint256 _AverageWithdrawCount, + uint256 _AverageWithdraw) public { + TotalIssueCount = _TotalIssueCount; + LifetimeIssued = _LifetimeIssued; + VaultDepositMaxSlaChange = _VaultDepositMaxSlaChange; + VaultWithdrawMaxSlaChange = _VaultWithdrawMaxSlaChange; + AverageDeposit = _AverageDeposit; + AverageDepositCount = _AverageDepositCount; + AverageWithdraw = _AverageWithdraw; + AverageWithdrawCount = _AverageWithdrawCount; + } - function _executeIssueSlaChange(uint256 amount) private returns (uint256) { + + function _executeIssueSlaChange(uint256 amount) internal returns (uint256) { uint256 count = TotalIssueCount + 1; TotalIssueCount = count; uint256 total = LifetimeIssued + amount; @@ -64,7 +99,12 @@ contract SLA { return (amount * maxSlaChange) / average; } - function _depositSlaChange(uint256 amount) private returns (uint256) { + + // Calculates the potential sla change for a vault depositing collateral. The value will be + // clipped between 0 and VaultDepositMaxSlaChange, but it does not take into consideration + // Vault's current SLA. It can return a value > 0 when its sla is already at the maximum. + + function _depositSlaChange(uint256 amount) internal returns (uint256) { uint256 maxSlaChange = VaultDepositMaxSlaChange; uint256 count = AverageDepositCount + 1; @@ -76,7 +116,7 @@ contract SLA { return (amount / average) * maxSlaChange; } - function _withdrawSlaChange(uint256 amount) private returns (uint256) { + function _withdrawSlaChange(uint256 amount) internal returns (uint256) { uint256 maxSlaChange = VaultWithdrawMaxSlaChange; uint256 count = AverageWithdrawCount + 1; @@ -88,11 +128,11 @@ contract SLA { return (amount / average) * maxSlaChange; } - function _liquidateSla(address vaultId) private returns (int256) { + function _liquidateSla(address vaultId) internal returns (int256) { // TODO //Self::liquidateStake::(vaultId)?; //Self::liquidateStake::(vaultId)?; - revert("TODO"); + // revert("TODO"); SlaData storage slaData = VaultSLA[vaultId]; int256 deltaSla = -int256(slaData.sla); slaData.sla = 0; @@ -103,11 +143,13 @@ contract SLA { uint256 min, uint256 cur, uint256 max - ) private pure returns (uint256) { + ) internal pure returns (uint256) { return cur > max ? max : (cur > min ? cur : min); } - function eventUpdateVaultSla( + event data (uint256,uint256,uint256, uint256); + + function _eventUpdateVaultSla( address vaultId, VaultEvent eventType, uint256 amount @@ -131,11 +173,11 @@ contract SLA { _liquidateSla(vaultId); return; } else { - revert("unknow type"); + revert("unknown type"); } uint256 newSla = currentSla + deltaSla; - uint256 maxSla = slaData.vaultTargetSla; // todo: check that this is indeed the max + uint256 maxSla = VaultSLATarget; // todo: check that this is indeed the max uint256 boundedNewSla = limit(0, newSla, maxSla); /* @@ -146,7 +188,89 @@ contract SLA { emit UpdateVaultSLA(vaultId, boundedNewSla, int256(deltaSla)); } - function SlashVault(address account) internal returns (uint256) {} - function updateSLA(address account, uint256 delta) internal {} + function _eventUpdateRelayerSla( + address relayerId, + VaultEvent eventType, + uint256 amount + ) internal { + SlaData storage slaData = StakedRelayerSLA[relayerId]; + uint256 currentSla = slaData.sla; + uint256 deltaSla; + if (eventType == VaultEvent.RedeemFailure) { + deltaSla = slaData.vaultRedeemFailure; + } else if (eventType == VaultEvent.SubmitIssueProof) { + deltaSla = slaData.vaultSubmitIssueProof; + } else if (eventType == VaultEvent.Refund) { + deltaSla = slaData.vaultRefund; + } else if (eventType == VaultEvent.ExecuteIssue) { + deltaSla = _executeIssueSlaChange(amount); + } else if (eventType == VaultEvent.Deposit) { + deltaSla = _depositSlaChange(amount); + } else if (eventType == VaultEvent.Withdraw) { + deltaSla = _withdrawSlaChange(amount); + } else if (eventType == VaultEvent.Liquidate) { + _liquidateSla(relayerId); + return; + } else { + revert("unknown type"); + } + + uint256 newSla = currentSla + deltaSla; + uint256 maxSla = VaultSLATarget; // todo: check that this is indeed the max + + uint256 boundedNewSla = limit(0, newSla, maxSla); + /* + Self::adjustStake::(vaultId, deltaSla)?; + Self::adjustStake::(vaultId, deltaSla)?; + */ + slaData.sla = boundedNewSla; + emit UpdateVaultSLA(relayerId, boundedNewSla, int256(deltaSla)); + } + + function calculateSlashAmount(address account) internal returns (uint256) { + SlaData memory vault = VaultSLA[account]; + uint256 slaTarget = VaultSLATarget; + uint256 sla = vault.sla; + uint256 liquidateThreshold = vault.liquidate; + uint256 premiumRedeemThreshold = vault.vaultRedeemFailure; + + uint256 realSlashed = premiumRedeemThreshold.sub(liquidateThreshold).div(slaTarget).mul(sla).add(liquidateThreshold); + + return realSlashed; + } + + function updateVaultSLA(address account, int256 delta) internal { + SlaData storage vault; + vault = VaultSLA[account]; + + if(delta > 0){ + vault.sla = vault.sla + uint256(delta); + } + if(delta <0){ + vault.sla = vault.sla - uint256(delta); + } + UpdateVaultSLA(account, vault.sla, delta); + } + + function _updateRelayerSla(address account, int256 delta) internal { + SlaData storage vault = StakedRelayerSLA[account]; + if(delta > 0){ + vault.sla = vault.sla + uint256(delta); + } + if(delta <0){ + vault.sla = vault.sla - uint256(delta); + } + UpdateRelayerSLA(account, vault.sla, delta); + + } + + + function getRelayerSla ( address vaultId) public view returns (uint256){ + return StakedRelayerSLA[vaultId].sla; + } + + function getVaultSla(address vaultId) public view returns (uint256 ){ + return VaultSLA[vaultId].sla; + } } diff --git a/contract/bridge/contracts/test/SlaWrapper.sol b/contract/bridge/contracts/test/SlaWrapper.sol new file mode 100644 index 0000000..298a38c --- /dev/null +++ b/contract/bridge/contracts/test/SlaWrapper.sol @@ -0,0 +1,55 @@ +pragma solidity ^0.6.12; + +import {SLA} from "../SLA.sol"; + +contract SLAWrapper is SLA { + // function slashVaultTest (address account) public returns (uint256){ + // uint256 slashAmount = SlashVault(account); + // return slashAmount; + // } + + // function updateSlaTes (address account, int256 delta) internal returns(uint256){ + // updateSLA(account, delta); + // } + + constructor(uint256 _TotalIssueCount, + uint256 _LifetimeIssued, + uint256 _VaultExecuteIssueMaxSlaChange, + uint256 _VaultDepositMaxSlaChange, + uint256 _VaultWithdrawMaxSlaChange, + uint256 _AverageDepositCount, + uint256 _AverageDeposit, + uint256 _AverageWithdrawCount, + uint256 _AverageWithdraw) SLA( _TotalIssueCount, + _LifetimeIssued, + _VaultExecuteIssueMaxSlaChange, + _VaultDepositMaxSlaChange, + _VaultWithdrawMaxSlaChange, + _AverageDepositCount, + _AverageDeposit, + _AverageWithdrawCount, + _AverageWithdraw) public {} + + function eventUpdateVaultSla(address vaultId, VaultEvent eventType,uint256 amount) public returns (uint256) { + _eventUpdateVaultSla(vaultId, eventType, amount); + } + + + function eventUpdateRelayerSla(address vaultId, VaultEvent eventType,uint256 amount) public returns (uint256) { + _eventUpdateRelayerSla(vaultId, eventType, amount); + } + + function updateRelayerSla(address account, int256 delta) public { + _updateRelayerSla(account, delta); + } + + // for use in tests + function setVaultSla(address vaultId, uint256 sla) public { + VaultSLA[vaultId].sla = sla; + } + + function setRelayerSla(address vaultId, uint256 sla) public { + StakedRelayerSLA[vaultId].sla = sla; + } + +} diff --git a/contract/bridge/scripts/sla.js b/contract/bridge/scripts/sla.js new file mode 100644 index 0000000..e69de29 diff --git a/contract/bridge/test/ExchangeRateOracle.test.js b/contract/bridge/test/ExchangeRateOracle.test.js index 3a0a8f6..246a2b2 100644 --- a/contract/bridge/test/ExchangeRateOracle.test.js +++ b/contract/bridge/test/ExchangeRateOracle.test.js @@ -2,7 +2,9 @@ const BN = require("bn.js"); const { expectRevert } = require("@openzeppelin/test-helpers"); const { web3 } = require("@openzeppelin/test-helpers/src/setup"); const { deployProxy } = require("@openzeppelin/truffle-upgrades"); -const ExchangeRateOracleWrapper = artifacts.require("ExchangeRateOracleWrapper"); +const ExchangeRateOracleWrapper = artifacts.require( + "ExchangeRateOracleWrapper" +); web3.extend({ property: "miner", @@ -22,7 +24,9 @@ web3.extend({ contract("ExchangeRateOracle unit test", (accounts) => { before(async function () { - this.ExchangeRateOracleWrapper = await deployProxy(ExchangeRateOracleWrapper); + this.ExchangeRateOracleWrapper = await deployProxy( + ExchangeRateOracleWrapper + ); }); it("setExchangeRate, getExchangeRate", async function () { @@ -34,7 +38,10 @@ contract("ExchangeRateOracle unit test", (accounts) => { const req = await this.ExchangeRateOracleWrapper.setExchangeRate(100); // check exchange rate, reverted with `ERR_INVALID_ORACLE_SOURCE` due to the MAX_DELAY limit - await expectRevert(this.ExchangeRateOracleWrapper.getExchangeRate(), 'ERR_MISSING_EXCHANGE_RATE'); + await expectRevert( + this.ExchangeRateOracleWrapper.getExchangeRate(), + "ERR_MISSING_EXCHANGE_RATE" + ); const event = req.logs.filter((log) => log.event == "SetExchangeRate")[0]; assert.equal(event.args.exchangeRate, 100); @@ -48,22 +55,26 @@ contract("ExchangeRateOracle unit test", (accounts) => { }); it("collateralToWrapped", async function () { - const amount = new BN('1000000000000000000'); // 1e18 - const collateralToWrapped = await this.ExchangeRateOracleWrapper.collateralToWrapped(amount); - + const amount = new BN("1000000000000000000"); // 1e18 + const collateralToWrapped = + await this.ExchangeRateOracleWrapper.collateralToWrapped(amount); + // check collateralToWrapped const exchangeRate = await this.ExchangeRateOracleWrapper.getExchangeRate(); - const expectedWrapped = amount.div(exchangeRate).div(new BN('10000000000')); + const expectedWrapped = amount.div(exchangeRate).div(new BN("10000000000")); assert.equal(collateralToWrapped.toString(), expectedWrapped.toString()); }); it("wrappedToCollateral", async function () { - const amount = new BN('100000000'); // 1e8 - const wrappedToCollateral = await this.ExchangeRateOracleWrapper.wrappedToCollateral(amount); - + const amount = new BN("100000000"); // 1e8 + const wrappedToCollateral = + await this.ExchangeRateOracleWrapper.wrappedToCollateral(amount); + // check collateralToWrapped const exchangeRate = await this.ExchangeRateOracleWrapper.getExchangeRate(); - const expectedCollateral = amount.mul(exchangeRate).mul(new BN('10000000000')); + const expectedCollateral = amount + .mul(exchangeRate) + .mul(new BN("10000000000")); assert.equal(wrappedToCollateral.toString(), expectedCollateral.toString()); }); }); diff --git a/contract/bridge/test/sla.js b/contract/bridge/test/sla.js new file mode 100644 index 0000000..e81b0f8 --- /dev/null +++ b/contract/bridge/test/sla.js @@ -0,0 +1,57 @@ +const SLA = artifacts.require("SLAWrapper"); + +contract("SLA", (account) => { + let contractInstance; + const vaultId = account[3]; + + before(async () => { + contractInstance = await SLA.new(100, 100, 10, 10, 10, 20, 20, 20, 20); + }); + + it("get functions working correctly and setting up vaultSla values for tests", async () => { + assert.equal(await contractInstance.getRelayerSla(vaultId), 0); + assert.equal(await contractInstance.getVaultSla(vaultId), 0); + + await contractInstance.setVaultSla(vaultId, 50); + await contractInstance.setRelayerSla(vaultId, 30); + + assert.equal(await contractInstance.getRelayerSla(vaultId), 30); + assert.equal(await contractInstance.getVaultSla(vaultId), 50); + }); + + it("deposit sla change - vault id : 4", async () => { + await contractInstance.eventUpdateVaultSla(vaultId, 4, 60); + const sla = await contractInstance.getVaultSla(vaultId); + assert.equal(sla.toNumber(), 70); + }); + + it("withdraw sla change - vault id: 5", async () => { + await contractInstance.eventUpdateVaultSla(vaultId, 5, 55); + const sla = await contractInstance.getVaultSla(vaultId); + assert.equal(sla.toNumber(), 90); + }); + + it("execute issue sla change - vault id: 3", async () => { + await contractInstance.eventUpdateVaultSla(vaultId, 3, 25); + const sla = await contractInstance.getVaultSla(vaultId); + assert.equal(sla.toNumber(), 90); + }); + + it("deposit sla change - relayer id : 4", async () => { + await contractInstance.eventUpdateRelayerSla(vaultId, 4, 60); + const sla = await contractInstance.getRelayerSla(vaultId); + assert.equal(sla.toNumber(), 50); + }); + + it("withdraw sla change - relayer id: 5", async () => { + await contractInstance.eventUpdateRelayerSla(vaultId, 5, 55); + const sla = await contractInstance.getRelayerSla(vaultId); + assert.equal(sla.toNumber(), 70); + }); + + it("execute issue sla change - relayer id: 3", async () => { + await contractInstance.eventUpdateRelayerSla(vaultId, 3, 25); + const sla = await contractInstance.getRelayerSla(vaultId); + assert.equal(sla.toNumber(), 70); + }); +}); diff --git a/contract/bridge/truffle-config.js b/contract/bridge/truffle-config.js index edf0fb3..c7cb3a1 100644 --- a/contract/bridge/truffle-config.js +++ b/contract/bridge/truffle-config.js @@ -33,38 +33,35 @@ module.exports = { networks: { test: { - host: 'localhost', - port: 7545, - network_id: '5777', + host: "localhost", + port: 9545, + network_id: "5777", // gas: 4712388 // host: "127.0.0.1", // port: 8545, // network_id: '*', - accounts: 5, - defaultEtherBalance: 500, - blockTime: 0 }, local: { - host: 'localhost', + host: "localhost", port: 7545, - network_id: '5777', + network_id: "5777", // gas: 4712388 // host: "127.0.0.1", // port: 8545, // network_id: '*', accounts: 5, defaultEtherBalance: 500, - blockTime: 0 + blockTime: 0, }, develop: { host: "127.0.0.1", port: 8545, - network_id: '*', + network_id: "*", accounts: 5, defaultEtherBalance: 500, - blockTime: 0 + blockTime: 0, }, }, @@ -76,11 +73,12 @@ module.exports = { // Configure your compilers compilers: { solc: { - version: "0.6.12+commit.27d51765", // Fetch exact version from solc-bin (default: truffle's version) - settings: { // See the solidity docs for advice about optimization and evmVersion + version: "0.6.12+commit.27d51765", // Fetch exact version from solc-bin (default: truffle's version) + settings: { + // See the solidity docs for advice about optimization and evmVersion optimizer: { enabled: true, - runs: 200 + runs: 200, }, // evmVersion: "byzantium" }