diff --git a/contracts/ERC677BridgeToken.sol b/contracts/ERC677BridgeToken.sol index 74cd52909..e335f2622 100644 --- a/contracts/ERC677BridgeToken.sol +++ b/contracts/ERC677BridgeToken.sol @@ -75,7 +75,7 @@ contract ERC677BridgeToken is } function isContract(address _addr) - private + internal view returns (bool) { diff --git a/contracts/ERC677BridgeTokenRewardable.sol b/contracts/ERC677BridgeTokenRewardable.sol new file mode 100644 index 000000000..41b95947d --- /dev/null +++ b/contracts/ERC677BridgeTokenRewardable.sol @@ -0,0 +1,66 @@ +pragma solidity 0.4.24; + +import "./ERC677BridgeToken.sol"; + + +contract ERC677BridgeTokenRewardable is ERC677BridgeToken { + + address public blockRewardContract; + address public validatorSetContract; + + constructor( + string _name, + string _symbol, + uint8 _decimals + ) public ERC677BridgeToken(_name, _symbol, _decimals) {} + + function setBlockRewardContract(address _blockRewardContract) onlyOwner public { + require(_blockRewardContract != address(0) && isContract(_blockRewardContract)); + blockRewardContract = _blockRewardContract; + } + + function setValidatorSetContract(address _validatorSetContract) onlyOwner public { + require(_validatorSetContract != address(0) && isContract(_validatorSetContract)); + validatorSetContract = _validatorSetContract; + } + + modifier onlyBlockRewardContract() { + require(msg.sender == blockRewardContract); + _; + } + + modifier onlyValidatorSetContract() { + require(msg.sender == validatorSetContract); + _; + } + + function mintReward(address[] _receivers, uint256[] _rewards) external onlyBlockRewardContract { + for (uint256 i = 0; i < _receivers.length; i++) { + address to = _receivers[i]; + uint256 amount = _rewards[i]; + + // Mint `amount` for `to` + totalSupply_ = totalSupply_.add(amount); + balances[to] = balances[to].add(amount); + emit Mint(to, amount); + emit Transfer(address(0), to, amount); + } + } + + function stake(address _staker, uint256 _amount) external onlyValidatorSetContract { + // Transfer `_amount` from `_staker` to `validatorSetContract` + require(_amount <= balances[_staker]); + balances[_staker] = balances[_staker].sub(_amount); + balances[validatorSetContract] = balances[validatorSetContract].add(_amount); + emit Transfer(_staker, validatorSetContract, _amount); + } + + function withdraw(address _staker, uint256 _amount) external onlyValidatorSetContract { + // Transfer `_amount` from `validatorSetContract` to `_staker` + require(_amount <= balances[validatorSetContract]); + balances[validatorSetContract] = balances[validatorSetContract].sub(_amount); + balances[_staker] = balances[_staker].add(_amount); + emit Transfer(validatorSetContract, _staker, _amount); + } + +} diff --git a/contracts/test/ValidatorSet.sol b/contracts/test/ValidatorSet.sol new file mode 100644 index 000000000..d715ffaa8 --- /dev/null +++ b/contracts/test/ValidatorSet.sol @@ -0,0 +1,6 @@ +pragma solidity 0.4.24; + + +contract ValidatorSet { + constructor() {} +} diff --git a/deploy/.env.example b/deploy/.env.example index 06bd47ebe..8145284f6 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -20,7 +20,7 @@ HOME_MIN_AMOUNT_PER_TX=500000000000000000 HOME_REQUIRED_BLOCK_CONFIRMATIONS=1 HOME_GAS_PRICE=1000000000 -#for bridge erc to native mode +#for bridge erc_to_native and native_to_erc mode BLOCK_REWARD_ADDRESS= FOREIGN_RPC_URL=https://sokol.poa.network @@ -39,3 +39,7 @@ REQUIRED_NUMBER_OF_VALIDATORS=1 #If several validators are used, list them separated by space without quotes #E.g. VALIDATORS=0x 0x 0x VALIDATORS=0x + +#for bridge native_to_erc mode +DEPLOY_REWARDABLE_TOKEN=false +DPOS_VALIDATOR_SET_ADDRESS= diff --git a/deploy/README.md b/deploy/README.md index 50b38b473..780627147 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -123,6 +123,16 @@ REQUIRED_NUMBER_OF_VALIDATORS=1 # the Foreign network to confirm that the finalized agreement was transferred # correctly to the Foreign network. VALIDATORS=0x 0x 0x + +# The flag defining whether to use ERC677BridgeTokenRewardable contract instead of +# ERC677BridgeToken. +DEPLOY_REWARDABLE_TOKEN=false +# The address of ValidatorSet contract used by ERC677BridgeTokenRewardable contract. +# Makes sense only when DEPLOY_REWARDABLE_TOKEN=true +DPOS_VALIDATOR_SET_ADDRESS=0x +# The address of BlockReward contract used by ERC677BridgeTokenRewardable contract. +# Makes sense only when DEPLOY_REWARDABLE_TOKEN=true +BLOCK_REWARD_ADDRESS=0x ``` diff --git a/deploy/src/loadEnv.js b/deploy/src/loadEnv.js index 21d5f9fe8..8b3d9f36e 100644 --- a/deploy/src/loadEnv.js +++ b/deploy/src/loadEnv.js @@ -61,7 +61,10 @@ if (BRIDGE_MODE === 'NATIVE_TO_ERC') { BRIDGEABLE_TOKEN_DECIMALS: envalid.num(), FOREIGN_DAILY_LIMIT: bigNumValidator(), FOREIGN_MAX_AMOUNT_PER_TX: bigNumValidator(), - FOREIGN_MIN_AMOUNT_PER_TX: bigNumValidator() + FOREIGN_MIN_AMOUNT_PER_TX: bigNumValidator(), + DEPLOY_REWARDABLE_TOKEN: envalid.bool(), + DPOS_VALIDATOR_SET_ADDRESS: addressValidator(), + BLOCK_REWARD_ADDRESS: addressValidator() } } if (BRIDGE_MODE === 'ERC_TO_ERC') { diff --git a/deploy/src/native_to_erc/foreign.js b/deploy/src/native_to_erc/foreign.js index 39202dc81..0b70c62bf 100644 --- a/deploy/src/native_to_erc/foreign.js +++ b/deploy/src/native_to_erc/foreign.js @@ -6,6 +6,7 @@ const { deployContract, privateKeyToAddress, sendRawTxForeign } = require('../de const { web3Foreign, deploymentPrivateKey, FOREIGN_RPC_URL } = require('../web3') const ERC677BridgeToken = require('../../../build/contracts/ERC677BridgeToken.json') +const ERC677BridgeTokenRewardable = require('../../../build/contracts/ERC677BridgeTokenRewardable.json') const EternalStorageProxy = require('../../../build/contracts/EternalStorageProxy.json') const BridgeValidators = require('../../../build/contracts/BridgeValidators.json') const ForeignBridge = require('../../../build/contracts/ForeignBridgeNativeToErc.json') @@ -27,7 +28,10 @@ const { BRIDGEABLE_TOKEN_SYMBOL, BRIDGEABLE_TOKEN_DECIMALS, HOME_DAILY_LIMIT, - HOME_MAX_AMOUNT_PER_TX + HOME_MAX_AMOUNT_PER_TX, + DEPLOY_REWARDABLE_TOKEN, + BLOCK_REWARD_ADDRESS, + DPOS_VALIDATOR_SET_ADDRESS } = env const DEPLOYMENT_ACCOUNT_ADDRESS = privateKeyToAddress(DEPLOYMENT_ACCOUNT_PRIVATE_KEY) @@ -40,7 +44,7 @@ async function deployForeign() { console.log('\n[Foreign] deploying BRIDGEABLE_TOKEN_SYMBOL token') const erc677bridgeToken = await deployContract( - ERC677BridgeToken, + DEPLOY_REWARDABLE_TOKEN ? ERC677BridgeTokenRewardable : ERC677BridgeToken, [BRIDGEABLE_TOKEN_NAME, BRIDGEABLE_TOKEN_SYMBOL, BRIDGEABLE_TOKEN_DECIMALS], { from: DEPLOYMENT_ACCOUNT_ADDRESS, network: 'foreign', nonce: foreignNonce } ) @@ -212,6 +216,36 @@ async function deployForeign() { assert.strictEqual(Web3Utils.hexToNumber(setBridgeContract.status), 1, 'Transaction Failed') foreignNonce++ + if (DEPLOY_REWARDABLE_TOKEN) { + console.log('\nset BlockReward contract on ERC677BridgeTokenRewardable') + const setBlockRewardContractData = await erc677bridgeToken.methods + .setBlockRewardContract(BLOCK_REWARD_ADDRESS) + .encodeABI({ from: DEPLOYMENT_ACCOUNT_ADDRESS }) + const setBlockRewardContract = await sendRawTxForeign({ + data: setBlockRewardContractData, + nonce: foreignNonce, + to: erc677bridgeToken.options.address, + privateKey: deploymentPrivateKey, + url: FOREIGN_RPC_URL + }) + assert.equal(Web3Utils.hexToNumber(setBlockRewardContract.status), 1, 'Transaction Failed') + foreignNonce++ + + console.log('\nset ValidatorSet contract on ERC677BridgeTokenRewardable') + const setValidatorSetContractData = await erc677bridgeToken.methods + .setValidatorSetContract(DPOS_VALIDATOR_SET_ADDRESS) + .encodeABI({ from: DEPLOYMENT_ACCOUNT_ADDRESS }) + const setValidatorSetContract = await sendRawTxForeign({ + data: setValidatorSetContractData, + nonce: foreignNonce, + to: erc677bridgeToken.options.address, + privateKey: deploymentPrivateKey, + url: FOREIGN_RPC_URL + }) + assert.equal(Web3Utils.hexToNumber(setValidatorSetContract.status), 1, 'Transaction Failed') + foreignNonce++ + } + console.log('transferring ownership of ERC677BridgeToken token to foreignBridge contract') const txOwnershipData = await erc677bridgeToken.methods .transferOwnership(foreignBridgeStorage.options.address) diff --git a/test/mockContracts/ERC677BridgeTokenRewardableMock.sol b/test/mockContracts/ERC677BridgeTokenRewardableMock.sol new file mode 100644 index 000000000..7edad2fc3 --- /dev/null +++ b/test/mockContracts/ERC677BridgeTokenRewardableMock.sol @@ -0,0 +1,22 @@ +pragma solidity 0.4.24; + +import '../../contracts/ERC677BridgeTokenRewardable.sol'; + + +contract ERC677BridgeTokenRewardableMock is ERC677BridgeTokenRewardable { + + constructor( + string _name, + string _symbol, + uint8 _decimals + ) public ERC677BridgeTokenRewardable(_name, _symbol, _decimals) {} + + function setBlockRewardContractMock(address _blockRewardContract) public { + blockRewardContract = _blockRewardContract; + } + + function setValidatorSetContractMock(address _validatorSetContract) public { + validatorSetContract = _validatorSetContract; + } + +} diff --git a/test/poa20_test.js b/test/poa20_test.js index 4e9ed8b77..57e9995b8 100644 --- a/test/poa20_test.js +++ b/test/poa20_test.js @@ -1,5 +1,8 @@ const POA20 = artifacts.require("ERC677BridgeToken.sol"); +const POA20RewardableMock = artifacts.require("./mockContracts/ERC677BridgeTokenRewardableMock"); const ERC677ReceiverTest = artifacts.require("ERC677ReceiverTest.sol") +const BlockRewardTest = artifacts.require("BlockReward.sol") +const ValidatorSetTest = artifacts.require("ValidatorSet.sol") const { ERROR_MSG, ZERO_ADDRESS} = require('./setup'); const Web3Utils = require('web3-utils'); const HomeErcToErcBridge = artifacts.require("HomeBridgeErcToErc.sol"); @@ -13,12 +16,13 @@ const halfEther = web3.toBigNumber(web3.toWei(0.5, "ether")); const executionDailyLimit = oneEther const executionMaxPerTx = halfEther -contract('ERC677BridgeToken', async (accounts) => { +async function testERC677BridgeToken(accounts, rewardable) { let token let owner = accounts[0] const user = accounts[1]; + const tokenContract = rewardable ? POA20RewardableMock : POA20; beforeEach(async () => { - token = await POA20.new("POA ERC20 Foundation", "POA20", 18); + token = await tokenContract.new("POA ERC20 Foundation", "POA20", 18); }) it('default values', async () => { @@ -76,6 +80,160 @@ contract('ERC677BridgeToken', async (accounts) => { }) }) + if (rewardable) { + describe('#blockRewardContract', async() => { + it('can set BlockReward contract', async () => { + const blockRewardContract = await BlockRewardTest.new(); + (await token.blockRewardContract()).should.be.equal(ZERO_ADDRESS); + + await token.setBlockRewardContract(blockRewardContract.address).should.be.fulfilled; + + (await token.blockRewardContract()).should.be.equal(blockRewardContract.address); + }) + + it('only owner can set BlockReward contract', async () => { + const blockRewardContract = await BlockRewardTest.new(); + (await token.blockRewardContract()).should.be.equal(ZERO_ADDRESS); + + await token.setBlockRewardContract(blockRewardContract.address, {from: user }).should.be.rejectedWith(ERROR_MSG); + (await token.blockRewardContract()).should.be.equal(ZERO_ADDRESS); + + await token.setBlockRewardContract(blockRewardContract.address, {from: owner }).should.be.fulfilled; + (await token.blockRewardContract()).should.be.equal(blockRewardContract.address); + }) + + it('fail to set invalid BlockReward contract address', async () => { + const invalidContractAddress = '0xaaB52d66283F7A1D5978bcFcB55721ACB467384b'; + (await token.blockRewardContract()).should.be.equal(ZERO_ADDRESS); + + await token.setBlockRewardContract(invalidContractAddress).should.be.rejectedWith(ERROR_MSG); + (await token.blockRewardContract()).should.be.equal(ZERO_ADDRESS); + + await token.setBlockRewardContract(ZERO_ADDRESS).should.be.rejectedWith(ERROR_MSG); + (await token.blockRewardContract()).should.be.equal(ZERO_ADDRESS); + }) + }) + + describe('#validatorSetContract', async() => { + it('can set ValidatorSet contract', async () => { + const validatorSetContract = await ValidatorSetTest.new(); + (await token.validatorSetContract()).should.be.equal(ZERO_ADDRESS); + + await token.setValidatorSetContract(validatorSetContract.address).should.be.fulfilled; + + (await token.validatorSetContract()).should.be.equal(validatorSetContract.address); + }) + + it('only owner can set ValidatorSet contract', async () => { + const validatorSetContract = await ValidatorSetTest.new(); + (await token.validatorSetContract()).should.be.equal(ZERO_ADDRESS); + + await token.setValidatorSetContract(validatorSetContract.address, {from: user }).should.be.rejectedWith(ERROR_MSG); + (await token.validatorSetContract()).should.be.equal(ZERO_ADDRESS); + + await token.setValidatorSetContract(validatorSetContract.address, {from: owner }).should.be.fulfilled; + (await token.validatorSetContract()).should.be.equal(validatorSetContract.address); + }) + + it('fail to set invalid ValidatorSet contract address', async () => { + const invalidContractAddress = '0xaaB52d66283F7A1D5978bcFcB55721ACB467384b'; + (await token.validatorSetContract()).should.be.equal(ZERO_ADDRESS); + + await token.setValidatorSetContract(invalidContractAddress).should.be.rejectedWith(ERROR_MSG); + (await token.validatorSetContract()).should.be.equal(ZERO_ADDRESS); + + await token.setValidatorSetContract(ZERO_ADDRESS).should.be.rejectedWith(ERROR_MSG); + (await token.validatorSetContract()).should.be.equal(ZERO_ADDRESS); + }) + }) + + describe('#mintReward', async() => { + it('can only be called by BlockReward contract', async () => { + await token.setBlockRewardContractMock(accounts[2]).should.be.fulfilled; + await token.mintReward([], [], {from: user }).should.be.rejectedWith(ERROR_MSG); + await token.mintReward([], [], {from: accounts[2] }).should.be.fulfilled; + }) + it('should increase totalSupply and balances', async () => { + const user1 = accounts[1]; + const user2 = accounts[2]; + const user3 = accounts[3]; + + assert.equal(await token.totalSupply(), 0); + (await token.balanceOf(user1)).should.be.bignumber.equal(0); + (await token.balanceOf(user2)).should.be.bignumber.equal(0); + (await token.balanceOf(user3)).should.be.bignumber.equal(0); + + await token.setBlockRewardContractMock(accounts[4]).should.be.fulfilled; + await token.mintReward([user1, user2, user3], [100, 200, 300], {from: accounts[4] }).should.be.fulfilled; + + assert.equal(await token.totalSupply(), 600); + (await token.balanceOf(user1)).should.be.bignumber.equal(100); + (await token.balanceOf(user2)).should.be.bignumber.equal(200); + (await token.balanceOf(user3)).should.be.bignumber.equal(300); + }) + }) + + describe('#stake', async() => { + it('can only be called by ValidatorSet contract', async () => { + await token.setBlockRewardContractMock(accounts[2]).should.be.fulfilled; + await token.mintReward([user], [100], {from: accounts[2] }).should.be.fulfilled; + await token.setValidatorSetContractMock(accounts[3]).should.be.fulfilled; + await token.stake(user, 100, {from: accounts[4] }).should.be.rejectedWith(ERROR_MSG); + await token.stake(user, 100, {from: accounts[3] }).should.be.fulfilled; + }) + it('should revert if user doesn\'t have enough balance', async () => { + await token.setBlockRewardContractMock(accounts[2]).should.be.fulfilled; + await token.mintReward([user], [99], {from: accounts[2] }).should.be.fulfilled; + (await token.balanceOf(user)).should.be.bignumber.equal(99); + await token.setValidatorSetContractMock(accounts[3]).should.be.fulfilled; + await token.stake(user, 100, {from: accounts[3] }).should.be.rejectedWith(ERROR_MSG); + }) + it('should decrease user\'s balance and increase ValidatorSet\'s balance', async () => { + await token.setBlockRewardContractMock(accounts[2]).should.be.fulfilled; + await token.mintReward([user], [100], {from: accounts[2] }).should.be.fulfilled; + (await token.balanceOf(user)).should.be.bignumber.equal(100); + (await token.balanceOf(accounts[3])).should.be.bignumber.equal(0); + await token.setValidatorSetContractMock(accounts[3]).should.be.fulfilled; + await token.stake(user, 100, {from: accounts[3] }).should.be.fulfilled; + (await token.balanceOf(user)).should.be.bignumber.equal(0); + (await token.balanceOf(accounts[3])).should.be.bignumber.equal(100); + }) + }) + + describe('#withdraw', async() => { + it('can only be called by ValidatorSet contract', async () => { + await token.setBlockRewardContractMock(accounts[2]).should.be.fulfilled; + await token.mintReward([user], [100], {from: accounts[2] }).should.be.fulfilled; + await token.setValidatorSetContractMock(accounts[3]).should.be.fulfilled; + await token.stake(user, 100, {from: accounts[3] }).should.be.fulfilled; + await token.withdraw(user, 100, {from: accounts[4] }).should.be.rejectedWith(ERROR_MSG); + await token.withdraw(user, 100, {from: accounts[3] }).should.be.fulfilled; + }) + it('should revert if ValidatorSet doesn\'t have enough balance', async () => { + await token.setBlockRewardContractMock(accounts[2]).should.be.fulfilled; + await token.mintReward([user], [100], {from: accounts[2] }).should.be.fulfilled; + (await token.balanceOf(user)).should.be.bignumber.equal(100); + await token.setValidatorSetContractMock(accounts[3]).should.be.fulfilled; + await token.stake(user, 100, {from: accounts[3] }).should.be.fulfilled; + await token.withdraw(user, 101, {from: accounts[3] }).should.be.rejectedWith(ERROR_MSG); + await token.withdraw(user, 100, {from: accounts[3] }).should.be.fulfilled; + }) + it('should decrease ValidatorSet\'s balance and increase user\'s balance', async () => { + await token.setBlockRewardContractMock(accounts[2]).should.be.fulfilled; + await token.mintReward([user], [100], {from: accounts[2] }).should.be.fulfilled; + (await token.balanceOf(user)).should.be.bignumber.equal(100); + (await token.balanceOf(accounts[3])).should.be.bignumber.equal(0); + await token.setValidatorSetContractMock(accounts[3]).should.be.fulfilled; + await token.stake(user, 100, {from: accounts[3] }).should.be.fulfilled; + (await token.balanceOf(user)).should.be.bignumber.equal(0); + (await token.balanceOf(accounts[3])).should.be.bignumber.equal(100); + await token.withdraw(user, 60, {from: accounts[3] }).should.be.fulfilled; + (await token.balanceOf(user)).should.be.bignumber.equal(60); + (await token.balanceOf(accounts[3])).should.be.bignumber.equal(40); + }) + }) + } + describe('#mint', async() => { it('can mint by owner', async () => { (await token.totalSupply()).should.be.bignumber.equal(0); @@ -262,7 +420,7 @@ contract('ERC677BridgeToken', async (accounts) => { it('can take send ERC20 tokens', async ()=> { const owner = accounts[0]; const halfEther = web3.toBigNumber(web3.toWei(0.5, "ether")); - let tokenSecond = await POA20.new("Roman Token", "RST", 18); + let tokenSecond = await tokenContract.new("Roman Token", "RST", 18); await tokenSecond.mint(accounts[0], halfEther).should.be.fulfilled; halfEther.should.be.bignumber.equal(await tokenSecond.balanceOf(accounts[0])) @@ -273,7 +431,6 @@ contract('ERC677BridgeToken', async (accounts) => { await token.claimTokens(tokenSecond.address, accounts[3], {from: owner}); '0'.should.be.bignumber.equal(await tokenSecond.balanceOf(token.address)) halfEther.should.be.bignumber.equal(await tokenSecond.balanceOf(accounts[3])) - }) }) describe('#transfer', async () => { @@ -295,7 +452,7 @@ contract('ERC677BridgeToken', async (accounts) => { logs[0].event.should.be.equal("Transfer") }) it('if transfer called on contract, still works even if onTokenTransfer doesnot exist', async () => { - const someContract = await POA20.new("Some", "Token", 18); + const someContract = await tokenContract.new("Some", "Token", 18); await token.mint(user, 2, {from: owner }).should.be.fulfilled; const tokenTransfer = await token.transfer(someContract.address, 1, {from: user}).should.be.fulfilled; const tokenTransfer2 = await token.transfer(accounts[0], 1, {from: user}).should.be.fulfilled; @@ -303,7 +460,14 @@ contract('ERC677BridgeToken', async (accounts) => { (await token.balanceOf(user)).should.be.bignumber.equal(0); tokenTransfer.logs[0].event.should.be.equal("Transfer") tokenTransfer2.logs[0].event.should.be.equal("Transfer") - }) }) +} + +contract('ERC677BridgeToken', async (accounts) => { + await testERC677BridgeToken(accounts, false); +}) + +contract('ERC677BridgeTokenRewardable', async (accounts) => { + await testERC677BridgeToken(accounts, true); })