From c89258f1ac35936f2e164f0484f9f7cf8093a420 Mon Sep 17 00:00:00 2001 From: Christophe Le Bars Date: Thu, 16 Aug 2018 05:47:29 +0700 Subject: [PATCH] bridge RGE draft (home & foreign contracts) --- contracts/BridgeRGEToken.sol | 155 +++++++++++++++++++ contracts/RougeBridge.sol | 121 +++++++++++++++ migrations/2_deploy_contracts.js | 11 +- test/BridgeRGEToken.js | 148 ++++++++++++++++++ test/RougeBridge.js | 202 ++++++++++++++++++++++++ yarn.lock | 258 +++++++++++++++++++++++++++++++ 6 files changed, 892 insertions(+), 3 deletions(-) create mode 100644 contracts/BridgeRGEToken.sol create mode 100644 contracts/RougeBridge.sol create mode 100644 test/BridgeRGEToken.js create mode 100644 test/RougeBridge.js create mode 100644 yarn.lock diff --git a/contracts/BridgeRGEToken.sol b/contracts/BridgeRGEToken.sol new file mode 100644 index 0000000..e2f25e8 --- /dev/null +++ b/contracts/BridgeRGEToken.sol @@ -0,0 +1,155 @@ +/* + + Bridged RGE contract to be used in FOREIGN CHAIN (not Ethereum Mainnet) + + See RougeBridge for the contract on Mainnet responsible to lock home RGE for every Bridged RGE + +*/ + +pragma solidity ^0.4.24; + +import "./EIP20.sol"; + +contract BridgeRGEToken is EIP20 { + + /* ERC20 */ + string public name; + string public symbol; + uint8 public decimals = 6; + + /* RGEToken - keeping most of the interface similar to RGE home */ + address owner; + string public version = 'v1.0f'; + uint256 public totalSupply = 1000000000 * 10**uint(decimals); + uint256 public reserveY1 = 0; + uint256 public reserveY2 = 0; + + modifier onlyBy(address _address) { + require(msg.sender == _address); + _; + } + + uint public network; /* the foreign RGE network ID */ + address public homeAuthority; /* owner of the RougeBridge contract on mainnet */ + address public bridge; + + constructor(uint _network, address _bridge, address _homeAuthority, string _name, string _symbol) + EIP20 (totalSupply, _name, decimals, _symbol) public { + owner = msg.sender; + network = _network; + homeAuthority = _homeAuthority; + bridge = _bridge; + balances[address(0)] = totalSupply; /* RGE on address(0) means there are not on this foreign chain */ + } + + bool public bridgeIsOpened = true; + + function toggleBridge(bool _flag) onlyBy(owner) public { + bridgeIsOpened = _flag; + } + + modifier BridgeOpen() { + require(bridgeIsOpened); + _; + } + + mapping (address => mapping (uint => bool)) public claimed; + mapping (address => mapping (uint => bool)) public surrendered; + + /* the first hash can be read on the bridge main chain by the user */ + /* the second hash is be sure that the RGE tokens are already locked + homeAuthority signature */ + + event RGEClaim(address indexed account, uint indexed _network, uint indexed depositBlock, uint256 value); + + function claim(bytes32 _sealHash, bytes32 _authHash, uint256 _value, uint _depositBlock, uint _lockBlock, + uint8 vLock, bytes32 rLock, bytes32 sLock, uint8 vAuth, bytes32 rAuth, bytes32 sAuth) + BridgeOpen public returns (bool success) { + require(msg.sender != homeAuthority); + require(balances[address(0)] >= _value); + require(!claimed[msg.sender][_depositBlock]); // check if the deposit has not been already claimed + bytes32 _lockHash = keccak256(abi.encodePacked('locking', msg.sender, _value, network, bridge, _depositBlock)); + require(ecrecover(prefixed(_lockHash), vLock, rLock, sLock) == owner); + require(_sealHash == keccak256(abi.encodePacked('sealing', _lockHash, vLock, rLock, sLock, _lockBlock))); + require(_authHash == keccak256(abi.encodePacked('authorization', _sealHash))); + require(ecrecover(prefixed(_authHash), vAuth, rAuth, sAuth) == homeAuthority); + claimed[msg.sender][_depositBlock] = true; // distribute the claim + balances[address(0)] -= _value; + balances[msg.sender] += _value; + emit RGEClaim(msg.sender, network, _depositBlock, _value); + emit Transfer(address(0), msg.sender, _value); + return true; + } + + event Surrender(address indexed account, uint indexed _network, uint indexed depositBlock, uint256 value); + + function surrender(uint256 _value, uint _depositBlock, uint8 vLock, bytes32 rLock, bytes32 sLock) public returns (bool success) { + require(msg.sender != homeAuthority); + require(claimed[msg.sender][_depositBlock]); + require(!surrendered[msg.sender][_depositBlock]); + bytes32 _lockHash = keccak256(abi.encodePacked('locking', msg.sender, _value, network, bridge, _depositBlock)); + require(ecrecover(prefixed(_lockHash), vLock, rLock, sLock) == owner); + require(balances[msg.sender] >= _value); + surrendered[msg.sender][_depositBlock] = true; // withdraw tokens from circulation + balances[msg.sender] -= _value; + balances[address(0)] += _value; + emit Transfer(msg.sender, address(0), _value); + emit Surrender(msg.sender, network, _depositBlock, _value); + return true; + } + + // XXX to put in lib + + function getHexString(bytes32 value) internal pure returns (string) { + bytes memory result = new bytes(64); + string memory characterString = "0123456789abcdef"; + bytes memory characters = bytes(characterString); + for (uint8 i = 0; i < 32; i++) { + result[i * 2] = characters[uint256((value[i] & 0xF0) >> 4)]; + result[i * 2 + 1] = characters[uint256(value[i] & 0xF)]; + } + return string(result); + } + + function prefixed(bytes32 _hash) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n76Bridge fct: ", getHexString(_hash))); + } + + // This function should be called after a surrender, to publish information necessary to unlock tokens on home chain + + event Repudiate(address indexed account, uint indexed _network, uint indexed depositBlock, uint8 v, bytes32 r, bytes32 s); + + function repudiate(bytes32 _hash, address _account, uint _depositBlock, uint8 v, bytes32 r, bytes32 s) + onlyBy(owner) public { + require(msg.sender != _account); + require(surrendered[_account][_depositBlock]); + require(_hash == keccak256(abi.encodePacked('unlocking', _account, network, bridge, _depositBlock))); + require(ecrecover(prefixed(_hash), v, r, s) == msg.sender); + emit Repudiate(_account, network, _depositBlock, v, r, s); + } + + /* standard RGE interface */ + + address public factory; + + function setFactory(address _factory) onlyBy(owner) public { + factory = _factory; + } + + function newCampaign(uint32 _issuance, uint256 _value) public { + transfer(factory,_value); + require(factory.call(bytes4(keccak256("createCampaign(address,uint32,uint256)")),msg.sender,_issuance,_value)); + } + + event Burn(address indexed burner, uint256 value); + + function burn(uint256 _value) public returns (bool success) { + require(_value > 0); + require(balances[msg.sender] >= _value); + balances[msg.sender] -= _value; + totalSupply -= _value; + emit Transfer(msg.sender, address(0), _value); + emit Burn(msg.sender, _value); + return true; + } + +} diff --git a/contracts/RougeBridge.sol b/contracts/RougeBridge.sol new file mode 100644 index 0000000..8455aeb --- /dev/null +++ b/contracts/RougeBridge.sol @@ -0,0 +1,121 @@ +/* + + Bridge to send RGE in other chains (eg POA, ...) + +*/ + +pragma solidity ^0.4.24; + +import "./RGETokenInterface.sol"; + +contract RougeBridge { + + string public version = 'v0.1'; + + address public owner; + + modifier onlyBy(address _account) { + require(msg.sender == _account); + _; + } + + RGETokenInterface public rge; + + constructor(address _rge) public { + owner = msg.sender; + rge = RGETokenInterface(_rge); + } + + mapping (uint => bool) public isOpen; /* is the bridge open for this network */ + mapping (uint => address) public foreignAuthority; /* foreignAuthority network owner */ + + function adminBridge(uint _network, bool _flag, address _foreignAuthority) onlyBy(owner) public { + isOpen[_network] = _flag; + foreignAuthority[_network] = _foreignAuthority; + } + + mapping (address => mapping (uint => mapping (uint => uint256))) public escrow; + mapping (address => mapping (uint => mapping (uint => bytes32))) public escrowSeal; + + event BridgeDeposit(address indexed account, uint indexed _network, uint indexed depositBlock, uint256 value); + + /* caller need to approve RGE transfer beforehand */ + function deposit(uint256 _value, uint _network) public { + require(isOpen[_network]); + require(msg.sender != owner); + require(_value > 0); + require(escrow[msg.sender][_network][block.number] == 0); // only 1 deposit per block per user maximum (XXX use punitive assert ?) + assert(escrowSeal[msg.sender][_network][block.number] == bytes32(0)); // there can't be seal, but let's be sure + require(rge.transferFrom(msg.sender, this, _value)); // transfer tokens to this home bridge contract + emit BridgeDeposit(msg.sender, _network, block.number, _value); + escrow[msg.sender][_network][block.number] = _value; + } + + event BridgeWithdraw(address indexed account, uint indexed _network, uint indexed depositBlock, uint256 value); + + function withdraw(uint _network, uint _depositBlock) public { + require(escrowSeal[msg.sender][_network][_depositBlock] == bytes32(0)); // can't withdraw locked tokens (ie no seal) + uint256 _value = escrow[msg.sender][_network][_depositBlock]; + require(_value > 0); // tokens should be in escrow + escrow[msg.sender][_network][_depositBlock] = 0; + require(rge.transfer(msg.sender, _value)); // transfer back tokens to sender + emit BridgeWithdraw(msg.sender, _network, _depositBlock, _value); + } + + function getHexString(bytes32 value) internal pure returns (string) { + bytes memory result = new bytes(64); + string memory characterString = "0123456789abcdef"; + bytes memory characters = bytes(characterString); + for (uint8 i = 0; i < 32; i++) { + result[i * 2] = characters[uint256((value[i] & 0xF0) >> 4)]; + result[i * 2 + 1] = characters[uint256(value[i] & 0xF)]; + } + return string(result); + } + + function prefixed(bytes32 _hash) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n76Bridge fct: ", getHexString(_hash))); + } + + + // Calculating the seal and locking the deposit (foreign authority needs to undersign this) + + event EscrowLocked(address indexed account, uint indexed _network, uint indexed depositBlock, uint8 v, bytes32 r, bytes32 s, bytes32 sealHash); + + function lockEscrow(bytes32 _hash, address _account, uint _network, uint _depositBlock, uint8 v, bytes32 r, bytes32 s) + onlyBy(owner) public { + require(escrow[_account][_network][_depositBlock] > 0); // do not work for nothing + require(_hash == keccak256(abi.encodePacked( + 'locking', _account, escrow[_account][_network][_depositBlock], _network, this, _depositBlock))); + require(ecrecover(prefixed(_hash), v, r, s) == foreignAuthority[_network]); // confirms that foreign authority has undersigned the tokens Locking + bytes32 sealHash = keccak256(abi.encodePacked('sealing', _hash, v, r, s, block.number)); + escrowSeal[_account][_network][_depositBlock] = sealHash; + emit EscrowLocked(_account, _network, _depositBlock, v, r, s, sealHash); + } + + // The seal needs to be signed again by RGE bridge owner to be used as authorization to claim token in foreign chain + // The user can claim the bridged tokens as soon as the authorization is visible on the main blockchain (via LOGS) + // (it's ok if the claiming tx comes before the authorization tx) + + event BridgeAuth(address indexed account, uint indexed _network, uint indexed depositBlock, uint8 v, bytes32 r, bytes32 s, bytes32 authHash); + + function createAuth(bytes32 _authHash, address _account, uint _network, uint _depositBlock, uint8 v, bytes32 r, bytes32 s) + onlyBy(owner) public { + require(_authHash == keccak256(abi.encodePacked('authorization', escrowSeal[_account][_network][_depositBlock]))); + require(ecrecover(prefixed(_authHash), v, r, s) == owner); + emit BridgeAuth(_account, _network, _depositBlock, v, r, s, _authHash); + } + + event EscrowUnlocked(address indexed account, uint indexed _network, uint indexed depositBlock); + + function unlockEscrow(bytes32 _hash, address _account, uint _network, uint _depositBlock, uint8 v, bytes32 r, bytes32 s) + onlyBy(owner) public { + require(escrow[_account][_network][_depositBlock] > 0); + escrowSeal[_account][_network][_depositBlock] != bytes32(0); // seal shoudl exists + require(_hash == keccak256(abi.encodePacked('unlocking', _account, _network, this, _depositBlock))); + require(ecrecover(prefixed(_hash), v, r, s) == foreignAuthority[_network]); + escrowSeal[_account][_network][_depositBlock] = bytes32(0); // remove the seal + emit EscrowUnlocked(_account, _network, _depositBlock); + } + +} diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index ef395da..7a99bf4 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -2,11 +2,16 @@ const TestRGEToken = artifacts.require("./TestRGEToken.sol"); const RougeFactory = artifacts.require("./RougeFactory.sol"); +const RougeBridge = artifacts.require("./RougeBridge.sol"); +const BridgeRGEToken = artifacts.require("./BridgeRGEToken.sol"); + module.exports = async function(deployer) { await Promise.all([ deployer.deploy(TestRGEToken), - deployer.deploy(RougeFactory) + deployer.deploy(RougeFactory), + deployer.deploy(RougeBridge, '0x345ca3e014aaf5dca488057592ee47305d9b3e10'), + deployer.deploy(BridgeRGEToken, 3, '0x0', '0x0', 'Foreign RGE', 'f_RGE') ]); instances = await Promise.all([ @@ -19,7 +24,7 @@ module.exports = async function(deployer) { results = await Promise.all([ rge.setFactory(factory.address), - factory.setParams(rge.address,100000) + factory.setParams(rge.address, 100000) ]); - + }; diff --git a/test/BridgeRGEToken.js b/test/BridgeRGEToken.js new file mode 100644 index 0000000..7c32ee1 --- /dev/null +++ b/test/BridgeRGEToken.js @@ -0,0 +1,148 @@ + +const abi = require('ethereumjs-abi') +const BN = require('bn.js') +const ethUtil = require('ethereumjs-util') + +const RGEToken = artifacts.require("./TestRGEToken.sol"); +const RougeBridge = artifacts.require("./RougeBridge.sol"); +const BridgeRGEToken = artifacts.require("./BridgeRGEToken.sol"); + +const createLockHash = function(msg, user, deposit, foreign_network, bridge, depositBlock) { + return '0x' + abi.soliditySHA3( + [ "string", "address", "uint", "uint", "address", "uint" ], + [ msg, new BN(user, 16), deposit, foreign_network, new BN(bridge, 16), depositBlock ] + ).toString('hex') +} + +const createSealHash = function(msg, hash, v, r, s, lockBlock) { + return '0x' + abi.soliditySHA3( + [ "string", "bytes32", "uint8", "bytes32", "bytes32", "uint" ], + [ msg, hash, v, r, s, lockBlock ] + ).toString('hex') +} + +const createAuthHash = function(msg, hash) { + return '0x' + abi.soliditySHA3( + [ "string", "bytes32" ], [ msg, hash ] + ).toString('hex') +} +const createUnlockHash = function(msg, user, foreign_network, bridge, depositBlock) { + return '0x' + abi.soliditySHA3( + [ "string", "address", "uint", "address", "uint" ], + [ msg, new BN(user, 16), foreign_network, new BN(bridge, 16), depositBlock ] + ).toString('hex') +} + +const getAccountSignature = function(hash, account) { + const signature = web3.eth.sign(account, ethUtil.bufferToHex(ethUtil.toBuffer('Bridge fct: ' + hash.substr(2)))).substr(2) + return { + r: '0x' + signature.slice(0, 64), + s: '0x' + signature.slice(64, 128), + v: web3.toDecimal( '0x' + signature.slice(128, 130) ) + 27 + } +} + +contract('BridgeRGEToken', function(accounts) { + + it("Claim tokens locked in home bridge", async function() { + + + // user need to use the SAME address on both chain. + const user = accounts[1]; + + const tokens = 1000 * 10**6; /* 1K RGE tokens */ + const deposit = 50 * 10**6; /* 1K RGE tokens */ + + const foreign_network = 3; + const foreign_authority = accounts[3]; + + const rge = await RGEToken.deployed(); + const bridge = await RougeBridge.deployed(); + + /* ########## THIS HAPPENS ON THE HOME CHAIN ########## */ + + await rge.giveMeRGE(tokens, {from: user}); + await rge.approve(bridge.address, deposit, {from: user}); + + await bridge.adminBridge(foreign_network, true, foreign_authority) + + const tx = await bridge.deposit(deposit, foreign_network, {from: user, gas: 67431 +20000, gasPrice: web3.toWei(1, "gwei")}) + const depositBlock = tx.receipt.blockNumber; + + const lockHash = createLockHash('locking', user, deposit, foreign_network, bridge.address, depositBlock) + const signLock = getAccountSignature(lockHash, foreign_authority) + const lock_tx = await bridge.lockEscrow(lockHash, user, foreign_network, depositBlock, signLock.v, signLock.r, signLock.s); + const lockBlock = lock_tx.receipt.blockNumber; + const expected_seal = createSealHash('sealing', lockHash, signLock.v, signLock.r, signLock.s, lockBlock); + const seal = await bridge.escrowSeal.call(user, foreign_network, depositBlock); + assert.equal(seal, expected_seal, "we got a correct seal"); + + // owner is create the Auth Hash (to be used on foreign chain) + + const authHash = createAuthHash('authorization', seal); + const signAuth = getAccountSignature(authHash, accounts[0]); + const auth_tx = await bridge.createAuth(authHash, user, foreign_network, depositBlock, signAuth.v, signAuth.r, signAuth.s); + + const event_BridgeAuth_sign = web3.sha3('BridgeAuth(address,uint256,uint256,uint8,bytes32,bytes32,bytes32)') + auth_tx.receipt.logs.forEach( function(e) { + if (e.topics[0] === event_BridgeAuth_sign) { + assert.equal(e.topics[1].slice(26, 66), user.substr(2), "user coherent in log"); + assert.equal(web3.toDecimal( e.topics[2] ), foreign_network , "coherent foreign_network"); + assert.equal(web3.toDecimal( e.topics[3] ), depositBlock, "coherent block number"); + assert.equal(web3.toDecimal(e.data.slice(0, 66)), signAuth.v, "sign v ok"); + assert.equal('0x' + e.data.slice(66, 130), signAuth.r, "sign r ok"); + assert.equal('0x' + e.data.slice(130, 194), signAuth.s, "sign s ok"); + assert.equal('0x' + e.data.slice(194, 260), authHash, "AuthHash ok"); + } + }) + + const user_balance = await rge.balanceOf.call(user); + assert.equal(user_balance.toNumber(), tokens - deposit, "tokens back with user"); + + /* ########## THIS HAPPENS ON THE FOREIGN CHAIN ########## */ + + /* nb: for the sake of test, it's the same chain */ + + const f_rge = await BridgeRGEToken.new(foreign_network, bridge.address, accounts[0], 'Foreign RGE', 'f_RGE', {from: foreign_authority}); + + const user_balance_before = await f_rge.balanceOf.call(user); + assert.equal(user_balance_before.toNumber(), 0, "user has no f_rge tokens to start with"); + + // all these arguments can be read from main chain ledger : LOG EscrowLocked (+ its block number) & LOG BridgeAuth + + const claim_tx = await f_rge.claim(seal, authHash, deposit, depositBlock, lockBlock, signLock.v, signLock.r, signLock.s, signAuth.v, signAuth.r, signAuth.s, {from: user}) + + const user_balance_after = await f_rge.balanceOf.call(user); + assert.equal(user_balance_after.toNumber(), deposit, "user get his rge on foreign chain"); + + const surrender_tx = await f_rge.surrender(deposit, depositBlock, signLock.v, signLock.r, signLock.s, {from: user}) + + const user_balance_post = await f_rge.balanceOf.call(user); + assert.equal(user_balance_post.toNumber(), 0, "user lost his f_rge tokens after surrender"); + + + const hash2 = createUnlockHash('unlocking', user, foreign_network, bridge.address, depositBlock) + const sign2 = getAccountSignature(hash2, foreign_authority) + const repudiate_tx = await f_rge.repudiate(hash2, user, depositBlock, sign2.v, sign2.r, sign2.s, {from: foreign_authority}); + + /* ########## BACK ON HOME CHAIN ########## */ + + /* arguments from unlockEscrow can be read from foreign chain LOG Repudiate */ + + const unlock_tx = await bridge.unlockEscrow(hash2, user, foreign_network, depositBlock, sign2.v, sign2.r, sign2.s); + + const seal_after = await bridge.escrowSeal.call(user, foreign_network, depositBlock); + assert.equal(seal_after, false, "the tokens are not locked anymore in the bridge contract"); + + const withdraw = await bridge.withdraw(foreign_network, depositBlock, {from: user}) + + const bridge_balance_after_withdraw = await rge.balanceOf.call(bridge.address); + assert.equal(bridge_balance_after_withdraw.toNumber(), 0, "empty bridge"); + + const user_balance_after_withdraw = await rge.balanceOf.call(user); + assert.equal(user_balance_after_withdraw.toNumber(), tokens, "tokens back with user"); + + }); + +}); + diff --git a/test/RougeBridge.js b/test/RougeBridge.js new file mode 100644 index 0000000..dabc356 --- /dev/null +++ b/test/RougeBridge.js @@ -0,0 +1,202 @@ + +const abi = require('ethereumjs-abi') +const BN = require('bn.js') +const ethUtil = require('ethereumjs-util') + +const RGEToken = artifacts.require("./TestRGEToken.sol"); +const RougeBridge = artifacts.require("./RougeBridge.sol"); + +const createLockHash = function(msg, user, deposit, foreign_network, bridge, depositBlock) { + return '0x' + abi.soliditySHA3( + [ "string", "address", "uint", "uint", "address", "uint" ], + [ msg, new BN(user, 16), deposit, foreign_network, new BN(bridge, 16), depositBlock ] + ).toString('hex') +} + +const createSealHash = function(msg, hash, v, r, s, lockBlock) { + return '0x' + abi.soliditySHA3( + [ "string", "bytes32", "uint8", "bytes32", "bytes32", "uint" ], + [ msg, hash, v, r, s, lockBlock ] + ).toString('hex') +} + +const createAuthHash = function(msg, hash) { + return '0x' + abi.soliditySHA3( + [ "string", "bytes32" ], [ msg, hash ] + ).toString('hex') +} +const createUnlockHash = function(msg, user, foreign_network, bridge, depositBlock) { + return '0x' + abi.soliditySHA3( + [ "string", "address", "uint", "address", "uint" ], + [ msg, new BN(user, 16), foreign_network, new BN(bridge, 16), depositBlock ] + ).toString('hex') +} + +const getSignature = function(hash, pkey) { + const signature = ethUtil.ecsign(ethUtil.hashPersonalMessage( + ethUtil.toBuffer('Bridge fct: ' + hash.substr(2))), new Buffer(pkey, 'hex') + ) + return { r: ethUtil.bufferToHex(signature.r), s: ethUtil.bufferToHex(signature.s), v: signature.v } +} + +const getAccountSignature = function(hash, account) { + const signature = web3.eth.sign(account, ethUtil.bufferToHex(ethUtil.toBuffer('Bridge fct: ' + hash.substr(2)))).substr(2) + return { + r: '0x' + signature.slice(0, 64), + s: '0x' + signature.slice(64, 128), + v: web3.toDecimal( '0x' + signature.slice(128, 130) ) + 27 + } +} + +contract('RougeBridge', function(accounts) { + + it("bridge has correct parameters", async function() { + + const rge = await RGEToken.deployed(); + const bridge = await RougeBridge.deployed(rge.address); + + const address_rge = await bridge.rge.call(); + assert.equal(address_rge, rge.address, "rge address price is set correctly in bridge"); + + }); + + it("Bridge deposit then withdraw without locked", async function() { + + const user = accounts[1]; + const tokens = 1000 * 10**6; /* 1K RGE tokens */ + const deposit = 50 * 10**6; /* 1K RGE tokens */ + const foreign_network = 3; + + const rge = await RGEToken.deployed(); + const bridge = await RougeBridge.deployed(); + + const user_balance_before = await rge.balanceOf.call(user); + assert.equal(user_balance_before.toNumber(), 0, "user has no rge tokens to start with"); + + await rge.giveMeRGE(tokens, {from: user}); + + const user_balance_post = await rge.balanceOf.call(user); + assert.equal(user_balance_post.toNumber(), tokens, "user has receive tokens"); + + await rge.approve(bridge.address, deposit, {from: user}); + + const is_closed = await bridge.isOpen.call(foreign_network); + assert.equal(is_closed, false, "Bridge closed by default"); + + await bridge.adminBridge(foreign_network, true, '0x0') + + const is_opened = await bridge.isOpen.call(foreign_network); + assert.equal(is_opened, true, "Bridge is now opened"); + + const estimate = await bridge.deposit.estimateGas(deposit, foreign_network, {gas: 200000, from: user}) + // console.log('Base estimate newCampaign => ', estimate) + + const result = await bridge.deposit(deposit, foreign_network, {from: user, gas: 67431 + 20000, gasPrice: web3.toWei(1, "gwei")}) + const depositBlock = result.receipt.blockNumber; + + const event_BridgeDeposit_sign = web3.sha3('BridgeDeposit(address,uint256,uint256,uint256)') + var countlog = 0; + result.receipt.logs.forEach( function(e) { + if (e.topics[0] === event_BridgeDeposit_sign) { + countlog++ + assert.equal(e.topics[1].slice(26, 66), user.substr(2), "user coherent in log"); + assert.equal(web3.toDecimal( e.topics[2] ), foreign_network , "coherent foreign_network"); + assert.equal(web3.toDecimal( e.topics[3] ), depositBlock, "coherent block number"); + } + }) + assert.equal(countlog, 1, "1 log tested"); + assert.equal(result.receipt.cumulativeGasUsed, estimate, "cumulativeGasUsed correctly predicted"); + + const user_balance_after_deposit = await rge.balanceOf.call(user); + assert.equal(user_balance_after_deposit.toNumber(), tokens - deposit, "tokens are in escrow, not user"); + + const bridge_balance_after_deposit = await rge.balanceOf.call(bridge.address); + assert.equal(bridge_balance_after_deposit.toNumber(), deposit, "tokens are in bridge address"); + + const withdraw = await bridge.withdraw(foreign_network, depositBlock, {from: user}) + + const user_balance_after_withdraw = await rge.balanceOf.call(user); + assert.equal(user_balance_after_withdraw.toNumber(), tokens, "tokens back with user"); + + const bridge_balance_after_withdraw = await rge.balanceOf.call(bridge.address); + assert.equal(bridge_balance_after_withdraw.toNumber(), 0, "empty bridge"); + + }); + + /* ********** ********** ********** ********** ********** ********** */ + + it("Bridge deposit locked by seal", async function() { + + const user = accounts[2]; + const tokens = 1000 * 10**6; /* 1K RGE tokens */ + const deposit = 50 * 10**6; /* 1K RGE tokens */ + const foreign_network = 3; + const foreign_authority = '0x955d20aedce1227941b12fa27aa1c77af758e10c'; + const foreign_authority_pkey = 'c81c5128f1051be82c1896906cb1e283e07ec99e8ff53c5d02ea78cf5e7cc790'; + + const rge = await RGEToken.deployed(); + const bridge = await RougeBridge.deployed(); + + await rge.giveMeRGE(tokens, {from: user}); + await rge.approve(bridge.address, deposit, {from: user}); + + await bridge.adminBridge(foreign_network, true, foreign_authority) + + const result = await bridge.deposit(deposit, foreign_network, {from: user, gas: 67431 +20000, gasPrice: web3.toWei(1, "gwei")}) + const depositBlock = result.receipt.blockNumber; + + // foreign chain + owner locking fct + + const hash1 = createLockHash('locking', user, deposit, foreign_network, bridge.address, depositBlock) + const sign1 = getSignature(hash1, foreign_authority_pkey) + const lock_tx = await bridge.lockEscrow(hash1, user, foreign_network, depositBlock, sign1.v, sign1.r, sign1.s); + const lockBlock = lock_tx.receipt.blockNumber; + + const expected_seal = createSealHash('sealing', hash1, sign1.v, sign1.r, sign1.s, lockBlock); + const seal = await bridge.escrowSeal.call(user, foreign_network, depositBlock); + assert.equal(seal, expected_seal, "check seal in that locked tokens"); + + // owner is create the Auth Hash (to be used on foreign chain) + + const authHash = createAuthHash('authorization', seal); + const signAuth = getAccountSignature(authHash, accounts[0]); + const auth_tx = await bridge.createAuth(authHash, user, foreign_network, depositBlock, signAuth.v, signAuth.r, signAuth.s); + + const event_BridgeAuth_sign = web3.sha3('BridgeAuth(address,uint256,uint256,uint8,bytes32,bytes32,bytes32)') + auth_tx.receipt.logs.forEach( function(e) { + if (e.topics[0] === event_BridgeAuth_sign) { + assert.equal(e.topics[1].slice(26, 66), user.substr(2), "user coherent in log"); + assert.equal(web3.toDecimal( e.topics[2] ), foreign_network , "coherent foreign_network"); + assert.equal(web3.toDecimal( e.topics[3] ), depositBlock, "coherent block number"); + assert.equal(web3.toDecimal(e.data.slice(0, 66)), signAuth.v, "sign v ok"); + assert.equal('0x' + e.data.slice(66, 130), signAuth.r, "sign r ok"); + assert.equal('0x' + e.data.slice(130, 194), signAuth.s, "sign s ok"); + assert.equal('0x' + e.data.slice(194, 260), authHash, "AuthHash ok"); + } + }) + + // this should fail as expected + // await bridge.withdraw(foreign_network, depositBlock, {from: user}) + + const hash2 = createUnlockHash('unlocking', user, foreign_network, bridge.address, depositBlock) + const sign2 = getSignature(hash2, foreign_authority_pkey) + const unlock_tx = await bridge.unlockEscrow(hash2, user, foreign_network, depositBlock, sign2.v, sign2.r, sign2.s); + + const seal_after = await bridge.escrowSeal.call(user, foreign_network, depositBlock); + assert.equal(seal_after, false, "the tokens are not locked anymore in the bridge contract"); + + const user_balance_before_withdraw = await rge.balanceOf.call(user); + assert.equal(user_balance_before_withdraw.toNumber(), tokens - deposit, "tokens still locked"); + + const withdraw = await bridge.withdraw(foreign_network, depositBlock, {from: user}) + + const bridge_balance_after_withdraw = await rge.balanceOf.call(bridge.address); + assert.equal(bridge_balance_after_withdraw.toNumber(), 0, "empty bridge"); + + const user_balance_after_withdraw = await rge.balanceOf.call(user); + assert.equal(user_balance_after_withdraw.toNumber(), tokens, "tokens back with user"); + + }); + +}); + diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..3671ac3 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,258 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +bindings@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" + +bip66@^1.1.3: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" + dependencies: + safe-buffer "^5.0.1" + +bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.3, bn.js@^4.4.0, bn.js@^4.8.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browserify-aes@^1.0.6: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-sha3@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/browserify-sha3/-/browserify-sha3-0.0.1.tgz#3ff34a3006ef15c0fb3567e541b91a2340123d11" + dependencies: + js-sha3 "^0.3.1" + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.4: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +drbg.js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/drbg.js/-/drbg.js-1.0.1.tgz#3e36b6c42b37043823cdbc332d58f31e2445480b" + dependencies: + browserify-aes "^1.0.6" + create-hash "^1.1.2" + create-hmac "^1.1.4" + +elliptic@^6.2.3: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +eth-sig-util@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-2.0.2.tgz#bfdb274293620404b7631019dc3d7f17bb2e06f4" + dependencies: + ethereumjs-abi "0.6.5" + ethereumjs-util "^5.1.1" + +ethereumjs-abi@0.6.5, ethereumjs-abi@^0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.5.tgz#5a637ef16ab43473fa72a29ad90871405b3f5241" + dependencies: + bn.js "^4.10.0" + ethereumjs-util "^4.3.0" + +ethereumjs-util@^4.3.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz#3e9428b317eebda3d7260d854fddda954b1f1bc6" + dependencies: + bn.js "^4.8.0" + create-hash "^1.1.2" + keccakjs "^0.2.0" + rlp "^2.0.0" + secp256k1 "^3.0.1" + +ethereumjs-util@^5.1.1, ethereumjs-util@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz#3e0c0d1741471acf1036052d048623dee54ad642" + dependencies: + bn.js "^4.11.0" + create-hash "^1.1.2" + ethjs-util "^0.1.3" + keccak "^1.0.2" + rlp "^2.0.0" + safe-buffer "^5.1.1" + secp256k1 "^3.0.1" + +ethjs-util@^0.1.3: + version "0.1.6" + resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" + dependencies: + is-hex-prefixed "1.0.0" + strip-hex-prefix "1.0.0" + +evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.5" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.5.tgz#e38ab4b85dfb1e0c40fe9265c0e9b54854c23812" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +inherits@^2.0.1, inherits@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +is-hex-prefixed@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz#7d8d37e6ad77e5d127148913c573e082d777f554" + +js-sha3@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.3.1.tgz#86122802142f0828502a0d1dee1d95e253bb0243" + +keccak@^1.0.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/keccak/-/keccak-1.4.0.tgz#572f8a6dbee8e7b3aa421550f9e6408ca2186f80" + dependencies: + bindings "^1.2.1" + inherits "^2.0.3" + nan "^2.2.1" + safe-buffer "^5.1.0" + +keccakjs@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/keccakjs/-/keccakjs-0.2.1.tgz#1d633af907ef305bbf9f2fa616d56c44561dfa4d" + dependencies: + browserify-sha3 "^0.0.1" + sha3 "^1.1.0" + +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +nan@2.10.0, nan@^2.2.1: + version "2.10.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +rlp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.1.0.tgz#e4f9886d5a982174f314543831e36e1a658460f9" + dependencies: + safe-buffer "^5.1.1" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +secp256k1@^3.0.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.5.0.tgz#677d3b8a8e04e1a5fa381a1ae437c54207b738d0" + dependencies: + bindings "^1.2.1" + bip66 "^1.1.3" + bn.js "^4.11.3" + create-hash "^1.1.2" + drbg.js "^1.0.1" + elliptic "^6.2.3" + nan "^2.2.1" + safe-buffer "^5.1.0" + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +sha3@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/sha3/-/sha3-1.2.2.tgz#a66c5098de4c25bc88336ec8b4817d005bca7ba9" + dependencies: + nan "2.10.0" + +strip-hex-prefix@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz#0c5f155fef1151373377de9dbb588da05500e36f" + dependencies: + is-hex-prefixed "1.0.0"