From ee2a48d37acb3a6b089c2e5b6b6dc4554d6592bb Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Mon, 16 Mar 2020 15:42:47 -0400 Subject: [PATCH 01/39] Basic multiplier --- eth-contracts/contracts/staking/Staking.sol | 40 +++++++++++++++------ 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index 52b498d900f..2274318e484 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -30,6 +30,13 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { Checkpointing.History claimHistory; } + // Multiplier used to increase funds + Checkpointing.History stakeMultiplier; + + // Internal multiplier + // TODO: Confirm this is necessary + // uint256 internalStakeMultiplier; + ERC20 internal stakingToken; mapping (address => Account) internal accounts; Checkpointing.History internal totalStakedHistory; @@ -52,9 +59,16 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { initialized(); stakingToken = ERC20(_stakingToken); treasuryAddress = _treasuryAddress; + // Initialize claim values to zero, disabling claim prior to initial funding currentClaimBlock = 0; currentClaimableAmount = 0; + + // Initialize multiplier history value + stakeMultiplier.add64(getBlockNumber64(), 1000); + + // Initialize internal multiplier + // internalStakeMultiplier = 1000; } /* External functions */ @@ -238,6 +252,15 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { return true; } + + // WORKING + + function getCurrentStakeMultiplier() public view isInitialized returns (uint256) { + return stakeMultiplier.getLatestValue(); + } + + // END WORKING + /** * @notice Get last time `_accountAddress` modified its staked balance * @param _accountAddress Account requesting for @@ -291,7 +314,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { */ function totalStakedFor(address _accountAddress) public view returns (uint256) { // we assume it's not possible to stake in the future - return accounts[_accountAddress].stakedHistory.getLatestValue(); + return (accounts[_accountAddress].stakedHistory.getLatestValue()).mul(stakeMultiplier.getLatestValue()); } /** @@ -303,14 +326,6 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { return totalStakedHistory.getLatestValue(); } - /* - function multicall(bytes[] _calls) public { - for(uint i = 0; i < _calls.length; i++) { - require(address(this).delegatecall(_calls[i]), ERROR_MULTICALL_DELEGATECALL); - } - } - */ - /* Internal functions */ function _stakeFor( @@ -322,8 +337,11 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // staking 0 tokens is invalid require(_amount > 0, ERROR_AMOUNT_ZERO); - // checkpoint updated staking balance - _modifyStakeBalance(_stakeAccount, _amount, true); + // Adjust amount by internal stake multiplier + uint internalStakeAmount = _amount.div(stakeMultiplier.getLatestValue()); + + // Checkpoint updated staking balance + _modifyStakeBalance(_stakeAccount, internalStakeAmount, true); // checkpoint total supply _modifyTotalStaked(_amount, true); From bfdce017ce7d5fad048ae02ef318ab9e8a1b091d Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Mon, 16 Mar 2020 23:16:14 -0400 Subject: [PATCH 02/39] DIRTY STATE - fix unstake amount --- eth-contracts/contracts/staking/Staking.sol | 30 ++- eth-contracts/scripts/truffle-test.sh | 4 +- eth-contracts/test/delegationManager.test.js | 217 +++++++++++++++++++ eth-contracts/test/serviceProvider.test.js | 10 +- eth-contracts/test/staking.test.js | 187 ++++++++++++---- 5 files changed, 388 insertions(+), 60 deletions(-) create mode 100644 eth-contracts/test/delegationManager.test.js diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index 2274318e484..c4ecb91207a 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -54,6 +54,10 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { uint256 amountClaimed ); + event Test( + uint256 test, + string msg); + function initialize(address _stakingToken, address _treasuryAddress) external onlyInit { require(isContract(_stakingToken), ERROR_TOKEN_NOT_CONTRACT); initialized(); @@ -81,21 +85,29 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // Stake tokens for msg.sender from treasuryAddress // Transfer tokens from msg.sender to current contract // Increase treasuryAddress stake value - _stakeFor( - treasuryAddress, - msg.sender, - _amount, - bytes("")); - // Update current claim information - currentClaimBlock = getBlockNumber(); - currentClaimableAmount = totalStakedFor(treasuryAddress); + // Update multiplier + // Update total stake + uint256 currentMultiplier = stakeMultiplier.getLatestValue(); + uint256 totalStake = totalStakedHistory.getLatestValue(); + + // Calculate and distribute funds by updating multiplier + uint256 multiplierDifference = (currentMultiplier.mul(_amount)).div(totalStake); + uint256 newMultiplier = currentMultiplier.add(multiplierDifference); + stakeMultiplier.add64(getBlockNumber64(), newMultiplier); + + // pull tokens into Staking contract from caller + stakingToken.safeTransferFrom(msg.sender, address(this), _amount); + + // Increase total supply by input amount + _modifyTotalStaked(_amount, true); } /** * @notice Allows reward claiming for service providers */ function makeClaim() external isInitialized { + require(false, 'Disabled for dev'); require(msg.sender != treasuryAddress, "Treasury cannot claim staking reward"); require(accounts[msg.sender].stakedHistory.history.length > 0, "Stake required to claim"); @@ -293,7 +305,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { * @return The amount of tokens staked by the account at the given block number */ function totalStakedForAt(address _accountAddress, uint256 _blockNumber) external view returns (uint256) { - return accounts[_accountAddress].stakedHistory.get(_blockNumber); + return (accounts[_accountAddress].stakedHistory.get(_blockNumber)).mul(stakeMultiplier.get(_blockNumber)); } /** diff --git a/eth-contracts/scripts/truffle-test.sh b/eth-contracts/scripts/truffle-test.sh index 3eabe1e653a..1b269624901 100755 --- a/eth-contracts/scripts/truffle-test.sh +++ b/eth-contracts/scripts/truffle-test.sh @@ -47,5 +47,5 @@ else fi # tear down -docker rm -f audius_ganache_cli_eth_contracts_test -rm -rf ./build/ +# docker rm -f audius_ganache_cli_eth_contracts_test +# rm -rf ./build/ diff --git a/eth-contracts/test/delegationManager.test.js b/eth-contracts/test/delegationManager.test.js new file mode 100644 index 00000000000..2b8a4af0d10 --- /dev/null +++ b/eth-contracts/test/delegationManager.test.js @@ -0,0 +1,217 @@ +import * as _lib from './_lib/lib.js' + +const encodeCall = require('./encodeCall') +const Registry = artifacts.require('Registry') +const AudiusToken = artifacts.require('AudiusToken') +const OwnedUpgradeabilityProxy = artifacts.require('OwnedUpgradeabilityProxy') +const ServiceProviderFactory = artifacts.require('ServiceProviderFactory') +const ServiceProviderStorage = artifacts.require('ServiceProviderStorage') +const Staking = artifacts.require('Staking') + +const fromBn = n => parseInt(n.valueOf(), 10) + +const getTokenBalance = async (token, account) => fromBn(await token.balanceOf(account)) +const claimBlockDiff = 46000 + +const toWei = (aud) => { + let amountInAudWei = web3.utils.toWei( + aud.toString(), + 'ether' + ) + + let amountInAudWeiBN = web3.utils.toBN(amountInAudWei) + return amountInAudWeiBN +} + +const fromWei = (wei) => { + return web3.utils.fromWei(wei) +} + +const getTokenBalance2 = async (token, account) => fromWei(await token.balanceOf(account)) + +const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') +const serviceProviderStorageKey = web3.utils.utf8ToHex('ServiceProviderStorage') +const serviceProviderFactoryKey = web3.utils.utf8ToHex('ServiceProviderFactory') + +const testDiscProvType = web3.utils.utf8ToHex('discovery-provider') +const testCreatorNodeType = web3.utils.utf8ToHex('creator-node') +const testEndpoint = 'https://localhost:5000' +const testEndpoint1 = 'https://localhost:5001' + +const MIN_STAKE_AMOUNT = 10 + +// 1000 AUD converted to AUDWei, multiplying by 10^18 +const INITIAL_BAL = toWei(1000) +const DEFAULT_AMOUNT = toWei(100) +const MAX_STAKE_AMOUNT = DEFAULT_AMOUNT * 100 + +contract('ServiceProvider test', async (accounts) => { + let treasuryAddress = accounts[0] + let proxyOwner = treasuryAddress + let proxy + let impl0 + let staking + let token + let registry + let stakingAddress + let tokenAddress + let serviceProviderStorage + let serviceProviderFactory + + beforeEach(async () => { + registry = await Registry.new() + + proxy = await OwnedUpgradeabilityProxy.new({ from: proxyOwner }) + + // Deploy registry + await registry.addContract(ownedUpgradeabilityProxyKey, proxy.address) + + token = await AudiusToken.new({ from: treasuryAddress }) + tokenAddress = token.address + // console.log(`AudiusToken Address : ${tokenAddress}`) + let initialTokenBal = fromBn(await token.balanceOf(accounts[0])) + // console.log(`AudiusToken Balance: ${initialTokenBal}`) + impl0 = await Staking.new() + + // Create initialization data + let initializeData = encodeCall( + 'initialize', + ['address', 'address'], + [token.address, treasuryAddress]) + + // Initialize staking contract + await proxy.upgradeToAndCall( + impl0.address, + initializeData, + { from: proxyOwner }) + + staking = await Staking.at(proxy.address) + stakingAddress = staking.address + + // Deploy sp storage + serviceProviderStorage = await ServiceProviderStorage.new(registry.address) + await registry.addContract(serviceProviderStorageKey, serviceProviderStorage.address) + + // Deploy sp factory + serviceProviderFactory = await ServiceProviderFactory.new( + registry.address, + ownedUpgradeabilityProxyKey, + serviceProviderStorageKey) + + await registry.addContract(serviceProviderFactoryKey, serviceProviderFactory.address) + + // Permission sp factory as caller, from the proxy owner address + // (which happens to equal treasury in this test case) + await staking.setStakingOwnerAddress(serviceProviderFactory.address, { from: proxyOwner }) + + // Transfer 1000 tokens to accounts[1] + await token.transfer(accounts[1], INITIAL_BAL, { from: treasuryAddress }) + }) + + /* Helper functions */ + + const registerServiceProvider = async (type, endpoint, amount, account) => { + // Approve staking transfer + await token.approve(stakingAddress, amount, { from: account }) + + let tx = await serviceProviderFactory.register( + type, + endpoint, + amount, + account, + { from: account }) + + let args = tx.logs.find(log => log.event === 'RegisteredServiceProvider').args + args.stakedAmountInt = fromBn(args._stakeAmount) + args.spID = fromBn(args._spID) + return args + } + + const getStakeAmountForAccount = async (account) => { + return fromBn(await staking.totalStakedFor(account)) + } + + const getServiceProviderIdsFromAddress = async (account, type) => { + // Query and convert returned IDs to bignumber + let ids = ( + await serviceProviderFactory.getServiceProviderIdsFromAddress(account, type) + ).map(x => fromBn(x)) + return ids + } + + const serviceProviderIDRegisteredToAccount = async (account, type, id) => { + let ids = await getServiceProviderIdsFromAddress(account, type) + let newIdFound = ids.includes(id) + return newIdFound + } + + describe('Registration flow', () => { + let regTx + const stakerAccount = accounts[1] + + beforeEach(async () => { + let initialBal = await token.balanceOf(stakerAccount) + + // 1st endpoint for stakerAccount = https://localhost:5000 + // Total Stake = 120 AUD + regTx = await registerServiceProvider( + testDiscProvType, + testEndpoint, + DEFAULT_AMOUNT, + stakerAccount) + + // Confirm event has correct amount + assert.equal(regTx.stakedAmountInt, DEFAULT_AMOUNT) + + // Confirm balance updated for tokens + let finalBal = await token.balanceOf(stakerAccount) + assert.isTrue(initialBal.eq(finalBal.add(DEFAULT_AMOUNT)), 'Expect funds to be transferred') + + let newIdFound = await serviceProviderIDRegisteredToAccount( + stakerAccount, + testDiscProvType, + regTx.spID) + assert.isTrue( + newIdFound, + 'Expected to find newly registered ID associated with this account') + + let stakedAmount = await getStakeAmountForAccount(stakerAccount) + assert.equal( + stakedAmount, + DEFAULT_AMOUNT, + 'Expect default stake amount') + + let spTypeInfo = await serviceProviderFactory.getServiceStakeInfo(testDiscProvType) + let typeMin = fromWei(spTypeInfo[0]) + let typeMax = fromWei(spTypeInfo[1]) + + // Validate min stake requirements + // Both current account bounds and single testDiscProvType bounds expected to be equal + let bounds = await serviceProviderFactory.getAccountStakeBounds(stakerAccount) + let accountMin = fromWei(bounds[0]) + let accountMax = fromWei(bounds[1]) + assert.equal( + typeMin, + accountMin, + 'Expect account min to equal sp type 1 min') + assert.equal( + typeMax, + accountMax, + 'Expect account max to equal sp type 1 max') + }) + + /* + * For fixing this + */ + it('sandbox', async () => { + // Confirm staking contract has correct amt + assert.equal(await getStakeAmountForAccount(stakerAccount), DEFAULT_AMOUNT) + + // let delegator = accounts[2] + // Transfer 1000 tokens to delegator + // await token.transfer(delegator, INITIAL_BAL, { from: treasuryAddress }) + let multiplier = await staking.getCurrentStakeMultiplier() + console.log(fromBn(multiplier)) + }) + }) +}) diff --git a/eth-contracts/test/serviceProvider.test.js b/eth-contracts/test/serviceProvider.test.js index 23ad11f4adc..ba25dbbfc02 100644 --- a/eth-contracts/test/serviceProvider.test.js +++ b/eth-contracts/test/serviceProvider.test.js @@ -375,7 +375,14 @@ contract('ServiceProvider test', async (accounts) => { */ it('confirm registered stake', async () => { // Confirm staking contract has correct amt - assert.equal(await getStakeAmountForAccount(stakerAccount), DEFAULT_AMOUNT) + let multiplier = await staking.getCurrentStakeMultiplier() + console.log(fromBn(multiplier)) + console.log('----') + + let returnedValue = await getStakeAmountForAccount(stakerAccount) + console.log(returnedValue) + console.log('----') + assert.equal(returnedValue, DEFAULT_AMOUNT) }) /* @@ -410,6 +417,7 @@ contract('ServiceProvider test', async (accounts) => { }) it('fails to register duplicate endpoint w/same account', async () => { + process.exit() // Attempt to register dup endpoint with the same account await _lib.assertRevert( registerServiceProvider( diff --git a/eth-contracts/test/staking.test.js b/eth-contracts/test/staking.test.js index 4bbbda44404..86321d5c099 100644 --- a/eth-contracts/test/staking.test.js +++ b/eth-contracts/test/staking.test.js @@ -8,6 +8,18 @@ const fromBn = n => parseInt(n.valueOf(), 10) const getTokenBalance = async (token, account) => fromBn(await token.balanceOf(account)) const claimBlockDiff = 46000 +const toWei = (aud) => { + let amountInAudWei = web3.utils.toWei( + aud.toString(), + 'ether' + ) + + let amountInAudWeiBN = web3.utils.toBN(amountInAudWei) + return amountInAudWeiBN +} + +const DEFAULT_AMOUNT = toWei(120) + contract('Staking test', async (accounts) => { let treasuryAddress = accounts[0] let testStakingCallerAddress = accounts[6] // Dummy stand in for sp factory in actual deployment @@ -19,26 +31,29 @@ contract('Staking test', async (accounts) => { let stakingAddress let tokenAddress - const DEFAULT_AMOUNT = 120 const DEFAULT_TREASURY_AMOUNT = DEFAULT_AMOUNT * 10 const EMPTY_STRING = '' const approveAndStake = async (amount, staker) => { + // console.log(`approving ${amount} - ${staker}`) + let tokenBal = await token.balanceOf(staker) // allow Staking app to move owner tokens await token.approve(stakingAddress, amount, { from: staker }) // stake tokens - await staking.stakeFor( + // console.log(`staking ${amount} - ${staker}, ${tokenBal} tokens`) + let tx = await staking.stakeFor( staker, amount, web3.utils.utf8ToHex(EMPTY_STRING), { from: testStakingCallerAddress }) + // console.log(`staked ${amount} - ${staker}`) } const approveAndFundNewClaim = async (amount, from) => { // allow Staking app to move owner tokens await token.approve(stakingAddress, amount, { from }) let receipt = await staking.fundNewClaim(amount, { from }) - // console.log(receipt) + console.log(receipt) return receipt } @@ -98,13 +113,16 @@ contract('Staking test', async (accounts) => { // Permission test address as caller await staking.setStakingOwnerAddress(testStakingCallerAddress, { from: treasuryAddress }) }) + /* it('has correct initial state', async () => { assert.equal(await staking.token(), tokenAddress, 'Token is wrong') assert.equal((await staking.totalStaked()).valueOf(), 0, 'Initial total staked amount should be zero') assert.equal(await staking.supportsHistory(), true, 'history support should match') }) + */ + /* it('stakes', async () => { const owner = accounts[0] const initialOwnerBalance = await getTokenBalance(token, owner) @@ -116,11 +134,16 @@ contract('Staking test', async (accounts) => { const finalStakingBalance = await getTokenBalance(token, stakingAddress) assert.equal(finalOwnerBalance, initialOwnerBalance - DEFAULT_AMOUNT, 'owner balance should match') assert.equal(finalStakingBalance, initialStakingBalance + DEFAULT_AMOUNT, 'Staking app balance should match') - assert.equal((await staking.totalStakedFor(owner)).valueOf(), DEFAULT_AMOUNT, 'staked value should match') + + console.log(fromBn(await staking.totalStakedFor(owner))) + console.log(DEFAULT_AMOUNT) + + assert.equal(fromBn(await staking.totalStakedFor(owner)), DEFAULT_AMOUNT, 'staked value should match') // total stake assert.equal((await staking.totalStaked()).toString(), DEFAULT_AMOUNT, 'Total stake should match') }) - + */ +/* it('unstakes', async () => { const owner = accounts[0] const initialOwnerBalance = await getTokenBalance(token, owner) @@ -132,7 +155,7 @@ contract('Staking test', async (accounts) => { const tmpStakingBalance = await getTokenBalance(token, stakingAddress) assert.equal(tmpOwnerBalance, initialOwnerBalance - DEFAULT_AMOUNT, 'owner balance should match') assert.equal(tmpStakingBalance, initialStakingBalance + DEFAULT_AMOUNT, 'Staking app balance should match') - assert.equal((await staking.totalStakedFor(owner)).valueOf(), DEFAULT_AMOUNT, 'staked value should match') + assert.equal(fromBn(await staking.totalStakedFor(owner)), DEFAULT_AMOUNT, 'staked value should match') // total stake assert.equal((await staking.totalStaked()).toString(), DEFAULT_AMOUNT, 'Total stake should match') @@ -165,33 +188,67 @@ contract('Staking test', async (accounts) => { it('supports history', async () => { assert.equal(await staking.supportsHistory(), true, 'It should support History') }) + */ + it('stake wth single account', async () => { + let staker = accounts[1] + // Transfer 1000 tokens to accounts[1] + await token.transfer(staker, DEFAULT_AMOUNT, { from: treasuryAddress }) + + let initialTotalStaked = await staking.totalStaked() + console.log('iniital stake -------------') + console.log(initialTotalStaked) + + await token.approve(stakingAddress, DEFAULT_AMOUNT, { from: staker }) + // stake tokens + console.log(`staking ${DEFAULT_AMOUNT} - ${staker}`) + + let tx = await staking.stakeFor( + staker, + DEFAULT_AMOUNT, + web3.utils.utf8ToHex(EMPTY_STRING), + { from: testStakingCallerAddress }) + + + let finalTotalStaked = parseInt(await staking.totalStaked()) + console.log(finalTotalStaked) + return + }) + /* it('stake with multiple accounts', async () => { // Transfer 1000 tokens to accounts[1] - await token.transfer(accounts[1], 1000, { from: treasuryAddress }) + await token.transfer(accounts[1], DEFAULT_AMOUNT, { from: treasuryAddress }) // Transfer 1000 tokens to accounts[2] - await token.transfer(accounts[2], 1000, { from: treasuryAddress }) + await token.transfer(accounts[2], DEFAULT_AMOUNT, { from: treasuryAddress }) let initialTotalStaked = await staking.totalStaked() + console.log('1') // Stake w/both accounts await approveAndStake(DEFAULT_AMOUNT, accounts[1]) + console.log('2') await approveAndStake(DEFAULT_AMOUNT, accounts[2]) + console.log('1a') + let finalTotalStaked = parseInt(await staking.totalStaked()) + console.log('1b') let expectedFinalStake = parseInt(initialTotalStaked + (DEFAULT_AMOUNT * 2)) + console.log('2') assert.equal( finalTotalStaked, expectedFinalStake, 'Final stake amount must be 2x default stake') }) + */ + /* it('slash functioning as expected', async () => { // Transfer 1000 tokens to accounts[1] // Transfer 1000 tokens to accounts[2] - await token.transfer(accounts[1], 1000, { from: treasuryAddress }) - await token.transfer(accounts[2], 1000, { from: treasuryAddress }) + await token.transfer(accounts[1], DEFAULT_AMOUNT, { from: treasuryAddress }) + await token.transfer(accounts[2], DEFAULT_AMOUNT, { from: treasuryAddress }) // Stake w/both accounts await approveAndStake(DEFAULT_AMOUNT, accounts[1]) @@ -202,13 +259,16 @@ contract('Staking test', async (accounts) => { let initialStakeAmount = parseInt(await staking.totalStakedFor(accounts[1])) assert.equal(initialStakeAmount, DEFAULT_AMOUNT) - let slashAmount = DEFAULT_AMOUNT / 2 + let slashAmount = web3.utils.toBn(DEFAULT_AMOUNT / 2) + console.log(slashAmount) + // Slash 1/2 value from treasury await slashAccount( slashAmount, accounts[1], treasuryAddress) + console.log('finished slash') // Confirm staked value let finalStakeAmt = parseInt(await staking.totalStakedFor(accounts[1])) assert.equal(finalStakeAmt, DEFAULT_AMOUNT / 2) @@ -219,7 +279,9 @@ contract('Staking test', async (accounts) => { await staking.totalStaked(), 'Total amount unchanged') }) + */ + /* it('new fund cycle resets block difference', async () => { // Stake initial treasury amount from treasury address const spAccount1 = accounts[1] @@ -227,26 +289,22 @@ contract('Staking test', async (accounts) => { const spAccount3 = accounts[3] const funderAccount = accounts[4] - // Transfer 1000 tokens to accounts[1] - await token.transfer(spAccount1, 1000, { from: treasuryAddress }) - - // Transfer 1000 tokens to accounts[2] - await token.transfer(spAccount2, 1000, { from: treasuryAddress }) + let initialStake = DEFAULT_AMOUNT - // Transfer 1000 tokens to accounts[3] - await token.transfer(spAccount3, 1000, { from: treasuryAddress }) + // Transfer tokens to accounts 1, 2, 3 + await token.transfer(spAccount1, DEFAULT_AMOUNT, { from: treasuryAddress }) + await token.transfer(spAccount2, DEFAULT_AMOUNT, { from: treasuryAddress }) + await token.transfer(spAccount3, DEFAULT_AMOUNT, { from: treasuryAddress }) // Transfer 100,000 tokens to funder await token.transfer(funderAccount, 10000, { from: treasuryAddress }) - // Stake with account 1 + // Stake with accounts 1, 2, 3 await approveAndStake(DEFAULT_AMOUNT, spAccount1) - // let initialSP1Stake = await getStakedAmountForAcct(spAccount1) - - // Stake with account 2 await approveAndStake(DEFAULT_AMOUNT, spAccount2) + await approveAndStake(DEFAULT_AMOUNT, spAccount3) - const INITIAL_FUNDING = 5000 + const INITIAL_FUNDING = toWei(5000) await approveAndFundNewClaim(INITIAL_FUNDING, funderAccount) // Claim for acct 1 @@ -277,6 +335,7 @@ contract('Staking test', async (accounts) => { claimResult2.amountClaimedInt > 0, 'Expect successful claim after re-funding') }) + */ it('multiple claims, single fund cycle', async () => { // Stake initial treasury amount from treasury address @@ -285,26 +344,18 @@ contract('Staking test', async (accounts) => { const spAccount3 = accounts[3] const funderAccount = accounts[4] - // Transfer 1000 tokens to accounts[1] - await token.transfer(spAccount1, 1000, { from: treasuryAddress }) - - // Transfer 1000 tokens to accounts[2] - await token.transfer(spAccount2, 1000, { from: treasuryAddress }) - - // Transfer 1000 tokens to accounts[3] - await token.transfer(spAccount3, 1000, { from: treasuryAddress }) - - // Transfer 100,000 tokens to funder - await token.transfer(funderAccount, 10000, { from: treasuryAddress }) + // TODO: Confirm that historic values for a single account can be recalculated by validating with blocknumber + // + // Transfer DEFAULLT tokens to accts 1, 2, 3 + await token.transfer(spAccount1, DEFAULT_AMOUNT, { from: treasuryAddress }) + await token.transfer(spAccount2, DEFAULT_AMOUNT, { from: treasuryAddress }) + await token.transfer(spAccount3, DEFAULT_AMOUNT, { from: treasuryAddress }) // Stake with account 1 // Treasury - 120 await approveAndStake(DEFAULT_AMOUNT, spAccount1) - let initiallyStakedAcct1 = await getStakedAmountForAcct(spAccount1) - // Stake with account 2 - // Treasury - 240 await approveAndStake(DEFAULT_AMOUNT, spAccount2) let currentTotalStake = parseInt(await staking.totalStaked()) @@ -318,24 +369,64 @@ contract('Staking test', async (accounts) => { // Confirm claim can not be made prior to claim funded _lib.assertRevert(claimStakingReward(spAccount1)) + let FIRST_CLAIM_FUND = toWei(120) + + // Transfer 120AUD tokens to staking contract + await token.transfer(funderAccount, FIRST_CLAIM_FUND, { from: treasuryAddress }) + // Transfer funds for claiming to contract - const INITIAL_FUNDING = 5000 - await approveAndFundNewClaim(INITIAL_FUNDING, funderAccount) + await approveAndFundNewClaim(FIRST_CLAIM_FUND, funderAccount) + + // Sanity checks + // let multiplier = await staking.getCurrentStakeMultiplier() + // console.log(fromBn(multiplier)) + // console.log('----') + // console.log(await getStakedAmountForAcct(spAccount1)) + // End sanity checks + + // Initial val should be first claim fund / 2 + let expectedValueAfterFirstFund = DEFAULT_AMOUNT.add(FIRST_CLAIM_FUND.div(web3.utils.toBN(2))) + console.log(`Expected val ${expectedValueAfterFirstFund}`) + + // Confirm value added to account 1 + // let claimResult1 = await claimStakingReward(spAccount1) + let acct1StakeAfterFund = await getStakedAmountForAcct(spAccount1) + console.log(`Acct 1 stake ${acct1StakeAfterFund}`) + assert.isTrue( + expectedValueAfterFirstFund.eq(web3.utils.toBN(acct1StakeAfterFund)), + 'Expected stake increase for acct 1') + + /* + assert.equal( + expectedValueAfterFirstFund, + acct1StakeAfterFund, + 'Expected value') + ) + */ - // Stake with account 3, A few blocks after claimBlock has been set + let acct2StakeAfterFund = await getStakedAmountForAcct(spAccount2) + console.log(`Acct 2 stake ${acct2StakeAfterFund}`) + + // Stake with account 3, after funding round await approveAndStake(DEFAULT_AMOUNT, spAccount3) + let acct3Stake = await getStakedAmountForAcct(spAccount3) + console.log(`Acct 3 stake ${acct3Stake}`) + + return + // Claim for acct 1 - let claimResult1 = await claimStakingReward(spAccount1) - let finallyStakedAcct1 = (await staking.totalStakedFor(spAccount1)).valueOf() + // let claimResult1 = await claimStakingReward(spAccount1) + // let finallyStakedAcct1 = (await staking.totalStakedFor(spAccount1)).valueOf() // Confirm claim without block diff reached reverts - _lib.assertRevert(claimStakingReward(spAccount1)) + // _lib.assertRevert(claimStakingReward(spAccount1)) - assert.equal( - parseInt(initiallyStakedAcct1) + claimResult1.amountClaimedInt, - finallyStakedAcct1, - 'Expected stake amount') + + // assert.equal( + // parseInt(initiallyStakedAcct1) + claimResult1.amountClaimedInt, + // finallyStakedAcct1, + // 'Expected stake amount') // Confirm no claim is awarded to account 3 when requested since no stake was present let claimAccount3 = await claimStakingReward(spAccount3) @@ -343,7 +434,7 @@ contract('Staking test', async (accounts) => { // Claim for account 2 let claimAccount2 = await claimStakingReward(spAccount2) - assert.equal(INITIAL_FUNDING / 2, claimAccount2.amountClaimedInt, 'Expected 1/2 initial treasury value claim for account 2') + // assert.equal(INITIAL_FUNDING / 2, claimAccount2.amountClaimedInt, 'Expected 1/2 initial treasury value claim for account 2') let finalTreasuryValue = parseInt(await staking.totalStakedFor(treasuryAddress)) assert.equal( From b98b5545f54d12720a2c5962f05f384298dcbafc Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Tue, 17 Mar 2020 13:31:40 -0400 Subject: [PATCH 03/39] more progress More tests working More tests passing Slash test now working Some additional verification Some more cleanup --- eth-contracts/contracts/staking/Staking.sol | 27 ++- eth-contracts/test/delegationManager.test.js | 217 ------------------- eth-contracts/test/staking.test.js | 185 ++++------------ 3 files changed, 65 insertions(+), 364 deletions(-) delete mode 100644 eth-contracts/test/delegationManager.test.js diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index c4ecb91207a..122fb305d95 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -108,6 +108,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { */ function makeClaim() external isInitialized { require(false, 'Disabled for dev'); + require(msg.sender != treasuryAddress, "Treasury cannot claim staking reward"); require(accounts[msg.sender].stakedHistory.history.length > 0, "Stake required to claim"); @@ -154,9 +155,12 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // unstaking 0 tokens is not allowed require(_amount > 0, ERROR_AMOUNT_ZERO); + // Adjust amount by internal stake multiplier + uint internalSlashAmount = _amount.div(stakeMultiplier.getLatestValue()); + // transfer slashed funds to treasury address // reduce stake balance for address being slashed - _transfer(_slashAddress, treasuryAddress, _amount); + _transfer(_slashAddress, treasuryAddress, internalSlashAmount); } /** @@ -206,8 +210,11 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // unstaking 0 tokens is not allowed require(_amount > 0, ERROR_AMOUNT_ZERO); + // Adjust amount by internal stake multiplier + uint internalStakeAmount = _amount.div(stakeMultiplier.getLatestValue()); + // checkpoint updated staking balance - _modifyStakeBalance(msg.sender, _amount, false); + _modifyStakeBalance(msg.sender, internalStakeAmount, false); // checkpoint total supply _modifyTotalStaked(_amount, false); @@ -232,8 +239,11 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // unstaking 0 tokens is not allowed require(_amount > 0, ERROR_AMOUNT_ZERO); + // Adjust amount by internal stake multiplier + uint internalStakeAmount = _amount.div(stakeMultiplier.getLatestValue()); + // checkpoint updated staking balance - _modifyStakeBalance(_accountAddress, _amount, false); + _modifyStakeBalance(_accountAddress, internalStakeAmount, false); // checkpoint total supply _modifyTotalStaked(_amount, false); @@ -368,14 +378,19 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { _data); } + // Note that _by value has been adjusted for the stake multiplier prior to getting passed in function _modifyStakeBalance(address _accountAddress, uint256 _by, bool _increase) internal { - uint256 currentStake = totalStakedFor(_accountAddress); + // currentInternalStake represents the internal stake value, without multiplier adjustment + uint256 currentInternalStake = accounts[_accountAddress].stakedHistory.getLatestValue(); uint256 newStake; if (_increase) { - newStake = currentStake.add(_by); + newStake = currentInternalStake.add(_by); } else { - newStake = currentStake.sub(_by); + require( + currentInternalStake >= _by, + 'Cannot decrease greater than current balance'); + newStake = currentInternalStake.sub(_by); } // add new value to account history diff --git a/eth-contracts/test/delegationManager.test.js b/eth-contracts/test/delegationManager.test.js deleted file mode 100644 index 2b8a4af0d10..00000000000 --- a/eth-contracts/test/delegationManager.test.js +++ /dev/null @@ -1,217 +0,0 @@ -import * as _lib from './_lib/lib.js' - -const encodeCall = require('./encodeCall') -const Registry = artifacts.require('Registry') -const AudiusToken = artifacts.require('AudiusToken') -const OwnedUpgradeabilityProxy = artifacts.require('OwnedUpgradeabilityProxy') -const ServiceProviderFactory = artifacts.require('ServiceProviderFactory') -const ServiceProviderStorage = artifacts.require('ServiceProviderStorage') -const Staking = artifacts.require('Staking') - -const fromBn = n => parseInt(n.valueOf(), 10) - -const getTokenBalance = async (token, account) => fromBn(await token.balanceOf(account)) -const claimBlockDiff = 46000 - -const toWei = (aud) => { - let amountInAudWei = web3.utils.toWei( - aud.toString(), - 'ether' - ) - - let amountInAudWeiBN = web3.utils.toBN(amountInAudWei) - return amountInAudWeiBN -} - -const fromWei = (wei) => { - return web3.utils.fromWei(wei) -} - -const getTokenBalance2 = async (token, account) => fromWei(await token.balanceOf(account)) - -const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') -const serviceProviderStorageKey = web3.utils.utf8ToHex('ServiceProviderStorage') -const serviceProviderFactoryKey = web3.utils.utf8ToHex('ServiceProviderFactory') - -const testDiscProvType = web3.utils.utf8ToHex('discovery-provider') -const testCreatorNodeType = web3.utils.utf8ToHex('creator-node') -const testEndpoint = 'https://localhost:5000' -const testEndpoint1 = 'https://localhost:5001' - -const MIN_STAKE_AMOUNT = 10 - -// 1000 AUD converted to AUDWei, multiplying by 10^18 -const INITIAL_BAL = toWei(1000) -const DEFAULT_AMOUNT = toWei(100) -const MAX_STAKE_AMOUNT = DEFAULT_AMOUNT * 100 - -contract('ServiceProvider test', async (accounts) => { - let treasuryAddress = accounts[0] - let proxyOwner = treasuryAddress - let proxy - let impl0 - let staking - let token - let registry - let stakingAddress - let tokenAddress - let serviceProviderStorage - let serviceProviderFactory - - beforeEach(async () => { - registry = await Registry.new() - - proxy = await OwnedUpgradeabilityProxy.new({ from: proxyOwner }) - - // Deploy registry - await registry.addContract(ownedUpgradeabilityProxyKey, proxy.address) - - token = await AudiusToken.new({ from: treasuryAddress }) - tokenAddress = token.address - // console.log(`AudiusToken Address : ${tokenAddress}`) - let initialTokenBal = fromBn(await token.balanceOf(accounts[0])) - // console.log(`AudiusToken Balance: ${initialTokenBal}`) - impl0 = await Staking.new() - - // Create initialization data - let initializeData = encodeCall( - 'initialize', - ['address', 'address'], - [token.address, treasuryAddress]) - - // Initialize staking contract - await proxy.upgradeToAndCall( - impl0.address, - initializeData, - { from: proxyOwner }) - - staking = await Staking.at(proxy.address) - stakingAddress = staking.address - - // Deploy sp storage - serviceProviderStorage = await ServiceProviderStorage.new(registry.address) - await registry.addContract(serviceProviderStorageKey, serviceProviderStorage.address) - - // Deploy sp factory - serviceProviderFactory = await ServiceProviderFactory.new( - registry.address, - ownedUpgradeabilityProxyKey, - serviceProviderStorageKey) - - await registry.addContract(serviceProviderFactoryKey, serviceProviderFactory.address) - - // Permission sp factory as caller, from the proxy owner address - // (which happens to equal treasury in this test case) - await staking.setStakingOwnerAddress(serviceProviderFactory.address, { from: proxyOwner }) - - // Transfer 1000 tokens to accounts[1] - await token.transfer(accounts[1], INITIAL_BAL, { from: treasuryAddress }) - }) - - /* Helper functions */ - - const registerServiceProvider = async (type, endpoint, amount, account) => { - // Approve staking transfer - await token.approve(stakingAddress, amount, { from: account }) - - let tx = await serviceProviderFactory.register( - type, - endpoint, - amount, - account, - { from: account }) - - let args = tx.logs.find(log => log.event === 'RegisteredServiceProvider').args - args.stakedAmountInt = fromBn(args._stakeAmount) - args.spID = fromBn(args._spID) - return args - } - - const getStakeAmountForAccount = async (account) => { - return fromBn(await staking.totalStakedFor(account)) - } - - const getServiceProviderIdsFromAddress = async (account, type) => { - // Query and convert returned IDs to bignumber - let ids = ( - await serviceProviderFactory.getServiceProviderIdsFromAddress(account, type) - ).map(x => fromBn(x)) - return ids - } - - const serviceProviderIDRegisteredToAccount = async (account, type, id) => { - let ids = await getServiceProviderIdsFromAddress(account, type) - let newIdFound = ids.includes(id) - return newIdFound - } - - describe('Registration flow', () => { - let regTx - const stakerAccount = accounts[1] - - beforeEach(async () => { - let initialBal = await token.balanceOf(stakerAccount) - - // 1st endpoint for stakerAccount = https://localhost:5000 - // Total Stake = 120 AUD - regTx = await registerServiceProvider( - testDiscProvType, - testEndpoint, - DEFAULT_AMOUNT, - stakerAccount) - - // Confirm event has correct amount - assert.equal(regTx.stakedAmountInt, DEFAULT_AMOUNT) - - // Confirm balance updated for tokens - let finalBal = await token.balanceOf(stakerAccount) - assert.isTrue(initialBal.eq(finalBal.add(DEFAULT_AMOUNT)), 'Expect funds to be transferred') - - let newIdFound = await serviceProviderIDRegisteredToAccount( - stakerAccount, - testDiscProvType, - regTx.spID) - assert.isTrue( - newIdFound, - 'Expected to find newly registered ID associated with this account') - - let stakedAmount = await getStakeAmountForAccount(stakerAccount) - assert.equal( - stakedAmount, - DEFAULT_AMOUNT, - 'Expect default stake amount') - - let spTypeInfo = await serviceProviderFactory.getServiceStakeInfo(testDiscProvType) - let typeMin = fromWei(spTypeInfo[0]) - let typeMax = fromWei(spTypeInfo[1]) - - // Validate min stake requirements - // Both current account bounds and single testDiscProvType bounds expected to be equal - let bounds = await serviceProviderFactory.getAccountStakeBounds(stakerAccount) - let accountMin = fromWei(bounds[0]) - let accountMax = fromWei(bounds[1]) - assert.equal( - typeMin, - accountMin, - 'Expect account min to equal sp type 1 min') - assert.equal( - typeMax, - accountMax, - 'Expect account max to equal sp type 1 max') - }) - - /* - * For fixing this - */ - it('sandbox', async () => { - // Confirm staking contract has correct amt - assert.equal(await getStakeAmountForAccount(stakerAccount), DEFAULT_AMOUNT) - - // let delegator = accounts[2] - // Transfer 1000 tokens to delegator - // await token.transfer(delegator, INITIAL_BAL, { from: treasuryAddress }) - let multiplier = await staking.getCurrentStakeMultiplier() - console.log(fromBn(multiplier)) - }) - }) -}) diff --git a/eth-contracts/test/staking.test.js b/eth-contracts/test/staking.test.js index 86321d5c099..ad5ee2a9da5 100644 --- a/eth-contracts/test/staking.test.js +++ b/eth-contracts/test/staking.test.js @@ -53,7 +53,6 @@ contract('Staking test', async (accounts) => { // allow Staking app to move owner tokens await token.approve(stakingAddress, amount, { from }) let receipt = await staking.fundNewClaim(amount, { from }) - console.log(receipt) return receipt } @@ -113,60 +112,45 @@ contract('Staking test', async (accounts) => { // Permission test address as caller await staking.setStakingOwnerAddress(testStakingCallerAddress, { from: treasuryAddress }) }) - /* - it('has correct initial state', async () => { assert.equal(await staking.token(), tokenAddress, 'Token is wrong') assert.equal((await staking.totalStaked()).valueOf(), 0, 'Initial total staked amount should be zero') assert.equal(await staking.supportsHistory(), true, 'history support should match') }) - */ - /* - it('stakes', async () => { - const owner = accounts[0] - const initialOwnerBalance = await getTokenBalance(token, owner) - const initialStakingBalance = await getTokenBalance(token, stakingAddress) - - await approveAndStake(DEFAULT_AMOUNT, treasuryAddress) - - const finalOwnerBalance = await getTokenBalance(token, owner) - const finalStakingBalance = await getTokenBalance(token, stakingAddress) - assert.equal(finalOwnerBalance, initialOwnerBalance - DEFAULT_AMOUNT, 'owner balance should match') - assert.equal(finalStakingBalance, initialStakingBalance + DEFAULT_AMOUNT, 'Staking app balance should match') - - console.log(fromBn(await staking.totalStakedFor(owner))) - console.log(DEFAULT_AMOUNT) - - assert.equal(fromBn(await staking.totalStakedFor(owner)), DEFAULT_AMOUNT, 'staked value should match') - // total stake - assert.equal((await staking.totalStaked()).toString(), DEFAULT_AMOUNT, 'Total stake should match') - }) - */ -/* it('unstakes', async () => { - const owner = accounts[0] - const initialOwnerBalance = await getTokenBalance(token, owner) + const staker = accounts[2] + // Transfer default tokens to account[2] + await token.transfer(staker, DEFAULT_AMOUNT, { from: treasuryAddress }) + + const initialOwnerBalance = await getTokenBalance(token, staker) const initialStakingBalance = await getTokenBalance(token, stakingAddress) - await approveAndStake(DEFAULT_AMOUNT, treasuryAddress) + await approveAndStake(DEFAULT_AMOUNT, staker) - const tmpOwnerBalance = await getTokenBalance(token, owner) + const tmpOwnerBalance = await getTokenBalance(token, staker) const tmpStakingBalance = await getTokenBalance(token, stakingAddress) - assert.equal(tmpOwnerBalance, initialOwnerBalance - DEFAULT_AMOUNT, 'owner balance should match') + assert.equal(tmpOwnerBalance, initialOwnerBalance - DEFAULT_AMOUNT, 'staker balance should match') assert.equal(tmpStakingBalance, initialStakingBalance + DEFAULT_AMOUNT, 'Staking app balance should match') - assert.equal(fromBn(await staking.totalStakedFor(owner)), DEFAULT_AMOUNT, 'staked value should match') + assert.equal(fromBn(await staking.totalStakedFor(staker)), DEFAULT_AMOUNT, 'staked value should match') // total stake assert.equal((await staking.totalStaked()).toString(), DEFAULT_AMOUNT, 'Total stake should match') + // unstake half of current value + let unstakeAmount = DEFAULT_AMOUNT.div(web3.utils.toBN(2)) + // Unstake default amount - await staking.unstake(DEFAULT_AMOUNT, web3.utils.utf8ToHex(EMPTY_STRING)) + await staking.unstake( + DEFAULT_AMOUNT, + web3.utils.utf8ToHex(EMPTY_STRING), + { from: staker } + ) - const finalOwnerBalance = await getTokenBalance(token, owner) + const finalOwnerBalance = await getTokenBalance(token, staker) const finalStakingBalance = await getTokenBalance(token, stakingAddress) - assert.equal(finalOwnerBalance, initialOwnerBalance, 'initial and final owner balance should match') + assert.equal(finalOwnerBalance, initialOwnerBalance, 'initial and final staker balance should match') assert.equal(finalStakingBalance, initialStakingBalance, 'initial and final staking balance should match') }) @@ -188,65 +172,52 @@ contract('Staking test', async (accounts) => { it('supports history', async () => { assert.equal(await staking.supportsHistory(), true, 'It should support History') }) - */ - it('stake wth single account', async () => { + + it('stake with single account', async () => { let staker = accounts[1] // Transfer 1000 tokens to accounts[1] await token.transfer(staker, DEFAULT_AMOUNT, { from: treasuryAddress }) - - let initialTotalStaked = await staking.totalStaked() - console.log('iniital stake -------------') - console.log(initialTotalStaked) - await token.approve(stakingAddress, DEFAULT_AMOUNT, { from: staker }) - // stake tokens - console.log(`staking ${DEFAULT_AMOUNT} - ${staker}`) + // stake tokens let tx = await staking.stakeFor( staker, DEFAULT_AMOUNT, web3.utils.utf8ToHex(EMPTY_STRING), { from: testStakingCallerAddress }) - let finalTotalStaked = parseInt(await staking.totalStaked()) - console.log(finalTotalStaked) - return + assert.equal( + finalTotalStaked, + DEFAULT_AMOUNT, + 'Final total stake amount must be default stake') + assert.equal( + fromBn(await staking.totalStakedFor(staker)), + DEFAULT_AMOUNT, + 'Account stake value should match default stake') }) - /* it('stake with multiple accounts', async () => { - // Transfer 1000 tokens to accounts[1] + // Transfer 1000 tokens to accounts[1], accounts[2] await token.transfer(accounts[1], DEFAULT_AMOUNT, { from: treasuryAddress }) - - // Transfer 1000 tokens to accounts[2] await token.transfer(accounts[2], DEFAULT_AMOUNT, { from: treasuryAddress }) let initialTotalStaked = await staking.totalStaked() - console.log('1') // Stake w/both accounts await approveAndStake(DEFAULT_AMOUNT, accounts[1]) - console.log('2') await approveAndStake(DEFAULT_AMOUNT, accounts[2]) - console.log('1a') - let finalTotalStaked = parseInt(await staking.totalStaked()) - console.log('1b') let expectedFinalStake = parseInt(initialTotalStaked + (DEFAULT_AMOUNT * 2)) - console.log('2') assert.equal( finalTotalStaked, expectedFinalStake, 'Final stake amount must be 2x default stake') }) - */ - /* it('slash functioning as expected', async () => { - // Transfer 1000 tokens to accounts[1] - // Transfer 1000 tokens to accounts[2] + // Transfer 1000 tokens to accounts[1], accounts[2] await token.transfer(accounts[1], DEFAULT_AMOUNT, { from: treasuryAddress }) await token.transfer(accounts[2], DEFAULT_AMOUNT, { from: treasuryAddress }) @@ -255,12 +226,10 @@ contract('Staking test', async (accounts) => { await approveAndStake(DEFAULT_AMOUNT, accounts[2]) let initialTotalStake = parseInt(await staking.totalStaked()) - let initialStakeAmount = parseInt(await staking.totalStakedFor(accounts[1])) assert.equal(initialStakeAmount, DEFAULT_AMOUNT) - let slashAmount = web3.utils.toBn(DEFAULT_AMOUNT / 2) - console.log(slashAmount) + let slashAmount = web3.utils.toBN(DEFAULT_AMOUNT / 2) // Slash 1/2 value from treasury await slashAccount( @@ -268,7 +237,6 @@ contract('Staking test', async (accounts) => { accounts[1], treasuryAddress) - console.log('finished slash') // Confirm staked value let finalStakeAmt = parseInt(await staking.totalStakedFor(accounts[1])) assert.equal(finalStakeAmt, DEFAULT_AMOUNT / 2) @@ -279,7 +247,6 @@ contract('Staking test', async (accounts) => { await staking.totalStaked(), 'Total amount unchanged') }) - */ /* it('new fund cycle resets block difference', async () => { @@ -377,94 +344,30 @@ contract('Staking test', async (accounts) => { // Transfer funds for claiming to contract await approveAndFundNewClaim(FIRST_CLAIM_FUND, funderAccount) - // Sanity checks - // let multiplier = await staking.getCurrentStakeMultiplier() - // console.log(fromBn(multiplier)) - // console.log('----') - // console.log(await getStakedAmountForAcct(spAccount1)) - // End sanity checks - // Initial val should be first claim fund / 2 let expectedValueAfterFirstFund = DEFAULT_AMOUNT.add(FIRST_CLAIM_FUND.div(web3.utils.toBN(2))) - console.log(`Expected val ${expectedValueAfterFirstFund}`) // Confirm value added to account 1 // let claimResult1 = await claimStakingReward(spAccount1) let acct1StakeAfterFund = await getStakedAmountForAcct(spAccount1) - console.log(`Acct 1 stake ${acct1StakeAfterFund}`) assert.isTrue( - expectedValueAfterFirstFund.eq(web3.utils.toBN(acct1StakeAfterFund)), + expectedValueAfterFirstFund.eq( + web3.utils.toBN(acct1StakeAfterFund)), 'Expected stake increase for acct 1') - /* - assert.equal( - expectedValueAfterFirstFund, - acct1StakeAfterFund, - 'Expected value') - ) - */ - let acct2StakeAfterFund = await getStakedAmountForAcct(spAccount2) - console.log(`Acct 2 stake ${acct2StakeAfterFund}`) + assert.isTrue( + expectedValueAfterFirstFund.eq( + web3.utils.toBN(acct2StakeAfterFund)), + 'Expected stake increase for acct 2') // Stake with account 3, after funding round await approveAndStake(DEFAULT_AMOUNT, spAccount3) - + // Confirm updated multiplier adjusts stake accurately let acct3Stake = await getStakedAmountForAcct(spAccount3) - console.log(`Acct 3 stake ${acct3Stake}`) - - return - - // Claim for acct 1 - // let claimResult1 = await claimStakingReward(spAccount1) - // let finallyStakedAcct1 = (await staking.totalStakedFor(spAccount1)).valueOf() - - // Confirm claim without block diff reached reverts - // _lib.assertRevert(claimStakingReward(spAccount1)) - - - // assert.equal( - // parseInt(initiallyStakedAcct1) + claimResult1.amountClaimedInt, - // finallyStakedAcct1, - // 'Expected stake amount') - - // Confirm no claim is awarded to account 3 when requested since no stake was present - let claimAccount3 = await claimStakingReward(spAccount3) - assert.equal(claimAccount3.amountClaimedInt, 0, 'Zero funds expected for sp 3 staking') - - // Claim for account 2 - let claimAccount2 = await claimStakingReward(spAccount2) - // assert.equal(INITIAL_FUNDING / 2, claimAccount2.amountClaimedInt, 'Expected 1/2 initial treasury value claim for account 2') - - let finalTreasuryValue = parseInt(await staking.totalStakedFor(treasuryAddress)) - assert.equal( - finalTreasuryValue, - 0, - 'Expect fund exhaustion from treasury') - }) - - /* - context('History', async () => { - const owner = accounts[0] - - // TODO: Consume mock and implement below history tests - - it('has correct "last staked for"', async () => { - const blockNumber = await staking.getBlockNumberPublic() - const lastStaked = blockNumber + 5 - await staking.setBlockNumber(lastStaked) - await approveAndStake() - assert.equal(await staking.lastStakedFor(owner), lastStaked, 'Last staked for should match') - }) - - it('has correct "total staked for at"', async () => { - const beforeBlockNumber = await staking.getBlockNumberPublic() - const lastStaked = beforeBlockNumber + 5 - await staking.setBlockNumber(lastStaked) - await approveAndStake() - assert.equal(await staking.totalStakedForAt(owner, beforeBlockNumber), 0, "Staked for at before staking should match") - assert.equal(await staking.totalStakedForAt(owner, lastStaked), DEFAULT_AMOUNT, "Staked for after staking should match") - }) + assert.isTrue( + DEFAULT_AMOUNT.eq( + web3.utils.toBN(acct3Stake)), + 'Expected stake increase for acct 2') }) - */ }) From 530e36994d4c7d047db27383638f25b118905a65 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Tue, 17 Mar 2020 16:45:54 -0400 Subject: [PATCH 04/39] Test cleanup --- eth-contracts/test/staking.test.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/eth-contracts/test/staking.test.js b/eth-contracts/test/staking.test.js index ad5ee2a9da5..1b2dca68d51 100644 --- a/eth-contracts/test/staking.test.js +++ b/eth-contracts/test/staking.test.js @@ -333,9 +333,6 @@ contract('Staking test', async (accounts) => { expectedTotalStake, 'Final stake amount must be 2x default stake') - // Confirm claim can not be made prior to claim funded - _lib.assertRevert(claimStakingReward(spAccount1)) - let FIRST_CLAIM_FUND = toWei(120) // Transfer 120AUD tokens to staking contract @@ -348,7 +345,6 @@ contract('Staking test', async (accounts) => { let expectedValueAfterFirstFund = DEFAULT_AMOUNT.add(FIRST_CLAIM_FUND.div(web3.utils.toBN(2))) // Confirm value added to account 1 - // let claimResult1 = await claimStakingReward(spAccount1) let acct1StakeAfterFund = await getStakedAmountForAcct(spAccount1) assert.isTrue( expectedValueAfterFirstFund.eq( From bc32af371e34308bef498794432fe19a2113c1d4 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Tue, 17 Mar 2020 16:46:34 -0400 Subject: [PATCH 05/39] more cleanup --- eth-contracts/test/staking.test.js | 64 ------------------------------ 1 file changed, 64 deletions(-) diff --git a/eth-contracts/test/staking.test.js b/eth-contracts/test/staking.test.js index 1b2dca68d51..d3531b3fd2c 100644 --- a/eth-contracts/test/staking.test.js +++ b/eth-contracts/test/staking.test.js @@ -56,14 +56,6 @@ contract('Staking test', async (accounts) => { return receipt } - const claimStakingReward = async (from) => { - let tx = await staking.makeClaim({ from }) - let claimArgs = tx.logs.find(log => log.event === 'Claimed').args - claimArgs.amountClaimedInt = claimArgs.amountClaimed.toNumber() - claimArgs.blockNumber = tx.receipt.blockNumber - return claimArgs - } - const getLatestBlock = async () => { let block = await web3.eth.getBlock('latest') // console.log(`Latest block: ${block.number}`) @@ -248,62 +240,6 @@ contract('Staking test', async (accounts) => { 'Total amount unchanged') }) - /* - it('new fund cycle resets block difference', async () => { - // Stake initial treasury amount from treasury address - const spAccount1 = accounts[1] - const spAccount2 = accounts[2] - const spAccount3 = accounts[3] - const funderAccount = accounts[4] - - let initialStake = DEFAULT_AMOUNT - - // Transfer tokens to accounts 1, 2, 3 - await token.transfer(spAccount1, DEFAULT_AMOUNT, { from: treasuryAddress }) - await token.transfer(spAccount2, DEFAULT_AMOUNT, { from: treasuryAddress }) - await token.transfer(spAccount3, DEFAULT_AMOUNT, { from: treasuryAddress }) - - // Transfer 100,000 tokens to funder - await token.transfer(funderAccount, 10000, { from: treasuryAddress }) - - // Stake with accounts 1, 2, 3 - await approveAndStake(DEFAULT_AMOUNT, spAccount1) - await approveAndStake(DEFAULT_AMOUNT, spAccount2) - await approveAndStake(DEFAULT_AMOUNT, spAccount3) - - const INITIAL_FUNDING = toWei(5000) - await approveAndFundNewClaim(INITIAL_FUNDING, funderAccount) - - // Claim for acct 1 - let claimResult1 = await claimStakingReward(spAccount1) - // console.dir(claimResult1, { depth: 5 }) - let block1 = claimResult1.blockNumber - - // Confirm claim without block diff reached reverts - _lib.assertRevert(claimStakingReward(spAccount1)) - - let block2 = await getLatestBlock() - assert.isTrue( - (block2 - block1) < claimBlockDiff, - 'Block difference not yet met') - - // Re-fund claim - await approveAndFundNewClaim(INITIAL_FUNDING, funderAccount) - - // Confirm claim works despite block difference not being met, due to new funding - let claimResult2 = await claimStakingReward(spAccount1) - let block3 = claimResult2.blockNumber - - assert.isTrue( - (block3 - block1) < claimBlockDiff, - 'Claim block difference not met') - - assert.isTrue( - claimResult2.amountClaimedInt > 0, - 'Expect successful claim after re-funding') - }) - */ - it('multiple claims, single fund cycle', async () => { // Stake initial treasury amount from treasury address const spAccount1 = accounts[1] From f704a16acc1535cd3afa37ede9e85a3ca06cfc41 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Tue, 17 Mar 2020 17:36:03 -0400 Subject: [PATCH 06/39] Some minor cleanup --- eth-contracts/contracts/staking/Staking.sol | 60 ++------------------- eth-contracts/test/staking.test.js | 8 +-- 2 files changed, 5 insertions(+), 63 deletions(-) diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index 122fb305d95..d1cd331809a 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -33,10 +33,6 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // Multiplier used to increase funds Checkpointing.History stakeMultiplier; - // Internal multiplier - // TODO: Confirm this is necessary - // uint256 internalStakeMultiplier; - ERC20 internal stakingToken; mapping (address => Account) internal accounts; Checkpointing.History internal totalStakedHistory; @@ -70,9 +66,6 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // Initialize multiplier history value stakeMultiplier.add64(getBlockNumber64(), 1000); - - // Initialize internal multiplier - // internalStakeMultiplier = 1000; } /* External functions */ @@ -82,12 +75,8 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { */ function fundNewClaim(uint256 _amount) external isInitialized { // TODO: Add additional require statements here... - // Stake tokens for msg.sender from treasuryAddress - // Transfer tokens from msg.sender to current contract - // Increase treasuryAddress stake value - // Update multiplier - // Update total stake + // Update multiplier, total stake uint256 currentMultiplier = stakeMultiplier.getLatestValue(); uint256 totalStake = totalStakedHistory.getLatestValue(); @@ -103,45 +92,6 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { _modifyTotalStaked(_amount, true); } - /** - * @notice Allows reward claiming for service providers - */ - function makeClaim() external isInitialized { - require(false, 'Disabled for dev'); - - require(msg.sender != treasuryAddress, "Treasury cannot claim staking reward"); - require(accounts[msg.sender].stakedHistory.history.length > 0, "Stake required to claim"); - - require(currentClaimBlock > 0, "Claim block must be initialized"); - require(currentClaimableAmount > 0, "Claimable amount must be initialized"); - - if (accounts[msg.sender].claimHistory.history.length > 0) { - uint256 lastClaimedBlock = accounts[msg.sender].claimHistory.lastUpdated(); - // Require a minimum block difference alloted to be ~1 week of blocks - // Note that a new claim funding after the latest claim for this staker overrides the minimum block difference - require( - lastClaimedBlock < currentClaimBlock, - "Minimum block difference not met"); - } - - uint256 claimBlockTotalStake = totalStakedHistory.get(currentClaimBlock); - uint256 treasuryStakeAtClaimBlock = accounts[treasuryAddress].stakedHistory.get(currentClaimBlock); - uint256 claimantStakeAtClaimBlock = accounts[msg.sender].stakedHistory.get(currentClaimBlock); - uint256 totalServiceProviderStakeAtClaimBlock = claimBlockTotalStake.sub(treasuryStakeAtClaimBlock); - - uint256 claimedValue = (claimantStakeAtClaimBlock.mul(currentClaimableAmount)).div(totalServiceProviderStakeAtClaimBlock); - - // Transfer value from treasury to claimant if > 0 is claimed - if (claimedValue > 0) { - _transfer(treasuryAddress, msg.sender, claimedValue); - } - - // Update claim history even if no value claimed - accounts[msg.sender].claimHistory.add64(getBlockNumber64(), claimedValue); - - emit Claimed(msg.sender, claimedValue); - } - /** * @notice Slashes `_amount` tokens from _slashAddress * Controlled by treasury address @@ -274,15 +224,13 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { return true; } - - // WORKING - + /** + * @return Current stake multiplier + */ function getCurrentStakeMultiplier() public view isInitialized returns (uint256) { return stakeMultiplier.getLatestValue(); } - // END WORKING - /** * @notice Get last time `_accountAddress` modified its staked balance * @param _accountAddress Account requesting for diff --git a/eth-contracts/test/staking.test.js b/eth-contracts/test/staking.test.js index d3531b3fd2c..dbb6c9bd50c 100644 --- a/eth-contracts/test/staking.test.js +++ b/eth-contracts/test/staking.test.js @@ -35,18 +35,15 @@ contract('Staking test', async (accounts) => { const EMPTY_STRING = '' const approveAndStake = async (amount, staker) => { - // console.log(`approving ${amount} - ${staker}`) let tokenBal = await token.balanceOf(staker) // allow Staking app to move owner tokens await token.approve(stakingAddress, amount, { from: staker }) // stake tokens - // console.log(`staking ${amount} - ${staker}, ${tokenBal} tokens`) - let tx = await staking.stakeFor( + await staking.stakeFor( staker, amount, web3.utils.utf8ToHex(EMPTY_STRING), { from: testStakingCallerAddress }) - // console.log(`staked ${amount} - ${staker}`) } const approveAndFundNewClaim = async (amount, from) => { @@ -129,9 +126,6 @@ contract('Staking test', async (accounts) => { // total stake assert.equal((await staking.totalStaked()).toString(), DEFAULT_AMOUNT, 'Total stake should match') - // unstake half of current value - let unstakeAmount = DEFAULT_AMOUNT.div(web3.utils.toBN(2)) - // Unstake default amount await staking.unstake( DEFAULT_AMOUNT, From 8fec2b4a8bd9d512afa3d9ad7470bfe57ff1f641 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Tue, 17 Mar 2020 17:39:56 -0400 Subject: [PATCH 07/39] SP test now working w/multiplier --- eth-contracts/test/serviceProvider.test.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/eth-contracts/test/serviceProvider.test.js b/eth-contracts/test/serviceProvider.test.js index ba25dbbfc02..3555046d10d 100644 --- a/eth-contracts/test/serviceProvider.test.js +++ b/eth-contracts/test/serviceProvider.test.js @@ -106,7 +106,6 @@ contract('ServiceProvider test', async (accounts) => { // Transfer 1000 tokens to accounts[1] await token.transfer(accounts[1], INITIAL_BAL, { from: treasuryAddress }) - // let accountBal = await token.balanceOf(accounts[1]) }) /* Helper functions */ @@ -375,13 +374,7 @@ contract('ServiceProvider test', async (accounts) => { */ it('confirm registered stake', async () => { // Confirm staking contract has correct amt - let multiplier = await staking.getCurrentStakeMultiplier() - console.log(fromBn(multiplier)) - console.log('----') - let returnedValue = await getStakeAmountForAccount(stakerAccount) - console.log(returnedValue) - console.log('----') assert.equal(returnedValue, DEFAULT_AMOUNT) }) @@ -417,7 +410,6 @@ contract('ServiceProvider test', async (accounts) => { }) it('fails to register duplicate endpoint w/same account', async () => { - process.exit() // Attempt to register dup endpoint with the same account await _lib.assertRevert( registerServiceProvider( From 3cc09f0debbd8978e0834b04dcc99ca8b0f5a83d Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Tue, 17 Mar 2020 18:02:43 -0400 Subject: [PATCH 08/39] minor reorg --- eth-contracts/test/staking.test.js | 67 +++++++++++++++--------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/eth-contracts/test/staking.test.js b/eth-contracts/test/staking.test.js index dbb6c9bd50c..320edf8304b 100644 --- a/eth-contracts/test/staking.test.js +++ b/eth-contracts/test/staking.test.js @@ -35,7 +35,6 @@ contract('Staking test', async (accounts) => { const EMPTY_STRING = '' const approveAndStake = async (amount, staker) => { - let tokenBal = await token.balanceOf(staker) // allow Staking app to move owner tokens await token.approve(stakingAddress, amount, { from: staker }) // stake tokens @@ -107,39 +106,6 @@ contract('Staking test', async (accounts) => { assert.equal(await staking.supportsHistory(), true, 'history support should match') }) - it('unstakes', async () => { - const staker = accounts[2] - // Transfer default tokens to account[2] - await token.transfer(staker, DEFAULT_AMOUNT, { from: treasuryAddress }) - - const initialOwnerBalance = await getTokenBalance(token, staker) - const initialStakingBalance = await getTokenBalance(token, stakingAddress) - - await approveAndStake(DEFAULT_AMOUNT, staker) - - const tmpOwnerBalance = await getTokenBalance(token, staker) - const tmpStakingBalance = await getTokenBalance(token, stakingAddress) - assert.equal(tmpOwnerBalance, initialOwnerBalance - DEFAULT_AMOUNT, 'staker balance should match') - assert.equal(tmpStakingBalance, initialStakingBalance + DEFAULT_AMOUNT, 'Staking app balance should match') - assert.equal(fromBn(await staking.totalStakedFor(staker)), DEFAULT_AMOUNT, 'staked value should match') - - // total stake - assert.equal((await staking.totalStaked()).toString(), DEFAULT_AMOUNT, 'Total stake should match') - - // Unstake default amount - await staking.unstake( - DEFAULT_AMOUNT, - web3.utils.utf8ToHex(EMPTY_STRING), - { from: staker } - ) - - const finalOwnerBalance = await getTokenBalance(token, staker) - const finalStakingBalance = await getTokenBalance(token, stakingAddress) - - assert.equal(finalOwnerBalance, initialOwnerBalance, 'initial and final staker balance should match') - assert.equal(finalStakingBalance, initialStakingBalance, 'initial and final staking balance should match') - }) - it('fails staking 0 amount', async () => { await token.approve(stakingAddress, 1) await _lib.assertRevert(staking.stake(0, web3.utils.utf8ToHex(EMPTY_STRING))) @@ -183,6 +149,39 @@ contract('Staking test', async (accounts) => { 'Account stake value should match default stake') }) + it('unstakes', async () => { + const staker = accounts[2] + // Transfer default tokens to account[2] + await token.transfer(staker, DEFAULT_AMOUNT, { from: treasuryAddress }) + + const initialOwnerBalance = await getTokenBalance(token, staker) + const initialStakingBalance = await getTokenBalance(token, stakingAddress) + + await approveAndStake(DEFAULT_AMOUNT, staker) + + const tmpOwnerBalance = await getTokenBalance(token, staker) + const tmpStakingBalance = await getTokenBalance(token, stakingAddress) + assert.equal(tmpOwnerBalance, initialOwnerBalance - DEFAULT_AMOUNT, 'staker balance should match') + assert.equal(tmpStakingBalance, initialStakingBalance + DEFAULT_AMOUNT, 'Staking app balance should match') + assert.equal(fromBn(await staking.totalStakedFor(staker)), DEFAULT_AMOUNT, 'staked value should match') + + // total stake + assert.equal((await staking.totalStaked()).toString(), DEFAULT_AMOUNT, 'Total stake should match') + + // Unstake default amount + await staking.unstake( + DEFAULT_AMOUNT, + web3.utils.utf8ToHex(EMPTY_STRING), + { from: staker } + ) + + const finalOwnerBalance = await getTokenBalance(token, staker) + const finalStakingBalance = await getTokenBalance(token, stakingAddress) + + assert.equal(finalOwnerBalance, initialOwnerBalance, 'initial and final staker balance should match') + assert.equal(finalStakingBalance, initialStakingBalance, 'initial and final staking balance should match') + }) + it('stake with multiple accounts', async () => { // Transfer 1000 tokens to accounts[1], accounts[2] await token.transfer(accounts[1], DEFAULT_AMOUNT, { from: treasuryAddress }) From e44042f1c0995164713d6e2fb031d9ed6919b21f Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Tue, 17 Mar 2020 19:30:10 -0400 Subject: [PATCH 09/39] Multiplier fix --- eth-contracts/contracts/staking/Staking.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index d1cd331809a..aa6db276a28 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -21,6 +21,9 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { string private constant ERROR_TOKEN_TRANSFER = "STAKING_TOKEN_TRANSFER"; string private constant ERROR_NOT_ENOUGH_BALANCE = "STAKING_NOT_ENOUGH_BALANCE"; + // standard - imitates relationship between Ether and Wei + uint8 private constant DECIMALS = 18; + // Reward tracking info uint256 internal currentClaimBlock; uint256 internal currentClaimableAmount; @@ -64,8 +67,9 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { currentClaimBlock = 0; currentClaimableAmount = 0; + uint256 initialMultiplier = 10**uint256(DECIMALS); // Initialize multiplier history value - stakeMultiplier.add64(getBlockNumber64(), 1000); + stakeMultiplier.add64(getBlockNumber64(), initialMultiplier); } /* External functions */ From 31b64782545f659104fab8b7e19a05472df04f7f Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Tue, 17 Mar 2020 22:59:16 -0400 Subject: [PATCH 10/39] Claim tests now working --- eth-contracts/test/claimFactory.test.js | 173 ++++++++++++------------ 1 file changed, 87 insertions(+), 86 deletions(-) diff --git a/eth-contracts/test/claimFactory.test.js b/eth-contracts/test/claimFactory.test.js index 4598cc42abb..5f4aca0f450 100644 --- a/eth-contracts/test/claimFactory.test.js +++ b/eth-contracts/test/claimFactory.test.js @@ -6,6 +6,20 @@ const OwnedUpgradeabilityProxy = artifacts.require('OwnedUpgradeabilityProxy') const Staking = artifacts.require('Staking') const encodeCall = require('./encodeCall') +const fromBn = n => parseInt(n.valueOf(), 10) + +const toWei = (aud) => { + let amountInAudWei = web3.utils.toWei( + aud.toString(), + 'ether' + ) + + let amountInAudWeiBN = web3.utils.toBN(amountInAudWei) + return amountInAudWeiBN +} + +const DEFAULT_AMOUNT = toWei(120) + contract('ClaimFactory', async (accounts) => { // Local web3, injected by truffle let treasuryAddress = accounts[0] @@ -17,12 +31,26 @@ contract('ClaimFactory', async (accounts) => { let proxy let impl0 let BN = web3.utils.BN + let testStakingCallerAddress = accounts[6] // Dummy stand in for sp factory in actual deployment const getLatestBlock = async () => { return web3.eth.getBlock('latest') } + const approveTransferAndStake = async (amount, staker) => { + // Transfer default tokens to + await token.transfer(staker, amount, { from: treasuryAddress }) + // Allow Staking app to move owner tokens + await token.approve(staking.address, amount, { from: staker }) + // Stake tokens + await staking.stakeFor( + staker, + amount, + web3.utils.utf8ToHex(''), + { from: testStakingCallerAddress }) + } + beforeEach(async () => { token = await AudiusToken.new({ from: accounts[0] }) proxy = await OwnedUpgradeabilityProxy.new({ from: proxyOwner }) @@ -50,98 +78,59 @@ contract('ClaimFactory', async (accounts) => { // Register new contract as a minter, from the same address that deployed the contract await token.addMinter(claimFactory.address, { from: accounts[0] }) + + // Permission test address as caller + await staking.setStakingOwnerAddress(testStakingCallerAddress, { from: treasuryAddress }) }) it('Initiate a claim', async () => { - // Get amount staked for treasury - let stakedForTreasury = await staking.totalStakedFor(treasuryAddress) + // Get amount staked + let totalStaked = await staking.totalStaked() assert.isTrue( - stakedForTreasury.isZero(), + totalStaked.isZero(), 'Expect zero treasury stake prior to claim funding') - // Get funds per claim - let fundsPerClaim = await claimFactory.getFundsPerClaim() - - await claimFactory.initiateClaim() - stakedForTreasury = await staking.totalStakedFor(treasuryAddress) - - assert.isTrue( - stakedForTreasury.eq(fundsPerClaim), - 'Expect single round of funding staked for treasury at this time') - - // Confirm another claim cannot be immediately funded - await _lib.assertRevert( - claimFactory.initiateClaim(), - 'Required block difference not met') - }) - - it('Initiate multiple claims after 1x claim block diff', async () => { - // Get amount staked for treasury - let stakedForTreasury = await staking.totalStakedFor(treasuryAddress) - assert.isTrue( - stakedForTreasury.isZero(), - 'Expect zero treasury stake prior to claim funding') + // Stake default amount + let staker = accounts[2] + await approveTransferAndStake(DEFAULT_AMOUNT, staker) // Get funds per claim let fundsPerClaim = await claimFactory.getFundsPerClaim() - // Initiate claim await claimFactory.initiateClaim() - stakedForTreasury = await staking.totalStakedFor(treasuryAddress) + totalStaked = await staking.totalStaked() assert.isTrue( - stakedForTreasury.eq(fundsPerClaim), - 'Expect single round of funding staked for treasury at this time') + totalStaked.eq(fundsPerClaim.add(DEFAULT_AMOUNT)), + 'Expect single round of funding + initial stake at this time') // Confirm another claim cannot be immediately funded await _lib.assertRevert( claimFactory.initiateClaim(), 'Required block difference not met') - - let currentBlock = await getLatestBlock() - let currentBlockNum = currentBlock.number - let lastClaimBlock = await claimFactory.getLastClaimedBlock() - let claimDiff = await claimFactory.getClaimBlockDifference() - let nextClaimBlock = lastClaimBlock.add(claimDiff) - - // Advance blocks to the next valid claim - while (currentBlockNum < nextClaimBlock) { - await _lib.advanceBlock(web3) - currentBlock = await getLatestBlock() - currentBlockNum = currentBlock.number - } - - stakedForTreasury = await staking.totalStakedFor(treasuryAddress) - assert.isTrue( - stakedForTreasury.eq(fundsPerClaim), - 'Expect treasury stake equal to single fund amount') - - // Initiate another claim - await claimFactory.initiateClaim() - let treasuryStakeSecondClaim = await staking.totalStakedFor(treasuryAddress) - - assert.isTrue( - treasuryStakeSecondClaim.eq(stakedForTreasury.mul(new BN('2'))), - 'Expect 2 rounds of funding staked for treasury at this time') }) it('Initiate multiple claims after 1x claim block diff', async () => { - // Get amount staked for treasury - let stakedForTreasury = await staking.totalStakedFor(treasuryAddress) + // Get amount staked + let totalStaked = await staking.totalStaked() assert.isTrue( - stakedForTreasury.isZero(), - 'Expect zero treasury stake prior to claim funding') + totalStaked.isZero(), + 'Expect zero stake prior to claim funding') + + // Stake default amount + let staker = accounts[2] + await approveTransferAndStake(DEFAULT_AMOUNT, staker) // Get funds per claim let fundsPerClaim = await claimFactory.getFundsPerClaim() // Initiate claim await claimFactory.initiateClaim() - stakedForTreasury = await staking.totalStakedFor(treasuryAddress) + totalStaked = await staking.totalStaked() assert.isTrue( - stakedForTreasury.eq(fundsPerClaim), - 'Expect single round of funding staked for treasury at this time') + totalStaked.eq(fundsPerClaim.add(DEFAULT_AMOUNT)), + 'Expect single round of funding + initial stake at this time') // Confirm another claim cannot be immediately funded await _lib.assertRevert( @@ -161,28 +150,39 @@ contract('ClaimFactory', async (accounts) => { currentBlockNum = currentBlock.number } - stakedForTreasury = await staking.totalStakedFor(treasuryAddress) + // No change expected after block diff + totalStaked = await staking.totalStaked() assert.isTrue( - stakedForTreasury.eq(fundsPerClaim), - 'Expect treasury stake equal to single fund amount') + totalStaked.eq(fundsPerClaim.add(DEFAULT_AMOUNT)), + 'Expect single round of funding + initial stake at this time') + + let accountStakeBeforeSecondClaim = await staking.totalStakedFor(staker) // Initiate another claim await claimFactory.initiateClaim() - let treasuryStakeSecondClaim = await staking.totalStakedFor(treasuryAddress) - - assert.isTrue( - treasuryStakeSecondClaim.eq(stakedForTreasury.mul(new BN('2'))), - 'Expect 2 rounds of funding staked for treasury at this time') + totalStaked = await staking.totalStaked() + let finalAcctStake = await staking.totalStakedFor(staker) + let expectedFinalValue = accountStakeBeforeSecondClaim.add(fundsPerClaim) + + // Note - we convert ouf of BN format here to handle infinitesimal precision loss + assert.equal( + fromBn(finalAcctStake), + fromBn(expectedFinalValue), + 'Expect additional increase in stake after 2nd claim') }) it('Initiate multiple claims consecutively after 2x claim block diff', async () => { // Get funds per claim let fundsPerClaim = await claimFactory.getFundsPerClaim() - // Get amount staked for treasury - let stakedForTreasury = await staking.totalStakedFor(treasuryAddress) + // Get amount staked + let totalStaked = await staking.totalStaked() assert.isTrue( - stakedForTreasury.isZero(), - 'Expect zero treasury stake prior to claim funding') + totalStaked.isZero(), + 'Expect zero stake prior to claim funding') + + // Stake default amount + let staker = accounts[2] + await approveTransferAndStake(DEFAULT_AMOUNT, staker) let currentBlock = await getLatestBlock() let currentBlockNum = currentBlock.number @@ -200,24 +200,25 @@ contract('ClaimFactory', async (accounts) => { // Initiate claim await claimFactory.initiateClaim() - stakedForTreasury = await staking.totalStakedFor(treasuryAddress) + totalStaked = await staking.totalStaked() assert.isTrue( - stakedForTreasury.eq(fundsPerClaim), - 'Expect single round of funding staked for treasury at this time') + totalStaked.eq(fundsPerClaim.add(DEFAULT_AMOUNT)), + 'Expect single round of funding + initial stake at this time') - stakedForTreasury = await staking.totalStakedFor(treasuryAddress) - assert.isTrue( - stakedForTreasury.eq(fundsPerClaim), - 'Expect treasury stake equal to single fund amount') + let accountStakeBeforeSecondClaim = await staking.totalStakedFor(staker) // Initiate another claim await claimFactory.initiateClaim() - let treasuryStakeSecondClaim = await staking.totalStakedFor(treasuryAddress) - - assert.isTrue( - treasuryStakeSecondClaim.eq(stakedForTreasury.mul(new BN('2'))), - 'Expect 2 rounds of funding staked for treasury at this time') + totalStaked = await staking.totalStaked() + let finalAcctStake = await staking.totalStakedFor(staker) + let expectedFinalValue = accountStakeBeforeSecondClaim.add(fundsPerClaim) + + // Note - we convert ouf of BN format here to handle infinitesimal precision loss + assert.equal( + fromBn(finalAcctStake), + fromBn(expectedFinalValue), + 'Expect additional increase in stake after 2nd claim') // Confirm another claim cannot be immediately funded await _lib.assertRevert( From 875333554a47c4a68f1fb5a7cb07ce3351bf4593 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Tue, 17 Mar 2020 23:20:33 -0400 Subject: [PATCH 11/39] upgrade test back to working --- eth-contracts/test/upgradeProxy.test.js | 30 ++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/eth-contracts/test/upgradeProxy.test.js b/eth-contracts/test/upgradeProxy.test.js index 6633770f242..4cf3f5cef82 100644 --- a/eth-contracts/test/upgradeProxy.test.js +++ b/eth-contracts/test/upgradeProxy.test.js @@ -6,6 +6,19 @@ const Staking = artifacts.require('Staking') const StakingTest = artifacts.require('StakingTest') const AudiusToken = artifacts.require('AudiusToken') +const fromBn = n => parseInt(n.valueOf(), 10) + +const toWei = (aud) => { + let amountInAudWei = web3.utils.toWei( + aud.toString(), + 'ether' + ) + + let amountInAudWeiBN = web3.utils.toBN(amountInAudWei) + return amountInAudWeiBN +} +const DEFAULT_AMOUNT = toWei(120) + contract('Upgrade proxy test', async (accounts) => { let treasuryAddress = accounts[0] let testStakingCallerAddress = accounts[6] // Dummy stand in for sp factory in actual deployment @@ -19,6 +32,8 @@ contract('Upgrade proxy test', async (accounts) => { let staking1 const approveAndStake = async (amount, staker, staking) => { + // Transfer default tokens to + await token.transfer(staker, amount, { from: treasuryAddress }) // allow Staking app to move owner tokens await token.approve(staking.address, amount, { from: staker }) // stake tokens @@ -94,22 +109,21 @@ contract('Upgrade proxy test', async (accounts) => { }) it('successfully upgrades contract and transfers state', async () => { - let testInitialStake = 100 - await approveAndStake(testInitialStake, accounts[1], staking0) + await approveAndStake(DEFAULT_AMOUNT, accounts[1], staking0) await proxy.upgradeTo(impl1.address, { from: proxyOwner }) let tx = await staking1.testFunction() let testEventCheck = tx.logs.find(log => log.event === 'TestEvent').args assert.isTrue(testEventCheck.msg.length > 0, 'Expect TestEvent to be fired') - assert.equal( - (await staking1.totalStaked()).valueOf(), - testInitialStake, + let totalStakedAfterUpgrade = await staking1.totalStaked() + assert.isTrue( + DEFAULT_AMOUNT.eq(totalStakedAfterUpgrade), 'total staked amount should transfer after upgrade') - assert.equal( - (await staking1.totalStakedFor(accounts[1]).valueOf()), - testInitialStake, + let accountStakeAfterUpgrade = await staking1.totalStakedFor(accounts[1]) + assert.isTrue( + DEFAULT_AMOUNT.eq(accountStakeAfterUpgrade), 'total staked for accounts[1] should match after upgrade') }) }) From 81e0608dea77ea492db3dc1e1368f14afeb3b772 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Fri, 20 Mar 2020 16:22:28 -0400 Subject: [PATCH 12/39] Compiliing reward transfer, testing now --- .../service/ServiceProviderFactory.sol | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/eth-contracts/contracts/service/ServiceProviderFactory.sol b/eth-contracts/contracts/service/ServiceProviderFactory.sol index 803b9738168..d165df5a776 100644 --- a/eth-contracts/contracts/service/ServiceProviderFactory.sol +++ b/eth-contracts/contracts/service/ServiceProviderFactory.sol @@ -24,6 +24,16 @@ contract ServiceProviderFactory is RegistryContract { mapping(bytes32 => ServiceInstanceStakeRequirements) serviceTypeStakeRequirements; // END Temporary data structures + /* + Maps directly staked amount by SP, not including delegators + */ + mapping(address => uint) spDeployerStake; + + /* + % Cut of delegator tokens assigned to sp deployer + */ + mapping(address => uint) spDeployerCut; + bytes empty; // standard - imitates relationship between Ether and Wei @@ -124,6 +134,9 @@ contract ServiceProviderFactory is RegistryContract { _delegateOwnerWallet ); + // Update deployer total + spDeployerStake[owner] += _stakeAmount; + uint currentlyStakedForOwner = validateAccountStakeBalances(owner); emit RegisteredServiceProvider( @@ -161,6 +174,9 @@ contract ServiceProviderFactory is RegistryContract { unstakeAmount, empty ); + + // Update deployer total + spDeployerStake[owner] -= unstakeAmount; } (uint deregisteredID) = ServiceProviderStorageInterface( @@ -207,6 +223,9 @@ contract ServiceProviderFactory is RegistryContract { validateAccountStakeBalances(owner); + // Update deployer total + spDeployerStake[owner] += _increaseStakeAmount; + return newStakeAmount; } @@ -245,6 +264,9 @@ contract ServiceProviderFactory is RegistryContract { validateAccountStakeBalances(owner); + // Update deployer total + spDeployerStake[owner] -= _decreaseStakeAmount; + return newStakeAmount; } @@ -285,6 +307,28 @@ contract ServiceProviderFactory is RegistryContract { return spId; } + /* + Update service provider balance + TODO: Called by delegate manager contract only + */ + function updateServiceProviderStake( + address _serviceProvider, + uint _amount + ) external + { + spDeployerStake[_serviceProvider] = _amount; + } + + /* + Represents amount direclty staked by service provider + */ + function getServiceProviderStake(address _address) + external view returns (uint stake) + { + return spDeployerStake[_address]; + } + + function getTotalServiceTypeProviders(bytes32 _serviceType) external view returns (uint numberOfProviders) { From 6feb5c8f867b2280fc644f4356d89cc859c208a6 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Fri, 20 Mar 2020 22:38:05 -0400 Subject: [PATCH 13/39] Basic fund now working --- .../contracts/service/DelegateManager.sol | 69 +++++ eth-contracts/test/delegateManager.test.js | 251 ++++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100644 eth-contracts/contracts/service/DelegateManager.sol create mode 100644 eth-contracts/test/delegateManager.test.js diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol new file mode 100644 index 00000000000..78dbc43e9c3 --- /dev/null +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -0,0 +1,69 @@ +pragma solidity ^0.5.0; +import "../staking/Staking.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; +import "./registry/RegistryContract.sol"; +import "./interface/registry/RegistryInterface.sol"; + +import "../staking/Staking.sol"; +import "./ServiceProviderFactory.sol"; + + +// WORKING CONTRACT +// Designed to manage delegation to staking contract +contract DelegateManager is RegistryContract { + RegistryInterface registry = RegistryInterface(0); + // standard - imitates relationship between Ether and Wei + // uint8 private constant DECIMALS = 18; + + address tokenAddress; + address stakingAddress; + + bytes32 stakingProxyOwnerKey; + bytes32 serviceProviderFactoryKey; + + // Staking contract ref + ERC20Mintable internal audiusToken; + + constructor( + address _tokenAddress, + address _registryAddress, + bytes32 _stakingProxyOwnerKey, + bytes32 _serviceProviderFactoryKey + ) public { + tokenAddress = _tokenAddress; + audiusToken = ERC20Mintable(tokenAddress); + + registry = RegistryInterface(_registryAddress); + stakingProxyOwnerKey = _stakingProxyOwnerKey; + serviceProviderFactoryKey = _serviceProviderFactoryKey; + } + + function makeClaim() external { + address claimer = msg.sender; + Staking stakingContract = Staking( + registry.getContract(stakingProxyOwnerKey) + ); + ServiceProviderFactory spFactory = ServiceProviderFactory( + registry.getContract(serviceProviderFactoryKey) + ); + + // Amount stored in staking contract for owner + uint totalBalanceInStaking = stakingContract.totalStakedFor(claimer); + require(totalBalanceInStaking > 0, 'Stake required for claim'); + // Amount in sp factory for user + uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(claimer); + require(totalBalanceInSPFactory > 0, 'Service Provider stake required'); + + // Require claim availability + require(totalBalanceInStaking > totalBalanceInSPFactory, 'No stake available to claim'); + + uint totalRewards = totalBalanceInStaking - totalBalanceInSPFactory; + + // TODO: Distribute rewards cut to delegates, add more to + uint newSpBalance = totalBalanceInSPFactory + totalRewards; + + spFactory.updateServiceProviderStake(claimer, newSpBalance); + } +} + diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js new file mode 100644 index 00000000000..9e142c411b2 --- /dev/null +++ b/eth-contracts/test/delegateManager.test.js @@ -0,0 +1,251 @@ +import * as _lib from './_lib/lib.js' + +const encodeCall = require('./encodeCall') +const Registry = artifacts.require('Registry') +const AudiusToken = artifacts.require('AudiusToken') +const OwnedUpgradeabilityProxy = artifacts.require('OwnedUpgradeabilityProxy') +const ServiceProviderFactory = artifacts.require('ServiceProviderFactory') +const ServiceProviderStorage = artifacts.require('ServiceProviderStorage') +const Staking = artifacts.require('Staking') + +const DelegateManager = artifacts.require('DelegateManager') + +const ClaimFactory = artifacts.require('ClaimFactory') + +const fromBn = n => parseInt(n.valueOf(), 10) + +const getTokenBalance = async (token, account) => fromBn(await token.balanceOf(account)) +const claimBlockDiff = 46000 + +const toWei = (aud) => { + let amountInAudWei = web3.utils.toWei( + aud.toString(), + 'ether' + ) + + let amountInAudWeiBN = web3.utils.toBN(amountInAudWei) + return amountInAudWeiBN +} + +const fromWei = (wei) => { + return web3.utils.fromWei(wei) +} + +const getTokenBalance2 = async (token, account) => fromWei(await token.balanceOf(account)) + +const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') +const serviceProviderStorageKey = web3.utils.utf8ToHex('ServiceProviderStorage') +const serviceProviderFactoryKey = web3.utils.utf8ToHex('ServiceProviderFactory') + +const testDiscProvType = web3.utils.utf8ToHex('discovery-provider') +const testCreatorNodeType = web3.utils.utf8ToHex('creator-node') +const testEndpoint = 'https://localhost:5000' +const testEndpoint1 = 'https://localhost:5001' + +const MIN_STAKE_AMOUNT = 10 + +// 1000 AUD converted to AUDWei, multiplying by 10^18 +const INITIAL_BAL = toWei(1000) +const DEFAULT_AMOUNT = toWei(120) +const MAX_STAKE_AMOUNT = DEFAULT_AMOUNT * 100 + +contract('DelegateManager', async (accounts) => { + let treasuryAddress = accounts[0] + let proxyOwner = treasuryAddress + let proxy + let impl0 + let staking + let token + let registry + let stakingAddress + let tokenAddress + let serviceProviderStorage + let serviceProviderFactory + + let claimFactory + let delegateManager + + beforeEach(async () => { + registry = await Registry.new() + + proxy = await OwnedUpgradeabilityProxy.new({ from: proxyOwner }) + + // Deploy registry + await registry.addContract(ownedUpgradeabilityProxyKey, proxy.address) + + token = await AudiusToken.new({ from: treasuryAddress }) + tokenAddress = token.address + impl0 = await Staking.new() + + // Create initialization data + let initializeData = encodeCall( + 'initialize', + ['address', 'address'], + [token.address, treasuryAddress]) + + // Initialize staking contract + await proxy.upgradeToAndCall( + impl0.address, + initializeData, + { from: proxyOwner }) + + staking = await Staking.at(proxy.address) + stakingAddress = staking.address + + // Deploy sp storage + serviceProviderStorage = await ServiceProviderStorage.new(registry.address) + await registry.addContract(serviceProviderStorageKey, serviceProviderStorage.address) + + // Deploy sp factory + serviceProviderFactory = await ServiceProviderFactory.new( + registry.address, + ownedUpgradeabilityProxyKey, + serviceProviderStorageKey) + + await registry.addContract(serviceProviderFactoryKey, serviceProviderFactory.address) + + // Permission sp factory as caller, from the proxy owner address + // (which happens to equal treasury in this test case) + await staking.setStakingOwnerAddress(serviceProviderFactory.address, { from: proxyOwner }) + + // Transfer 1000 tokens to accounts[1] + await token.transfer(accounts[1], INITIAL_BAL, { from: treasuryAddress }) + + // Create new claim factory instance + claimFactory = await ClaimFactory.new( + token.address, + proxy.address, + { from: accounts[0] }) + + // Register new contract as a minter, from the same address that deployed the contract + await token.addMinter(claimFactory.address, { from: accounts[0] }) + + delegateManager = await DelegateManager.new( + token.address, + registry.address, + ownedUpgradeabilityProxyKey, + serviceProviderFactoryKey) + }) + + /* Helper functions */ + + const registerServiceProvider = async (type, endpoint, amount, account) => { + // Approve staking transfer + await token.approve(stakingAddress, amount, { from: account }) + + let tx = await serviceProviderFactory.register( + type, + endpoint, + amount, + account, + { from: account }) + + let args = tx.logs.find(log => log.event === 'RegisteredServiceProvider').args + args.stakedAmountInt = fromBn(args._stakeAmount) + args.spID = fromBn(args._spID) + return args + } + + const increaseRegisteredProviderStake = async (type, endpoint, increase, account) => { + // Approve token transfer + await token.approve( + stakingAddress, + increase, + { from: account }) + + let tx = await serviceProviderFactory.increaseStake( + increase, + { from: account }) + + let args = tx.logs.find(log => log.event === 'UpdatedStakeAmount').args + // console.dir(args, { depth: 5 }) + } + + const getStakeAmountForAccount = async (account) => { + return fromBn(await staking.totalStakedFor(account)) + } + + const decreaseRegisteredProviderStake = async (decrease, account) => { + // Approve token transfer from staking contract to account + let tx = await serviceProviderFactory.decreaseStake( + decrease, + { from: account }) + + let args = tx.logs.find(log => log.event === 'UpdatedStakeAmount').args + // console.dir(args, { depth: 5 }) + } + + const deregisterServiceProvider = async (type, endpoint, account) => { + let deregTx = await serviceProviderFactory.deregister( + type, + endpoint, + { from: account }) + let args = deregTx.logs.find(log => log.event === 'DeregisteredServiceProvider').args + args.unstakedAmountInt = fromBn(args._unstakeAmount) + args.spID = fromBn(args._spID) + return args + } + + const getServiceProviderIdsFromAddress = async (account, type) => { + // Query and convert returned IDs to bignumber + let ids = ( + await serviceProviderFactory.getServiceProviderIdsFromAddress(account, type) + ).map(x => fromBn(x)) + return ids + } + + const serviceProviderIDRegisteredToAccount = async (account, type, id) => { + let ids = await getServiceProviderIdsFromAddress(account, type) + let newIdFound = ids.includes(id) + return newIdFound + } + + describe('Delegation flow', () => { + let regTx + const stakerAccount = accounts[1] + const stakerAccount2 = accounts[2] + + beforeEach(async () => { + let initialBal = await token.balanceOf(stakerAccount) + + // 1st endpoint for stakerAccount = https://localhost:5000 + // Total Stake = 120 AUD + regTx = await registerServiceProvider( + testDiscProvType, + testEndpoint, + DEFAULT_AMOUNT, + stakerAccount) + + // Confirm event has correct amount + assert.equal(regTx.stakedAmountInt, DEFAULT_AMOUNT) + + // Confirm balance updated for tokens + let finalBal = await token.balanceOf(stakerAccount) + assert.isTrue(initialBal.eq(finalBal.add(DEFAULT_AMOUNT)), 'Expect funds to be transferred') + }) + + it('sandbox', async () => { + console.log('configured') + let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + console.log(`SP Factory Stake: ${fromBn(spStake)}`) + let totalStaked = await staking.totalStaked() + console.log(`Total Stake: ${totalStaked}`) + console.log('---') + + await claimFactory.initiateClaim() + totalStaked = await staking.totalStaked() + console.log(`Total Stake 2: ${totalStaked}`) + + let totalStakedForAccount = await staking.totalStakedFor(stakerAccount) + console.log(`Total Stake for Account: ${totalStakedForAccount}`) + spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + + await delegateManager.makeClaim({ from: stakerAccount }) + totalStakedForAccount = await staking.totalStakedFor(stakerAccount) + console.log(`Total Stake for Account: ${totalStakedForAccount}`) + spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + console.log(`SP Factory Stake 2: ${fromBn(spStake)}`) + + }) + }) +}) From 14e8252860dcf5cc8568044a2477049845d93704 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Mon, 23 Mar 2020 20:41:15 -0400 Subject: [PATCH 14/39] CHKPT - still not yet working --- .../contracts/service/DelegateManager.sol | 95 ++++++++++++++++++- eth-contracts/contracts/staking/Staking.sol | 91 ++++++++++-------- eth-contracts/test/delegateManager.test.js | 42 ++++++-- 3 files changed, 183 insertions(+), 45 deletions(-) diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 78dbc43e9c3..ca507be5bbc 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -25,6 +25,20 @@ contract DelegateManager is RegistryContract { // Staking contract ref ERC20Mintable internal audiusToken; + // Service provider address -> list of delegators + // TODO: Bounded list + mapping (address => address[]) serviceProviderDelegates; + + // Total staked for a given delegator + mapping (address => uint) delegatorStakeTotal; + + // Delegator stake by address delegated to + // delegator -> (service provider -> delegatedStake) + mapping (address => mapping(address => uint)) delegateInfo; + + // TODO: Evaluate whether this is necessary + bytes empty; + constructor( address _tokenAddress, address _registryAddress, @@ -33,12 +47,77 @@ contract DelegateManager is RegistryContract { ) public { tokenAddress = _tokenAddress; audiusToken = ERC20Mintable(tokenAddress); - registry = RegistryInterface(_registryAddress); stakingProxyOwnerKey = _stakingProxyOwnerKey; serviceProviderFactoryKey = _serviceProviderFactoryKey; } + function increaseDelegatedStake( + address _target, + uint _amount + ) external returns (uint delegeatedAmountForSP) + { + // TODO: Require _target is a valid SP + // TODO: Validate sp account total balance + // TODO: Enforce min _amount? + address delegator = msg.sender; + Staking stakingContract = Staking( + registry.getContract(stakingProxyOwnerKey) + ); + + // Stake on behalf of target service provider + stakingContract.delegateStakeFor( + _target, + delegator, + _amount, + empty); + + // Update list of delegators to SP if necessary + // TODO: Any validation on returned value? + updateServiceProviderDelegatorsIfNecessary(delegator, _target); + + // Update amount staked from this delegator to targeted service provider + delegateInfo[delegator][_target] += _amount; + + // Update total delegated stake + delegatorStakeTotal[delegator] += _amount; + + // Return new total + return delegateInfo[delegator][_target]; + } + + function decreaseDelegatedStake( + address _target, + uint _amount + ) external returns (uint delegateAmount) { + address delegator = msg.sender; + Staking stakingContract = Staking( + registry.getContract(stakingProxyOwnerKey) + ); + bool delegatorRecordExists = updateServiceProviderDelegatorsIfNecessary(delegator, _target); + require(delegatorRecordExists, 'Delegator must exist to decrease stake'); + + uint currentlyDelegatedToSP = delegateInfo[delegator][_target]; + require( + _amount < currentlyDelegatedToSP, + 'Cannot decrease greater than currently staked for this ServiceProvider'); + + // Stake on behalf of target service provider + stakingContract.unstakeFor( + _target, + _amount, + empty); + + // Update amount staked from this delegator to targeted service provider + delegateInfo[delegator][_target] -= _amount; + + // Update total delegated stake + delegatorStakeTotal[delegator] -= _amount; + + // Return new total + return delegateInfo[delegator][_target]; + } + function makeClaim() external { address claimer = msg.sender; Staking stakingContract = Staking( @@ -65,5 +144,19 @@ contract DelegateManager is RegistryContract { spFactory.updateServiceProviderStake(claimer, newSpBalance); } + + function updateServiceProviderDelegatorsIfNecessary ( + address _delegator, + address _serviceProvider + ) internal returns (bool exists) { + for (uint i = 0; i < serviceProviderDelegates[_serviceProvider].length; i++) { + if (serviceProviderDelegates[_serviceProvider][i] == _delegator) { + return true; + } + } + // If not found, update list of delegates + serviceProviderDelegates[_serviceProvider].push(_delegator); + return false; + } } diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index aa6db276a28..94ec4e566ce 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -147,7 +147,8 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { * @param _data Used in Staked event, to add signalling information in more complex staking applications */ function stakeFor(address _accountAddress, uint256 _amount, bytes calldata _data) external isInitialized { - require(msg.sender == stakingOwnerAddress, "Unauthorized staking operation"); + // TODO: permission to contract addresses via registry contract instead of 'stakingOwnerAddress' + // require(msg.sender == stakingOwnerAddress, "Unauthorized staking operation"); _stakeFor( _accountAddress, _accountAddress, @@ -160,27 +161,13 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { * @param _amount Number of tokens staked * @param _data Used in Unstaked event, to add signalling information in more complex staking applications */ + // TODO: Convert to internal model w/transfer address and account address function unstake(uint256 _amount, bytes calldata _data) external isInitialized { - // unstaking 0 tokens is not allowed - require(_amount > 0, ERROR_AMOUNT_ZERO); - - // Adjust amount by internal stake multiplier - uint internalStakeAmount = _amount.div(stakeMultiplier.getLatestValue()); - - // checkpoint updated staking balance - _modifyStakeBalance(msg.sender, internalStakeAmount, false); - - // checkpoint total supply - _modifyTotalStaked(_amount, false); - - // transfer tokens - stakingToken.safeTransfer(msg.sender, _amount); - - emit Unstaked( - msg.sender, - _amount, - totalStakedFor(msg.sender), - _data); + _unstakeFor( + msg.sender, + msg.sender, + _amount, + _data); } /** @@ -189,26 +176,30 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { * @param _data Used in Unstaked event, to add signalling information in more complex staking applications */ function unstakeFor(address _accountAddress, uint256 _amount, bytes calldata _data) external isInitialized { - require(msg.sender == stakingOwnerAddress, "Unauthorized staking operation"); + // TODO: permission to contract addresses via registry contract instead of 'stakingOwnerAddress' + // require(msg.sender == stakingOwnerAddress, "Unauthorized staking operation"); // unstaking 0 tokens is not allowed - require(_amount > 0, ERROR_AMOUNT_ZERO); - - // Adjust amount by internal stake multiplier - uint internalStakeAmount = _amount.div(stakeMultiplier.getLatestValue()); - - // checkpoint updated staking balance - _modifyStakeBalance(_accountAddress, internalStakeAmount, false); - - // checkpoint total supply - _modifyTotalStaked(_amount, false); - - // transfer tokens - stakingToken.safeTransfer(_accountAddress, _amount); + _unstakeFor( + _accountAddress, + _accountAddress, + _amount, + _data); + } - emit Unstaked( + /** + * @notice Stakes `_amount` tokens, transferring them from caller, and assigns them to `_accountAddress` + * @param _accountAddress The final staker of the tokens + * @param _delegatorAddress Address from which to transfer tokens + * @param _amount Number of tokens staked + * @param _data Used in Staked event, to add signalling information in more complex staking applications + */ + function delegateStakeFor(address _accountAddress, address _delegatorAddress, uint256 _amount, bytes calldata _data) external isInitialized { + // TODO: permission to contract addresses via registry contract instead of 'stakingOwnerAddress' + // require(msg.sender == stakingOwnerAddress, "Unauthorized staking operation"); + _stakeFor( _accountAddress, + _delegatorAddress, _amount, - totalStakedFor(_accountAddress), _data); } @@ -330,6 +321,32 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { _data); } + function _unstakeFor( + address _stakeAccount, + address _transferAccount, + uint256 _amount, + bytes memory _data) internal + { + require(_amount > 0, ERROR_AMOUNT_ZERO); + // Adjust amount by internal stake multiplier + uint internalStakeAmount = _amount.div(stakeMultiplier.getLatestValue()); + + // checkpoint updated staking balance + _modifyStakeBalance(_stakeAccount, internalStakeAmount, false); + + // checkpoint total supply + _modifyTotalStaked(_amount, false); + + // transfer tokens + stakingToken.safeTransfer(_transferAccount, _amount); + + emit Unstaked( + _stakeAccount, + _amount, + totalStakedFor(_stakeAccount), + _data); + } + // Note that _by value has been adjusted for the stake multiplier prior to getting passed in function _modifyStakeBalance(address _accountAddress, uint256 _by, bool _increase) internal { // currentInternalStake represents the internal stake value, without multiplier adjustment diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 9e142c411b2..a04203d9889 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -65,6 +65,9 @@ contract('DelegateManager', async (accounts) => { let claimFactory let delegateManager + const stakerAccount = accounts[1] + const delegatorAccount1 = accounts[2] + beforeEach(async () => { registry = await Registry.new() @@ -108,9 +111,6 @@ contract('DelegateManager', async (accounts) => { // (which happens to equal treasury in this test case) await staking.setStakingOwnerAddress(serviceProviderFactory.address, { from: proxyOwner }) - // Transfer 1000 tokens to accounts[1] - await token.transfer(accounts[1], INITIAL_BAL, { from: treasuryAddress }) - // Create new claim factory instance claimFactory = await ClaimFactory.new( token.address, @@ -202,10 +202,10 @@ contract('DelegateManager', async (accounts) => { describe('Delegation flow', () => { let regTx - const stakerAccount = accounts[1] - const stakerAccount2 = accounts[2] - beforeEach(async () => { + // Transfer 1000 tokens to staker + await token.transfer(stakerAccount, INITIAL_BAL, { from: treasuryAddress }) + let initialBal = await token.balanceOf(stakerAccount) // 1st endpoint for stakerAccount = https://localhost:5000 @@ -224,7 +224,9 @@ contract('DelegateManager', async (accounts) => { assert.isTrue(initialBal.eq(finalBal.add(DEFAULT_AMOUNT)), 'Expect funds to be transferred') }) - it('sandbox', async () => { + /* + it('initial state', async () => { + // Validate basic claim w/SP path console.log('configured') let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) console.log(`SP Factory Stake: ${fromBn(spStake)}`) @@ -245,7 +247,33 @@ contract('DelegateManager', async (accounts) => { console.log(`Total Stake for Account: ${totalStakedForAccount}`) spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) console.log(`SP Factory Stake 2: ${fromBn(spStake)}`) + }) + */ + + it('single delegator', async () => { + // Transfer 1000 tokens to delegator + await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) + + let totalStakedForSP = await staking.totalStakedFor(stakerAccount) + console.log(`Total Stake for SP: ${totalStakedForSP}`) + + let initialDelegateAmount = toWei(60) + + // Approve staking transfer + await token.approve( + stakingAddress, + initialDelegateAmount, + { from: delegatorAccount1 }) + + await delegateManager.increaseDelegatedStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 }) + + totalStakedForSP = await staking.totalStakedFor(stakerAccount) + console.log(`Total Stake for SP - after delegation: ${totalStakedForSP}`) + return true }) }) }) From 7a2ff45695ef9c835d8ca32ca24704e0ad3be3ba Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Mon, 23 Mar 2020 20:54:55 -0400 Subject: [PATCH 15/39] Savepoint --- still broken --- .../contracts/service/DelegateManager.sol | 3 +- eth-contracts/contracts/staking/Staking.sol | 30 ++++++++++++++++++- eth-contracts/test/delegateManager.test.js | 5 ++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index ca507be5bbc..72e23d64125 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -103,8 +103,9 @@ contract DelegateManager is RegistryContract { 'Cannot decrease greater than currently staked for this ServiceProvider'); // Stake on behalf of target service provider - stakingContract.unstakeFor( + stakingContract.undelegateStakeFor( _target, + delegator, _amount, empty); diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index 94ec4e566ce..88d2bea9f44 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -172,6 +172,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { /** * @notice Unstakes `_amount` tokens, returning them to the desired account. + * @param _accountAddress Account unstaked for, and token recipient * @param _amount Number of tokens staked * @param _data Used in Unstaked event, to add signalling information in more complex staking applications */ @@ -193,7 +194,12 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { * @param _amount Number of tokens staked * @param _data Used in Staked event, to add signalling information in more complex staking applications */ - function delegateStakeFor(address _accountAddress, address _delegatorAddress, uint256 _amount, bytes calldata _data) external isInitialized { + function delegateStakeFor( + address _accountAddress, + address _delegatorAddress, + uint256 _amount, + bytes calldata _data + ) external isInitialized { // TODO: permission to contract addresses via registry contract instead of 'stakingOwnerAddress' // require(msg.sender == stakingOwnerAddress, "Unauthorized staking operation"); _stakeFor( @@ -203,6 +209,28 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { _data); } + /** + * @notice Stakes `_amount` tokens, transferring them from caller, and assigns them to `_accountAddress` + * @param _accountAddress The staker of the tokens + * @param _delegatorAddress Address from which to transfer tokens + * @param _amount Number of tokens unstaked + * @param _data Used in Staked event, to add signalling information in more complex staking applications + */ + function undelegateStakeFor( + address _accountAddress, + address _delegatorAddress, + uint256 _amount, + bytes calldata _data + ) external isInitialized { + // TODO: permission to contract addresses via registry contract instead of 'stakingOwnerAddress' + // require(msg.sender == stakingOwnerAddress, "Unauthorized staking operation"); + _unstakeFor( + _accountAddress, + _delegatorAddress, + _amount, + _data); + } + /** * @notice Get the token used by the contract for staking and locking * @return The token used by the contract for staking and locking diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index a04203d9889..8eb09d2205e 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -273,6 +273,11 @@ contract('DelegateManager', async (accounts) => { totalStakedForSP = await staking.totalStakedFor(stakerAccount) console.log(`Total Stake for SP - after delegation: ${totalStakedForSP}`) + await delegateManager.decreaseDelegatedStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 }) + return true }) }) From 8b871d55f55ba960dc2869ecd6948f972f23b40a Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Mon, 23 Mar 2020 20:58:24 -0400 Subject: [PATCH 16/39] CHKPT - undelegate works --- eth-contracts/contracts/service/DelegateManager.sol | 2 +- eth-contracts/test/delegateManager.test.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 72e23d64125..0f7999fa16d 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -99,7 +99,7 @@ contract DelegateManager is RegistryContract { uint currentlyDelegatedToSP = delegateInfo[delegator][_target]; require( - _amount < currentlyDelegatedToSP, + _amount <= currentlyDelegatedToSP, 'Cannot decrease greater than currently staked for this ServiceProvider'); // Stake on behalf of target service provider diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 8eb09d2205e..0945eff3ba5 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -278,6 +278,8 @@ contract('DelegateManager', async (accounts) => { initialDelegateAmount, { from: delegatorAccount1 }) + totalStakedForSP = await staking.totalStakedFor(stakerAccount) + console.log(`Total Stake for SP - after rm delegation: ${totalStakedForSP}`) return true }) }) From 0c26a2200a1c352b91b4c7e4711cfa98a84632dc Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Mon, 23 Mar 2020 23:06:11 -0400 Subject: [PATCH 17/39] Code compiling, not yet tested --- .../contracts/service/DelegateManager.sol | 88 +++++++++++++++---- .../service/ServiceProviderFactory.sol | 62 ++++++++++--- eth-contracts/test/delegateManager.test.js | 2 +- 3 files changed, 120 insertions(+), 32 deletions(-) diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 0f7999fa16d..470599552b6 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -12,7 +12,8 @@ import "./ServiceProviderFactory.sol"; // WORKING CONTRACT // Designed to manage delegation to staking contract contract DelegateManager is RegistryContract { - RegistryInterface registry = RegistryInterface(0); + using SafeMath for uint256; + RegistryInterface registry = RegistryInterface(0); // standard - imitates relationship between Ether and Wei // uint8 private constant DECIMALS = 18; @@ -27,7 +28,7 @@ contract DelegateManager is RegistryContract { // Service provider address -> list of delegators // TODO: Bounded list - mapping (address => address[]) serviceProviderDelegates; + mapping (address => address[]) spDelegates; // Total staked for a given delegator mapping (address => uint) delegatorStakeTotal; @@ -115,48 +116,99 @@ contract DelegateManager is RegistryContract { // Update total delegated stake delegatorStakeTotal[delegator] -= _amount; + // Remove from delegators list if no delegated stake remaining + if (delegateInfo[delegator][_target] == 0) { + bool foundDelegator; + uint delegatorIndex; + for (uint i = 0; i < spDelegates[_target].length; i++) { + if (spDelegates[_target][i] == delegator) { + foundDelegator = true; + delegatorIndex = i; + } + } + + // Overwrite and shrink delegators list + spDelegates[_target][delegatorIndex] = spDelegates[_target][spDelegates[_target].length - 1]; + spDelegates[_target].length--; + } + // Return new total return delegateInfo[delegator][_target]; } function makeClaim() external { - address claimer = msg.sender; - Staking stakingContract = Staking( - registry.getContract(stakingProxyOwnerKey) - ); + // address claimer = msg.sender; ServiceProviderFactory spFactory = ServiceProviderFactory( registry.getContract(serviceProviderFactoryKey) ); // Amount stored in staking contract for owner - uint totalBalanceInStaking = stakingContract.totalStakedFor(claimer); + uint totalBalanceInStaking = Staking( + registry.getContract(stakingProxyOwnerKey) + ).totalStakedFor(msg.sender); require(totalBalanceInStaking > 0, 'Stake required for claim'); - // Amount in sp factory for user - uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(claimer); + + // Amount in sp factory for claimer + uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(msg.sender); require(totalBalanceInSPFactory > 0, 'Service Provider stake required'); - // Require claim availability - require(totalBalanceInStaking > totalBalanceInSPFactory, 'No stake available to claim'); + // Amount in delegate manager staked to service provider + // TODO: Consider caching this value + uint totalBalanceInDelegateManager = 0; + for (uint i = 0; i < spDelegates[msg.sender].length; i++) + { + address delegator = spDelegates[msg.sender][i]; + uint delegateStakeToSP = delegateInfo[delegator][msg.sender]; + totalBalanceInDelegateManager += delegateStakeToSP; + } - uint totalRewards = totalBalanceInStaking - totalBalanceInSPFactory; + uint totalBalanceOutsideStaking = totalBalanceInSPFactory + totalBalanceInDelegateManager; - // TODO: Distribute rewards cut to delegates, add more to - uint newSpBalance = totalBalanceInSPFactory + totalRewards; + // Require claim availability + require(totalBalanceInStaking > totalBalanceOutsideStaking, 'No stake available to claim'); + + // Total rewards + // Equal to (balance in staking) - ((balance in sp factory) + (balance in delegate manager)) + uint totalRewards = totalBalanceInStaking - totalBalanceOutsideStaking; + + uint deployerCut = spFactory.getServiceProviderDeployerCut(msg.sender); + uint deployerCutBase = spFactory.getServiceProviderDeployerCutBase(); + uint spDeployerCutRewards = 0; + + // Traverse all delegates and calculate their rewards + for (uint i = 0; i < spDelegates[msg.sender].length; i++) + { + address delegator = spDelegates[msg.sender][i]; + uint delegateStakeToSP = delegateInfo[delegator][msg.sender]; + // Calculate rewards by ((delegateStakeToSP / totalBalanceOutsideStaking) * totalRewards) + uint rewardsPriorToSPCut = (delegateStakeToSP.mul(totalRewards)).div(totalBalanceOutsideStaking); + // Multiply by deployer cut fraction to calculate reward for SP + uint spDeployerCut = (rewardsPriorToSPCut.mul(deployerCut)).div(deployerCutBase); + spDeployerCutRewards += spDeployerCut; + // Increase total delegate reward in DelegateManager + // Subtract SP reward from rewards to calculate delegate reward + // delegateReward = rewardsPriorToSPCut - spDeployerCut; + delegateInfo[delegator][msg.sender] += (rewardsPriorToSPCut - spDeployerCut); + delegatorStakeTotal[delegator] += (rewardsPriorToSPCut - spDeployerCut); + } - spFactory.updateServiceProviderStake(claimer, newSpBalance); + // TODO: Validate below with test cases + uint spRewardShare = (totalBalanceInSPFactory.mul(totalRewards)).div(totalBalanceOutsideStaking); + uint newSpBalance = totalBalanceInSPFactory + spRewardShare + spDeployerCutRewards; + spFactory.updateServiceProviderStake(msg.sender, newSpBalance); } function updateServiceProviderDelegatorsIfNecessary ( address _delegator, address _serviceProvider ) internal returns (bool exists) { - for (uint i = 0; i < serviceProviderDelegates[_serviceProvider].length; i++) { - if (serviceProviderDelegates[_serviceProvider][i] == _delegator) { + for (uint i = 0; i < spDelegates[_serviceProvider].length; i++) { + if (spDelegates[_serviceProvider][i] == _delegator) { return true; } } // If not found, update list of delegates - serviceProviderDelegates[_serviceProvider].push(_delegator); + spDelegates[_serviceProvider].push(_delegator); return false; } } diff --git a/eth-contracts/contracts/service/ServiceProviderFactory.sol b/eth-contracts/contracts/service/ServiceProviderFactory.sol index d165df5a776..a6da88d0bff 100644 --- a/eth-contracts/contracts/service/ServiceProviderFactory.sol +++ b/eth-contracts/contracts/service/ServiceProviderFactory.sol @@ -24,14 +24,10 @@ contract ServiceProviderFactory is RegistryContract { mapping(bytes32 => ServiceInstanceStakeRequirements) serviceTypeStakeRequirements; // END Temporary data structures - /* - Maps directly staked amount by SP, not including delegators - */ + // Maps directly staked amount by SP, not including delegators mapping(address => uint) spDeployerStake; - /* - % Cut of delegator tokens assigned to sp deployer - */ + // % Cut of delegator tokens assigned to sp deployer mapping(address => uint) spDeployerCut; bytes empty; @@ -39,6 +35,10 @@ contract ServiceProviderFactory is RegistryContract { // standard - imitates relationship between Ether and Wei uint8 private constant DECIMALS = 18; + // denominator for deployer cut calculations + // user values are intended to be x/DEPLOYER_CUT_BASE + uint private constant DEPLOYER_CUT_BASE = 100; + event RegisteredServiceProvider( uint _spID, bytes32 _serviceType, @@ -307,10 +307,10 @@ contract ServiceProviderFactory is RegistryContract { return spId; } - /* - Update service provider balance - TODO: Called by delegate manager contract only - */ + /** + * @notice Update service provider balance + * TODO: Permission to only delegatemanager + */ function updateServiceProviderStake( address _serviceProvider, uint _amount @@ -319,15 +319,51 @@ contract ServiceProviderFactory is RegistryContract { spDeployerStake[_serviceProvider] = _amount; } - /* - Represents amount direclty staked by service provider - */ + /** + * @notice Update service provider cut + * SPs will interact with this value as a percent, value translation done client side + */ + function updateServiceProviderCut( + address _serviceProvider, + uint _cut + ) external + { + require( + msg.sender == _serviceProvider, + 'Service Provider cut update operation restricted to deployer'); + + require( + _cut <= DEPLOYER_CUT_BASE, + 'Service Provider cut cannot exceed base value'); + spDeployerCut[_serviceProvider] = _cut; + } + + /** + * @notice Represents amount directly staked by service provider + */ function getServiceProviderStake(address _address) external view returns (uint stake) { return spDeployerStake[_address]; } + /** + * @notice Represents % taken by sp deployer of rewards + */ + function getServiceProviderDeployerCut(address _address) + external view returns (uint cut) + { + return spDeployerCut[_address]; + } + + /** + * @notice Denominator for deployer cut calculations + */ + function getServiceProviderDeployerCutBase() + external pure returns (uint base) + { + return DEPLOYER_CUT_BASE; + } function getTotalServiceTypeProviders(bytes32 _serviceType) external view returns (uint numberOfProviders) diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 0945eff3ba5..5a6ecab5eab 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -250,7 +250,7 @@ contract('DelegateManager', async (accounts) => { }) */ - it('single delegator', async () => { + it('single delegator operations', async () => { // Transfer 1000 tokens to delegator await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) From 58577f6e8bdab2b34de2461f430b964013b9949f Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Tue, 24 Mar 2020 11:44:17 -0400 Subject: [PATCH 18/39] Base case w/single delgator working --- .../contracts/service/DelegateManager.sol | 19 ++++++ eth-contracts/test/delegateManager.test.js | 67 +++++++++++++++++-- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 470599552b6..8f92c149393 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -176,6 +176,7 @@ contract DelegateManager is RegistryContract { uint spDeployerCutRewards = 0; // Traverse all delegates and calculate their rewards + // As each delegate reward is calculated, increment SP cut reward accordingly for (uint i = 0; i < spDelegates[msg.sender].length; i++) { address delegator = spDelegates[msg.sender][i]; @@ -198,6 +199,24 @@ contract DelegateManager is RegistryContract { spFactory.updateServiceProviderStake(msg.sender, newSpBalance); } + /** + * @notice List of delegators for a given service provider + */ + function getDelegatorsList(address _sp) + external view returns (address[] memory dels) + { + return spDelegates[_sp]; + } + + /** + * @notice Total currently staked for a delegator, across service providers + */ + function getTotalDelegatorStake(address _delegator) + external view returns (uint amount) + { + return delegatorStakeTotal[_delegator]; + } + function updateServiceProviderDelegatorsIfNecessary ( address _delegator, address _serviceProvider diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 5a6ecab5eab..121283b8820 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -224,8 +224,7 @@ contract('DelegateManager', async (accounts) => { assert.isTrue(initialBal.eq(finalBal.add(DEFAULT_AMOUNT)), 'Expect funds to be transferred') }) - /* - it('initial state', async () => { + it('initial state + claim', async () => { // Validate basic claim w/SP path console.log('configured') let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) @@ -248,9 +247,9 @@ contract('DelegateManager', async (accounts) => { spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) console.log(`SP Factory Stake 2: ${fromBn(spStake)}`) }) - */ - it('single delegator operations', async () => { + it('single delegator basic operations', async () => { + // TODO: Validate all // Transfer 1000 tokens to delegator await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) @@ -265,13 +264,20 @@ contract('DelegateManager', async (accounts) => { initialDelegateAmount, { from: delegatorAccount1 }) + let delegators = await delegateManager.getDelegatorsList(stakerAccount) + console.log(`Delegators 1: ${delegators}`) + await delegateManager.increaseDelegatedStake( stakerAccount, initialDelegateAmount, { from: delegatorAccount1 }) totalStakedForSP = await staking.totalStakedFor(stakerAccount) + delegators = await delegateManager.getDelegatorsList(stakerAccount) + let delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) console.log(`Total Stake for SP - after delegation: ${totalStakedForSP}`) + console.log(`Delegators 2: ${delegators}`) + console.log(`Delegated stake: ${delegatedStake}`) await delegateManager.decreaseDelegatedStake( stakerAccount, @@ -280,7 +286,58 @@ contract('DelegateManager', async (accounts) => { totalStakedForSP = await staking.totalStakedFor(stakerAccount) console.log(`Total Stake for SP - after rm delegation: ${totalStakedForSP}`) - return true + delegators = await delegateManager.getDelegatorsList(stakerAccount) + console.log(`Delegators 3: ${delegators}`) + // TODO: Confirm decrease + }) + + it('single delegator + claim', async () => { + // TODO: Validate all + // Transfer 1000 tokens to delegator + await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) + + let totalStakedForSP = await staking.totalStakedFor(stakerAccount) + console.log(`Total Stake for SP: ${totalStakedForSP}`) + + let initialDelegateAmount = toWei(60) + + // Approve staking transfer + await token.approve( + stakingAddress, + initialDelegateAmount, + { from: delegatorAccount1 }) + + await delegateManager.increaseDelegatedStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 }) + + totalStakedForSP = await staking.totalStakedFor(stakerAccount) + console.log(`Total Stake for SP - after delegation: ${totalStakedForSP}`) + let delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + console.log(`Delegated stake: ${delegatedStake}`) + + // Update SP Deployer Cut + await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) + let deployerCut = await serviceProviderFactory.getServiceProviderDeployerCut(stakerAccount) + console.log(`SP deployer cut ${deployerCut}`) + + console.log(`Initiating claim...`) + await claimFactory.initiateClaim() + totalStakedForSP = await staking.totalStakedFor(stakerAccount) + console.log(`Total in staking.sol for stakerAcct after claim: distributed ${totalStakedForSP}`) + + let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + console.log(`SPFactory - staked amount prior to claim ${spStake}`) + console.log(`DelegateManager - staked amount prior to claim ${delegatedStake}`) + console.log(`Making claim...`) + + await delegateManager.makeClaim({ from: stakerAccount }) + spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + console.log(`SPFactory - staked amount after making claim ${spStake}`) + console.log(`DelegateManager - staked amount after making claim ${delegatedStake}`) }) }) }) From d38b3a4808a380d8401d8cd3dcf2b717198209cc Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Tue, 24 Mar 2020 15:58:34 -0400 Subject: [PATCH 19/39] 1st two cases validated --- .../contracts/service/DelegateManager.sol | 14 +++++ eth-contracts/test/delegateManager.test.js | 57 +++++++++++-------- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 8f92c149393..9f309b88e3b 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -40,6 +40,10 @@ contract DelegateManager is RegistryContract { // TODO: Evaluate whether this is necessary bytes empty; + event Test( + uint256 test, + string msg); + constructor( address _tokenAddress, address _registryAddress, @@ -197,6 +201,7 @@ contract DelegateManager is RegistryContract { uint spRewardShare = (totalBalanceInSPFactory.mul(totalRewards)).div(totalBalanceOutsideStaking); uint newSpBalance = totalBalanceInSPFactory + spRewardShare + spDeployerCutRewards; spFactory.updateServiceProviderStake(msg.sender, newSpBalance); + // require(false, 'tmp fail'); } /** @@ -217,6 +222,15 @@ contract DelegateManager is RegistryContract { return delegatorStakeTotal[_delegator]; } + /** + * @notice Total currently staked for a delegator, for a given service provider + */ + function getDelegatorStakeForServiceProvider(address _delegator, address _serviceProvider) + external view returns (uint amount) + { + return delegateInfo[_delegator][_serviceProvider]; + } + function updateServiceProviderDelegatorsIfNecessary ( address _delegator, address _serviceProvider diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 121283b8820..c44558c7671 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -226,26 +226,20 @@ contract('DelegateManager', async (accounts) => { it('initial state + claim', async () => { // Validate basic claim w/SP path - console.log('configured') let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - console.log(`SP Factory Stake: ${fromBn(spStake)}`) - let totalStaked = await staking.totalStaked() - console.log(`Total Stake: ${totalStaked}`) - console.log('---') + let totalStakedForAccount = await staking.totalStakedFor(stakerAccount) await claimFactory.initiateClaim() - totalStaked = await staking.totalStaked() - console.log(`Total Stake 2: ${totalStaked}`) - let totalStakedForAccount = await staking.totalStakedFor(stakerAccount) - console.log(`Total Stake for Account: ${totalStakedForAccount}`) + totalStakedForAccount = await staking.totalStakedFor(stakerAccount) spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) await delegateManager.makeClaim({ from: stakerAccount }) totalStakedForAccount = await staking.totalStakedFor(stakerAccount) - console.log(`Total Stake for Account: ${totalStakedForAccount}`) spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - console.log(`SP Factory Stake 2: ${fromBn(spStake)}`) + assert.isTrue( + spStake.eq(totalStakedForAccount), + 'Stake value in SPFactory and Staking.sol must be equal') }) it('single delegator basic operations', async () => { @@ -254,8 +248,7 @@ contract('DelegateManager', async (accounts) => { await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) let totalStakedForSP = await staking.totalStakedFor(stakerAccount) - console.log(`Total Stake for SP: ${totalStakedForSP}`) - + let initialSpStake = totalStakedForSP let initialDelegateAmount = toWei(60) // Approve staking transfer @@ -265,32 +258,49 @@ contract('DelegateManager', async (accounts) => { { from: delegatorAccount1 }) let delegators = await delegateManager.getDelegatorsList(stakerAccount) - console.log(`Delegators 1: ${delegators}`) - await delegateManager.increaseDelegatedStake( stakerAccount, initialDelegateAmount, { from: delegatorAccount1 }) - totalStakedForSP = await staking.totalStakedFor(stakerAccount) delegators = await delegateManager.getDelegatorsList(stakerAccount) - let delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - console.log(`Total Stake for SP - after delegation: ${totalStakedForSP}`) - console.log(`Delegators 2: ${delegators}`) - console.log(`Delegated stake: ${delegatedStake}`) + let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + let delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + let delegatedStakeForSP = await delegateManager.getDelegatorStakeForServiceProvider( + delegatorAccount1, + stakerAccount) + let delegatorFound = delegators.includes(delegatorAccount1) + + assert.isTrue( + delegatorFound, + 'Delegator found in array' + ) + assert.isTrue( + delegatedStake.eq(delegatedStakeForSP), + 'All stake expected for Service Provider' + ) + assert.isTrue( + totalStakedForSP.eq(spStake.add(delegatedStake)), + 'Sum of Staking.sol equals SPFactory and DelegateManager' + ) await delegateManager.decreaseDelegatedStake( stakerAccount, initialDelegateAmount, { from: delegatorAccount1 }) totalStakedForSP = await staking.totalStakedFor(stakerAccount) - console.log(`Total Stake for SP - after rm delegation: ${totalStakedForSP}`) delegators = await delegateManager.getDelegatorsList(stakerAccount) - console.log(`Delegators 3: ${delegators}`) - // TODO: Confirm decrease + assert.equal( + delegators.length, + 0, + 'Expect no remaining delegators') + assert.isTrue( + initialSpStake.eq(totalStakedForSP), + 'Staking.sol back to initial value') }) + /* it('single delegator + claim', async () => { // TODO: Validate all // Transfer 1000 tokens to delegator @@ -339,5 +349,6 @@ contract('DelegateManager', async (accounts) => { console.log(`SPFactory - staked amount after making claim ${spStake}`) console.log(`DelegateManager - staked amount after making claim ${delegatedStake}`) }) + */ }) }) From 292ab2ed13a422d62f1f15f97943c3c636a65a68 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Wed, 25 Mar 2020 13:17:55 -0400 Subject: [PATCH 20/39] TODO: Expected value verification --- eth-contracts/test/delegateManager.test.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index c44558c7671..6ece4174769 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -300,13 +300,13 @@ contract('DelegateManager', async (accounts) => { 'Staking.sol back to initial value') }) - /* it('single delegator + claim', async () => { // TODO: Validate all // Transfer 1000 tokens to delegator await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) let totalStakedForSP = await staking.totalStakedFor(stakerAccount) + console.log(`-------`) console.log(`Total Stake for SP: ${totalStakedForSP}`) let initialDelegateAmount = toWei(60) @@ -330,25 +330,37 @@ contract('DelegateManager', async (accounts) => { // Update SP Deployer Cut await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) let deployerCut = await serviceProviderFactory.getServiceProviderDeployerCut(stakerAccount) + let deployerBase = await serviceProviderFactory.getServiceProviderDeployerCutBase() console.log(`SP deployer cut ${deployerCut}`) + console.log(`SP deployer cut base ${deployerBase}`) console.log(`Initiating claim...`) await claimFactory.initiateClaim() - totalStakedForSP = await staking.totalStakedFor(stakerAccount) - console.log(`Total in staking.sol for stakerAcct after claim: distributed ${totalStakedForSP}`) let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + totalStakedForSP = await staking.totalStakedFor(stakerAccount) delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + + let totalValueOutsideStaking = spStake.add(delegatedStake) + console.log(`Total val outside staking: ${totalValueOutsideStaking}`) + + let totalRewards = totalStakedForSP.sub(totalValueOutsideStaking) + console.log(`Total rewards ${totalRewards}`) + + console.log(`Total in staking.sol for stakerAcct after claim: distributed ${totalStakedForSP}`) console.log(`SPFactory - staked amount prior to claim ${spStake}`) console.log(`DelegateManager - staked amount prior to claim ${delegatedStake}`) console.log(`Making claim...`) + // TODO: Calculate expected value await delegateManager.makeClaim({ from: stakerAccount }) spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) console.log(`SPFactory - staked amount after making claim ${spStake}`) console.log(`DelegateManager - staked amount after making claim ${delegatedStake}`) }) - */ + + // 2 service providers, 1 claim, no delegation + // 2 service providers, 1 claim, delegation to first SP }) }) From a7d18ff527ae13dbe6e432958caa8b263a1dae71 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Wed, 25 Mar 2020 15:24:30 -0400 Subject: [PATCH 21/39] all 3 tests passing --- eth-contracts/test/delegateManager.test.js | 38 +++++++++------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 6ece4174769..56380ef77de 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -306,9 +306,6 @@ contract('DelegateManager', async (accounts) => { await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) let totalStakedForSP = await staking.totalStakedFor(stakerAccount) - console.log(`-------`) - console.log(`Total Stake for SP: ${totalStakedForSP}`) - let initialDelegateAmount = toWei(60) // Approve staking transfer @@ -323,41 +320,36 @@ contract('DelegateManager', async (accounts) => { { from: delegatorAccount1 }) totalStakedForSP = await staking.totalStakedFor(stakerAccount) - console.log(`Total Stake for SP - after delegation: ${totalStakedForSP}`) let delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - console.log(`Delegated stake: ${delegatedStake}`) // Update SP Deployer Cut await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) let deployerCut = await serviceProviderFactory.getServiceProviderDeployerCut(stakerAccount) - let deployerBase = await serviceProviderFactory.getServiceProviderDeployerCutBase() - console.log(`SP deployer cut ${deployerCut}`) - console.log(`SP deployer cut base ${deployerBase}`) - - console.log(`Initiating claim...`) + let deployerCutBase = await serviceProviderFactory.getServiceProviderDeployerCutBase() await claimFactory.initiateClaim() let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) totalStakedForSP = await staking.totalStakedFor(stakerAccount) delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - let totalValueOutsideStaking = spStake.add(delegatedStake) - console.log(`Total val outside staking: ${totalValueOutsideStaking}`) - let totalRewards = totalStakedForSP.sub(totalValueOutsideStaking) - console.log(`Total rewards ${totalRewards}`) - console.log(`Total in staking.sol for stakerAcct after claim: distributed ${totalStakedForSP}`) - console.log(`SPFactory - staked amount prior to claim ${spStake}`) - console.log(`DelegateManager - staked amount prior to claim ${delegatedStake}`) - console.log(`Making claim...`) + // Manually calculate expected value prior to making claim + // Identical math as contract + let delegateRewardsPriorToSPCut = (delegatedStake.mul(totalRewards)).div(totalValueOutsideStaking) + let spDeployerCut = (delegateRewardsPriorToSPCut.mul(deployerCut)).div(deployerCutBase) + let delegateRewards = delegateRewardsPriorToSPCut.sub(spDeployerCut) + let expectedDelegateStake = delegatedStake.add(delegateRewards) + + let spRewardShare = (spStake.mul(totalRewards)).div(totalValueOutsideStaking) + let expectedSpStake = spStake.add(spRewardShare.add(spDeployerCut)) - // TODO: Calculate expected value await delegateManager.makeClaim({ from: stakerAccount }) - spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - console.log(`SPFactory - staked amount after making claim ${spStake}`) - console.log(`DelegateManager - staked amount after making claim ${delegatedStake}`) + let finalSpStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + let finalDelegateStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + + assert.isTrue(finalSpStake.eq(expectedSpStake), 'Expected SP stake matches found value') + assert.isTrue(finalDelegateStake.eq(expectedDelegateStake), 'Expected delegate stake matches found value') }) // 2 service providers, 1 claim, no delegation From 8e07a8edc691a03c2be8595959d4c253cc59434d Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Wed, 25 Mar 2020 18:29:22 -0400 Subject: [PATCH 22/39] Functioning slash, validation required however --- .../contracts/service/DelegateManager.sol | 45 +++++++++++++ eth-contracts/contracts/staking/Staking.sol | 4 +- eth-contracts/test/delegateManager.test.js | 63 ++++++++++++++++++- 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 9f309b88e3b..e904d6d25fd 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -204,6 +204,51 @@ contract DelegateManager is RegistryContract { // require(false, 'tmp fail'); } + // TODO: Permission to governance contract only + function slash(uint _amount, address _slashAddress) + external + { + Staking stakingContract = Staking( + registry.getContract(stakingProxyOwnerKey) + ); + + ServiceProviderFactory spFactory = ServiceProviderFactory( + registry.getContract(serviceProviderFactoryKey) + ); + + // Amount stored in staking contract for owner + // TODO: See whether totalBalanceOutsideOfStaking is better than val in staking + // Benefit of this value is no need to recompute total value outside of staking + uint totalBalanceInStaking = stakingContract.totalStakedFor(_slashAddress); + require(totalBalanceInStaking > 0, 'Stake required prior to slash'); + require(totalBalanceInStaking > _amount, 'Cannot slash more than total currently staked'); + + // Amount in sp factory for slash target + uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(_slashAddress); + require(totalBalanceInSPFactory > 0, 'Service Provider stake required'); + + // For each delegator and deployer + // newStakeAmount = stakeAmount - (slashAmount * (stakeAmount / totalStakeAmount)) + for (uint i = 0; i < spDelegates[_slashAddress].length; i++) + { + address delegator = spDelegates[_slashAddress][i]; + uint delegateStakeToSP = delegateInfo[delegator][_slashAddress]; + uint slashAmountForDelegator = (delegateStakeToSP.mul(_amount)).div(totalBalanceInStaking); + + // Subtract slashed amount from delegator balances + delegateInfo[delegator][_slashAddress] -= (slashAmountForDelegator); + delegatorStakeTotal[delegator] -= (slashAmountForDelegator); + } + + // Substract proportional amount from ServiceProviderFactory + uint spSlashAmount = (totalBalanceInSPFactory.mul(_amount)).div(totalBalanceInStaking); + uint newSpBalance = totalBalanceInSPFactory - spSlashAmount; + spFactory.updateServiceProviderStake(_slashAddress, newSpBalance); + + // Decrease value in Staking contract + stakingContract.slash(_amount, _slashAddress); + } + /** * @notice List of delegators for a given service provider */ diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index 3da0511a13a..9580f0c061f 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -110,8 +110,8 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { * @param _slashAddress address being slashed */ function slash(uint256 _amount, address _slashAddress) external isInitialized { - // restrict functionality - require(msg.sender == treasuryAddress, "Slashing functionality locked to treasury owner"); + // TODO: restrict functionality to delegate manager + // require(msg.sender == treasuryAddress, "Slashing functionality locked to treasury owner"); // unstaking 0 tokens is not allowed require(_amount > 0, ERROR_AMOUNT_ZERO); diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 56380ef77de..043ea706cb6 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -224,6 +224,7 @@ contract('DelegateManager', async (accounts) => { assert.isTrue(initialBal.eq(finalBal.add(DEFAULT_AMOUNT)), 'Expect funds to be transferred') }) + /* it('initial state + claim', async () => { // Validate basic claim w/SP path let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) @@ -322,7 +323,7 @@ contract('DelegateManager', async (accounts) => { totalStakedForSP = await staking.totalStakedFor(stakerAccount) let delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - // Update SP Deployer Cut + // Update SP Deployer Cut to 10% await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) let deployerCut = await serviceProviderFactory.getServiceProviderDeployerCut(stakerAccount) let deployerCutBase = await serviceProviderFactory.getServiceProviderDeployerCutBase() @@ -351,6 +352,66 @@ contract('DelegateManager', async (accounts) => { assert.isTrue(finalSpStake.eq(expectedSpStake), 'Expected SP stake matches found value') assert.isTrue(finalDelegateStake.eq(expectedDelegateStake), 'Expected delegate stake matches found value') }) + */ + + it('single delegator + claim + slash', async () => { + // TODO: Validate all + // Transfer 1000 tokens to delegator + await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) + + let delegatedStake + let spFactoryStake + let totalInStakingContract + + totalInStakingContract = await staking.totalStakedFor(stakerAccount) + let initialDelegateAmount = toWei(60) + + // Approve staking transfer + await token.approve( + stakingAddress, + initialDelegateAmount, + { from: delegatorAccount1 }) + + await delegateManager.increaseDelegatedStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 }) + + totalInStakingContract = await staking.totalStakedFor(stakerAccount) + delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + + spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + totalInStakingContract = await staking.totalStakedFor(stakerAccount) + delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Staking: ${totalInStakingContract}`) + + // Update SP Deployer Cut to 10% + await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) + // Fund new claim + await claimFactory.initiateClaim() + + // Perform claim + await delegateManager.makeClaim({ from: stakerAccount }) + + // let finalSpStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + // let finalDelegateStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + + spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + totalInStakingContract = await staking.totalStakedFor(stakerAccount) + delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Staking: ${totalInStakingContract}`) + + // Perform slash functions + let slashAmount = toWei(100) + await delegateManager.slash(slashAmount, stakerAccount); + + spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + totalInStakingContract = await staking.totalStakedFor(stakerAccount) + delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Staking: ${totalInStakingContract}`) + // assert.isTrue(finalSpStake.eq(expectedSpStake), 'Expected SP stake matches found value') + // assert.isTrue(finalDelegateStake.eq(expectedDelegateStake), 'Expected delegate stake matches found value') + }) // 2 service providers, 1 claim, no delegation // 2 service providers, 1 claim, delegation to first SP From 32c482699ee55b497444d8f040542bc19843a54c Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Wed, 25 Mar 2020 21:09:59 -0400 Subject: [PATCH 23/39] SLASH in progress --- eth-contracts/test/delegateManager.test.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 043ea706cb6..fde658a5ad2 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -399,16 +399,24 @@ contract('DelegateManager', async (accounts) => { spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) totalInStakingContract = await staking.totalStakedFor(stakerAccount) delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Staking: ${totalInStakingContract}`) + let outsideStake = spFactoryStake.add(delegatedStake) + let slashAmount = toWei(100) + + let currentMultiplier = await staking.getCurrentStakeMultiplier() + console.log(`Slash amount: ${slashAmount}, stake multiplier: ${currentMultiplier}`) + console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) + console.log('Slashing...') // Perform slash functions - let slashAmount = toWei(100) await delegateManager.slash(slashAmount, stakerAccount); spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) totalInStakingContract = await staking.totalStakedFor(stakerAccount) delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Staking: ${totalInStakingContract}`) + outsideStake = spFactoryStake.add(delegatedStake) + console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) + let stakeDiscrepancy = totalInStakingContract.sub(spFactoryStake) + console.log(`Stake discrepancy: ${stakeDiscrepancy}`) // assert.isTrue(finalSpStake.eq(expectedSpStake), 'Expected SP stake matches found value') // assert.isTrue(finalDelegateStake.eq(expectedDelegateStake), 'Expected delegate stake matches found value') }) From 7b560bde29a7b81d4c51d477d975e99e03ed97b9 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Wed, 25 Mar 2020 22:45:20 -0400 Subject: [PATCH 24/39] Slash test still pending, finalizing multiplier initial value --- eth-contracts/contracts/staking/Staking.sol | 2 ++ eth-contracts/test/delegateManager.test.js | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index 9580f0c061f..d93700bd6d9 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -67,6 +67,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { currentClaimBlock = 0; currentClaimableAmount = 0; + // TODO: Finalize multiplier value uint256 initialMultiplier = 10**uint256(DECIMALS); // Initialize multiplier history value stakeMultiplier.add64(getBlockNumber64(), initialMultiplier); @@ -423,6 +424,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { _modifyStakeBalance(_from, _amount, false); _modifyStakeBalance(_to, _amount, true); + // TODO: Emit multiplier, OR adjust and emit correct amount emit StakeTransferred(_from,_amount, _to); } } diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index fde658a5ad2..737d3b42d63 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -224,7 +224,6 @@ contract('DelegateManager', async (accounts) => { assert.isTrue(initialBal.eq(finalBal.add(DEFAULT_AMOUNT)), 'Expect funds to be transferred') }) - /* it('initial state + claim', async () => { // Validate basic claim w/SP path let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) @@ -352,7 +351,6 @@ contract('DelegateManager', async (accounts) => { assert.isTrue(finalSpStake.eq(expectedSpStake), 'Expected SP stake matches found value') assert.isTrue(finalDelegateStake.eq(expectedDelegateStake), 'Expected delegate stake matches found value') }) - */ it('single delegator + claim + slash', async () => { // TODO: Validate all @@ -415,7 +413,7 @@ contract('DelegateManager', async (accounts) => { delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) outsideStake = spFactoryStake.add(delegatedStake) console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) - let stakeDiscrepancy = totalInStakingContract.sub(spFactoryStake) + let stakeDiscrepancy = totalInStakingContract.sub(outsideStake) console.log(`Stake discrepancy: ${stakeDiscrepancy}`) // assert.isTrue(finalSpStake.eq(expectedSpStake), 'Expected SP stake matches found value') // assert.isTrue(finalDelegateStake.eq(expectedDelegateStake), 'Expected delegate stake matches found value') From 478e0d00b9e094736ad442ec1d1e88d992213247 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Thu, 26 Mar 2020 14:20:23 -0400 Subject: [PATCH 25/39] Fix slash by unstake and transferring out --- eth-contracts/contracts/staking/Staking.sol | 15 ++++++++------- eth-contracts/test/delegateManager.test.js | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index d93700bd6d9..b39375f6638 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -113,16 +113,17 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { function slash(uint256 _amount, address _slashAddress) external isInitialized { // TODO: restrict functionality to delegate manager // require(msg.sender == treasuryAddress, "Slashing functionality locked to treasury owner"); - // unstaking 0 tokens is not allowed require(_amount > 0, ERROR_AMOUNT_ZERO); - // Adjust amount by internal stake multiplier - uint internalSlashAmount = _amount.div(stakeMultiplier.getLatestValue()); - - // transfer slashed funds to treasury address - // reduce stake balance for address being slashed - _transfer(_slashAddress, treasuryAddress, internalSlashAmount); + // Transfer slashed tokens to treasury address + // Amount is adjusted in _unstakeFor + // TODO: Optionally burn + _unstakeFor( + _slashAddress, + treasuryAddress, + _amount, + bytes('')); } /** diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 737d3b42d63..1070169799d 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -415,8 +415,25 @@ contract('DelegateManager', async (accounts) => { console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) let stakeDiscrepancy = totalInStakingContract.sub(outsideStake) console.log(`Stake discrepancy: ${stakeDiscrepancy}`) + let totalStaked = await staking.totalStaked() + console.log(`Total for all SP ${totalStaked}`) // assert.isTrue(finalSpStake.eq(expectedSpStake), 'Expected SP stake matches found value') // assert.isTrue(finalDelegateStake.eq(expectedDelegateStake), 'Expected delegate stake matches found value') + // + // Fund second claim + await claimFactory.initiateClaim() + console.log('') + console.log(`Funding second claim...`) + // Perform claim + await delegateManager.makeClaim({ from: stakerAccount }) + + spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + totalInStakingContract = await staking.totalStakedFor(stakerAccount) + delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + outsideStake = spFactoryStake.add(delegatedStake) + console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) + currentMultiplier = await staking.getCurrentStakeMultiplier() + console.log(`New stake multiplier: ${currentMultiplier}`) }) // 2 service providers, 1 claim, no delegation From e2a811f2e2581de96eb33ff6431b056815e8343d Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Thu, 26 Mar 2020 14:33:51 -0400 Subject: [PATCH 26/39] Test resolution --- eth-contracts/test/delegateManager.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 1070169799d..af329bc312f 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -390,6 +390,8 @@ contract('DelegateManager', async (accounts) => { // Perform claim await delegateManager.makeClaim({ from: stakerAccount }) + console.log('') + console.log('Claimed...') // let finalSpStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) // let finalDelegateStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) @@ -401,10 +403,11 @@ contract('DelegateManager', async (accounts) => { let slashAmount = toWei(100) let currentMultiplier = await staking.getCurrentStakeMultiplier() - console.log(`Slash amount: ${slashAmount}, stake multiplier: ${currentMultiplier}`) console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) + console.log('') console.log('Slashing...') + console.log(`Slash amount: ${slashAmount}, stake multiplier: ${currentMultiplier}`) // Perform slash functions await delegateManager.slash(slashAmount, stakerAccount); From c66f68a1f1a5f72e2f08f4ff4d82dcbcf98c3bd5 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Thu, 26 Mar 2020 15:25:57 -0400 Subject: [PATCH 27/39] linted delman --- .../contracts/service/DelegateManager.sol | 503 +++++++++--------- 1 file changed, 255 insertions(+), 248 deletions(-) diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index e904d6d25fd..3af9e1638fe 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -12,282 +12,289 @@ import "./ServiceProviderFactory.sol"; // WORKING CONTRACT // Designed to manage delegation to staking contract contract DelegateManager is RegistryContract { - using SafeMath for uint256; - RegistryInterface registry = RegistryInterface(0); - // standard - imitates relationship between Ether and Wei - // uint8 private constant DECIMALS = 18; + using SafeMath for uint256; + RegistryInterface registry = RegistryInterface(0); - address tokenAddress; - address stakingAddress; + address tokenAddress; + address stakingAddress; - bytes32 stakingProxyOwnerKey; - bytes32 serviceProviderFactoryKey; + bytes32 stakingProxyOwnerKey; + bytes32 serviceProviderFactoryKey; - // Staking contract ref - ERC20Mintable internal audiusToken; + // Staking contract ref + ERC20Mintable internal audiusToken; - // Service provider address -> list of delegators - // TODO: Bounded list - mapping (address => address[]) spDelegates; + // Service provider address -> list of delegators + // TODO: Bounded list + mapping (address => address[]) spDelegates; - // Total staked for a given delegator - mapping (address => uint) delegatorStakeTotal; + // Total staked for a given delegator + mapping (address => uint) delegatorStakeTotal; - // Delegator stake by address delegated to - // delegator -> (service provider -> delegatedStake) - mapping (address => mapping(address => uint)) delegateInfo; + // Delegator stake by address delegated to + // delegator -> (service provider -> delegatedStake) + mapping (address => mapping(address => uint)) delegateInfo; - // TODO: Evaluate whether this is necessary - bytes empty; + // TODO: Evaluate whether this is necessary + bytes empty; - event Test( + event Test( uint256 test, string msg); - constructor( - address _tokenAddress, - address _registryAddress, - bytes32 _stakingProxyOwnerKey, - bytes32 _serviceProviderFactoryKey - ) public { - tokenAddress = _tokenAddress; - audiusToken = ERC20Mintable(tokenAddress); - registry = RegistryInterface(_registryAddress); - stakingProxyOwnerKey = _stakingProxyOwnerKey; - serviceProviderFactoryKey = _serviceProviderFactoryKey; - } - - function increaseDelegatedStake( - address _target, - uint _amount - ) external returns (uint delegeatedAmountForSP) - { - // TODO: Require _target is a valid SP - // TODO: Validate sp account total balance - // TODO: Enforce min _amount? - address delegator = msg.sender; - Staking stakingContract = Staking( - registry.getContract(stakingProxyOwnerKey) - ); - - // Stake on behalf of target service provider - stakingContract.delegateStakeFor( - _target, - delegator, - _amount, - empty); - - // Update list of delegators to SP if necessary - // TODO: Any validation on returned value? - updateServiceProviderDelegatorsIfNecessary(delegator, _target); - - // Update amount staked from this delegator to targeted service provider - delegateInfo[delegator][_target] += _amount; - - // Update total delegated stake - delegatorStakeTotal[delegator] += _amount; - - // Return new total - return delegateInfo[delegator][_target]; - } - - function decreaseDelegatedStake( - address _target, - uint _amount - ) external returns (uint delegateAmount) { - address delegator = msg.sender; - Staking stakingContract = Staking( - registry.getContract(stakingProxyOwnerKey) - ); - bool delegatorRecordExists = updateServiceProviderDelegatorsIfNecessary(delegator, _target); - require(delegatorRecordExists, 'Delegator must exist to decrease stake'); - - uint currentlyDelegatedToSP = delegateInfo[delegator][_target]; - require( - _amount <= currentlyDelegatedToSP, - 'Cannot decrease greater than currently staked for this ServiceProvider'); - - // Stake on behalf of target service provider - stakingContract.undelegateStakeFor( - _target, - delegator, - _amount, - empty); - - // Update amount staked from this delegator to targeted service provider - delegateInfo[delegator][_target] -= _amount; - - // Update total delegated stake - delegatorStakeTotal[delegator] -= _amount; - - // Remove from delegators list if no delegated stake remaining - if (delegateInfo[delegator][_target] == 0) { - bool foundDelegator; - uint delegatorIndex; - for (uint i = 0; i < spDelegates[_target].length; i++) { - if (spDelegates[_target][i] == delegator) { - foundDelegator = true; - delegatorIndex = i; - } - } - - // Overwrite and shrink delegators list - spDelegates[_target][delegatorIndex] = spDelegates[_target][spDelegates[_target].length - 1]; - spDelegates[_target].length--; + constructor( + address _tokenAddress, + address _registryAddress, + bytes32 _stakingProxyOwnerKey, + bytes32 _serviceProviderFactoryKey + ) public { + tokenAddress = _tokenAddress; + audiusToken = ERC20Mintable(tokenAddress); + registry = RegistryInterface(_registryAddress); + stakingProxyOwnerKey = _stakingProxyOwnerKey; + serviceProviderFactoryKey = _serviceProviderFactoryKey; } - // Return new total - return delegateInfo[delegator][_target]; - } - - function makeClaim() external { - // address claimer = msg.sender; - ServiceProviderFactory spFactory = ServiceProviderFactory( - registry.getContract(serviceProviderFactoryKey) - ); - - // Amount stored in staking contract for owner - uint totalBalanceInStaking = Staking( - registry.getContract(stakingProxyOwnerKey) - ).totalStakedFor(msg.sender); - require(totalBalanceInStaking > 0, 'Stake required for claim'); - - // Amount in sp factory for claimer - uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(msg.sender); - require(totalBalanceInSPFactory > 0, 'Service Provider stake required'); - - // Amount in delegate manager staked to service provider - // TODO: Consider caching this value - uint totalBalanceInDelegateManager = 0; - for (uint i = 0; i < spDelegates[msg.sender].length; i++) + function increaseDelegatedStake( + address _target, + uint _amount + ) external returns (uint delegeatedAmountForSP) { - address delegator = spDelegates[msg.sender][i]; - uint delegateStakeToSP = delegateInfo[delegator][msg.sender]; - totalBalanceInDelegateManager += delegateStakeToSP; + // TODO: Require _target is a valid SP + // TODO: Validate sp account total balance + // TODO: Enforce min _amount? + address delegator = msg.sender; + Staking stakingContract = Staking( + registry.getContract(stakingProxyOwnerKey) + ); + + // Stake on behalf of target service provider + stakingContract.delegateStakeFor( + _target, + delegator, + _amount, + empty + ); + + // Update list of delegators to SP if necessary + // TODO: Any validation on returned value? + updateServiceProviderDelegatorsIfNecessary(delegator, _target); + + // Update amount staked from this delegator to targeted service provider + delegateInfo[delegator][_target] += _amount; + + // Update total delegated stake + delegatorStakeTotal[delegator] += _amount; + + // Return new total + return delegateInfo[delegator][_target]; } - uint totalBalanceOutsideStaking = totalBalanceInSPFactory + totalBalanceInDelegateManager; + function decreaseDelegatedStake( + address _target, + uint _amount + ) external returns (uint delegateAmount) + { + address delegator = msg.sender; + Staking stakingContract = Staking( + registry.getContract(stakingProxyOwnerKey) + ); + bool delegatorRecordExists = updateServiceProviderDelegatorsIfNecessary( + delegator, + _target + ); + require(delegatorRecordExists, "Delegator must exist to decrease stake"); + + uint currentlyDelegatedToSP = delegateInfo[delegator][_target]; + require( + _amount <= currentlyDelegatedToSP, + "Cannot decrease greater than currently staked for this ServiceProvider"); + + // Stake on behalf of target service provider + stakingContract.undelegateStakeFor( + _target, + delegator, + _amount, + empty + ); + + // Update amount staked from this delegator to targeted service provider + delegateInfo[delegator][_target] -= _amount; + + // Update total delegated stake + delegatorStakeTotal[delegator] -= _amount; + + // Remove from delegators list if no delegated stake remaining + if (delegateInfo[delegator][_target] == 0) { + bool foundDelegator; + uint delegatorIndex; + for (uint i = 0; i < spDelegates[_target].length; i++) { + if (spDelegates[_target][i] == delegator) { + foundDelegator = true; + delegatorIndex = i; + } + } + + // Overwrite and shrink delegators list + spDelegates[_target][delegatorIndex] = spDelegates[_target][spDelegates[_target].length - 1]; + spDelegates[_target].length--; + } + + // Return new total + return delegateInfo[delegator][_target]; + } - // Require claim availability - require(totalBalanceInStaking > totalBalanceOutsideStaking, 'No stake available to claim'); + function makeClaim() external { + // address claimer = msg.sender; + ServiceProviderFactory spFactory = ServiceProviderFactory( + registry.getContract(serviceProviderFactoryKey) + ); + + // Amount stored in staking contract for owner + uint totalBalanceInStaking = Staking( + registry.getContract(stakingProxyOwnerKey) + ).totalStakedFor(msg.sender); + require(totalBalanceInStaking > 0, "Stake required for claim"); + + // Amount in sp factory for claimer + uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(msg.sender); + require(totalBalanceInSPFactory > 0, "Service Provider stake required"); + + // Amount in delegate manager staked to service provider + // TODO: Consider caching this value + uint totalBalanceInDelegateManager = 0; + for (uint i = 0; i < spDelegates[msg.sender].length; i++) { + address delegator = spDelegates[msg.sender][i]; + uint delegateStakeToSP = delegateInfo[delegator][msg.sender]; + totalBalanceInDelegateManager += delegateStakeToSP; + } - // Total rewards - // Equal to (balance in staking) - ((balance in sp factory) + (balance in delegate manager)) - uint totalRewards = totalBalanceInStaking - totalBalanceOutsideStaking; + uint totalBalanceOutsideStaking = totalBalanceInSPFactory + totalBalanceInDelegateManager; + + // Require claim availability + require(totalBalanceInStaking > totalBalanceOutsideStaking, "No stake available to claim"); + + // Total rewards + // Equal to (balance in staking) - ((balance in sp factory) + (balance in delegate manager)) + uint totalRewards = totalBalanceInStaking - totalBalanceOutsideStaking; + + uint deployerCut = spFactory.getServiceProviderDeployerCut(msg.sender); + uint deployerCutBase = spFactory.getServiceProviderDeployerCutBase(); + uint spDeployerCutRewards = 0; + + // Traverse all delegates and calculate their rewards + // As each delegate reward is calculated, increment SP cut reward accordingly + for (uint i = 0; i < spDelegates[msg.sender].length; i++) { + address delegator = spDelegates[msg.sender][i]; + uint delegateStakeToSP = delegateInfo[delegator][msg.sender]; + // Calculate rewards by ((delegateStakeToSP / totalBalanceOutsideStaking) * totalRewards) + uint rewardsPriorToSPCut = ( + delegateStakeToSP.mul(totalRewards) + ).div(totalBalanceOutsideStaking); + // Multiply by deployer cut fraction to calculate reward for SP + uint spDeployerCut = (rewardsPriorToSPCut.mul(deployerCut)).div(deployerCutBase); + spDeployerCutRewards += spDeployerCut; + // Increase total delegate reward in DelegateManager + // Subtract SP reward from rewards to calculate delegate reward + // delegateReward = rewardsPriorToSPCut - spDeployerCut; + delegateInfo[delegator][msg.sender] += (rewardsPriorToSPCut - spDeployerCut); + delegatorStakeTotal[delegator] += (rewardsPriorToSPCut - spDeployerCut); + } - uint deployerCut = spFactory.getServiceProviderDeployerCut(msg.sender); - uint deployerCutBase = spFactory.getServiceProviderDeployerCutBase(); - uint spDeployerCutRewards = 0; + // TODO: Validate below with test cases + uint spRewardShare = ( + totalBalanceInSPFactory.mul(totalRewards) + ).div(totalBalanceOutsideStaking); + uint newSpBalance = totalBalanceInSPFactory + spRewardShare + spDeployerCutRewards; + spFactory.updateServiceProviderStake(msg.sender, newSpBalance); + } - // Traverse all delegates and calculate their rewards - // As each delegate reward is calculated, increment SP cut reward accordingly - for (uint i = 0; i < spDelegates[msg.sender].length; i++) + // TODO: Permission to governance contract only + function slash(uint _amount, address _slashAddress) + external { - address delegator = spDelegates[msg.sender][i]; - uint delegateStakeToSP = delegateInfo[delegator][msg.sender]; - // Calculate rewards by ((delegateStakeToSP / totalBalanceOutsideStaking) * totalRewards) - uint rewardsPriorToSPCut = (delegateStakeToSP.mul(totalRewards)).div(totalBalanceOutsideStaking); - // Multiply by deployer cut fraction to calculate reward for SP - uint spDeployerCut = (rewardsPriorToSPCut.mul(deployerCut)).div(deployerCutBase); - spDeployerCutRewards += spDeployerCut; - // Increase total delegate reward in DelegateManager - // Subtract SP reward from rewards to calculate delegate reward - // delegateReward = rewardsPriorToSPCut - spDeployerCut; - delegateInfo[delegator][msg.sender] += (rewardsPriorToSPCut - spDeployerCut); - delegatorStakeTotal[delegator] += (rewardsPriorToSPCut - spDeployerCut); + Staking stakingContract = Staking( + registry.getContract(stakingProxyOwnerKey) + ); + + ServiceProviderFactory spFactory = ServiceProviderFactory( + registry.getContract(serviceProviderFactoryKey) + ); + + // Amount stored in staking contract for owner + // TODO: See whether totalBalanceOutsideOfStaking is better than val in staking + // Benefit of this value is no need to recompute total value outside of staking + uint totalBalanceInStaking = stakingContract.totalStakedFor(_slashAddress); + require(totalBalanceInStaking > 0, "Stake required prior to slash"); + require(totalBalanceInStaking > _amount, "Cannot slash more than total currently staked"); + + // Amount in sp factory for slash target + uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(_slashAddress); + require(totalBalanceInSPFactory > 0, "Service Provider stake required"); + + // For each delegator and deployer + // newStakeAmount = stakeAmount - (slashAmount * (stakeAmount / totalStakeAmount)) + for (uint i = 0; i < spDelegates[_slashAddress].length; i++) { + address delegator = spDelegates[_slashAddress][i]; + uint delegateStakeToSP = delegateInfo[delegator][_slashAddress]; + uint slashAmountForDelegator = ( + delegateStakeToSP.mul(_amount) + ).div(totalBalanceInStaking); + + // Subtract slashed amount from delegator balances + delegateInfo[delegator][_slashAddress] -= (slashAmountForDelegator); + delegatorStakeTotal[delegator] -= (slashAmountForDelegator); + } + + // Substract proportional amount from ServiceProviderFactory + uint spSlashAmount = (totalBalanceInSPFactory.mul(_amount)).div(totalBalanceInStaking); + uint newSpBalance = totalBalanceInSPFactory - spSlashAmount; + spFactory.updateServiceProviderStake(_slashAddress, newSpBalance); + + // Decrease value in Staking contract + stakingContract.slash(_amount, _slashAddress); } - // TODO: Validate below with test cases - uint spRewardShare = (totalBalanceInSPFactory.mul(totalRewards)).div(totalBalanceOutsideStaking); - uint newSpBalance = totalBalanceInSPFactory + spRewardShare + spDeployerCutRewards; - spFactory.updateServiceProviderStake(msg.sender, newSpBalance); - // require(false, 'tmp fail'); - } + /** + * @notice List of delegators for a given service provider + */ + function getDelegatorsList(address _sp) + external view returns (address[] memory dels) + { + return spDelegates[_sp]; + } - // TODO: Permission to governance contract only - function slash(uint _amount, address _slashAddress) - external - { - Staking stakingContract = Staking( - registry.getContract(stakingProxyOwnerKey) - ); - - ServiceProviderFactory spFactory = ServiceProviderFactory( - registry.getContract(serviceProviderFactoryKey) - ); - - // Amount stored in staking contract for owner - // TODO: See whether totalBalanceOutsideOfStaking is better than val in staking - // Benefit of this value is no need to recompute total value outside of staking - uint totalBalanceInStaking = stakingContract.totalStakedFor(_slashAddress); - require(totalBalanceInStaking > 0, 'Stake required prior to slash'); - require(totalBalanceInStaking > _amount, 'Cannot slash more than total currently staked'); - - // Amount in sp factory for slash target - uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(_slashAddress); - require(totalBalanceInSPFactory > 0, 'Service Provider stake required'); - - // For each delegator and deployer - // newStakeAmount = stakeAmount - (slashAmount * (stakeAmount / totalStakeAmount)) - for (uint i = 0; i < spDelegates[_slashAddress].length; i++) + /** + * @notice Total currently staked for a delegator, across service providers + */ + function getTotalDelegatorStake(address _delegator) + external view returns (uint amount) { - address delegator = spDelegates[_slashAddress][i]; - uint delegateStakeToSP = delegateInfo[delegator][_slashAddress]; - uint slashAmountForDelegator = (delegateStakeToSP.mul(_amount)).div(totalBalanceInStaking); + return delegatorStakeTotal[_delegator]; + } - // Subtract slashed amount from delegator balances - delegateInfo[delegator][_slashAddress] -= (slashAmountForDelegator); - delegatorStakeTotal[delegator] -= (slashAmountForDelegator); + /** + * @notice Total currently staked for a delegator, for a given service provider + */ + function getDelegatorStakeForServiceProvider(address _delegator, address _serviceProvider) + external view returns (uint amount) + { + return delegateInfo[_delegator][_serviceProvider]; } - // Substract proportional amount from ServiceProviderFactory - uint spSlashAmount = (totalBalanceInSPFactory.mul(_amount)).div(totalBalanceInStaking); - uint newSpBalance = totalBalanceInSPFactory - spSlashAmount; - spFactory.updateServiceProviderStake(_slashAddress, newSpBalance); - - // Decrease value in Staking contract - stakingContract.slash(_amount, _slashAddress); - } - - /** - * @notice List of delegators for a given service provider - */ - function getDelegatorsList(address _sp) - external view returns (address[] memory dels) - { - return spDelegates[_sp]; - } - - /** - * @notice Total currently staked for a delegator, across service providers - */ - function getTotalDelegatorStake(address _delegator) - external view returns (uint amount) - { - return delegatorStakeTotal[_delegator]; - } - - /** - * @notice Total currently staked for a delegator, for a given service provider - */ - function getDelegatorStakeForServiceProvider(address _delegator, address _serviceProvider) - external view returns (uint amount) - { - return delegateInfo[_delegator][_serviceProvider]; - } - - function updateServiceProviderDelegatorsIfNecessary ( - address _delegator, - address _serviceProvider - ) internal returns (bool exists) { - for (uint i = 0; i < spDelegates[_serviceProvider].length; i++) { - if (spDelegates[_serviceProvider][i] == _delegator) { - return true; - } + function updateServiceProviderDelegatorsIfNecessary ( + address _delegator, + address _serviceProvider + ) internal returns (bool exists) + { + for (uint i = 0; i < spDelegates[_serviceProvider].length; i++) { + if (spDelegates[_serviceProvider][i] == _delegator) { + return true; + } + } + // If not found, update list of delegates + spDelegates[_serviceProvider].push(_delegator); + return false; } - // If not found, update list of delegates - spDelegates[_serviceProvider].push(_delegator); - return false; - } } From edeb800b11db953d5f610e85e4629821f5699ca2 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Thu, 26 Mar 2020 15:58:49 -0400 Subject: [PATCH 28/39] linting and all tests fixed --- .../contracts/service/ClaimFactory.sol | 100 +++++++++--------- .../service/ServiceProviderFactory.sol | 40 +++---- eth-contracts/test/staking.test.js | 19 ++-- 3 files changed, 82 insertions(+), 77 deletions(-) diff --git a/eth-contracts/contracts/service/ClaimFactory.sol b/eth-contracts/contracts/service/ClaimFactory.sol index 67cc1ad2f20..1fa95d55f95 100644 --- a/eth-contracts/contracts/service/ClaimFactory.sol +++ b/eth-contracts/contracts/service/ClaimFactory.sol @@ -3,68 +3,72 @@ import "../staking/Staking.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; + // WORKING CONTRACT // Designed to automate claim funding, minting tokens as necessary contract ClaimFactory { - // standard - imitates relationship between Ether and Wei - uint8 private constant DECIMALS = 18; + // standard - imitates relationship between Ether and Wei + uint8 private constant DECIMALS = 18; - address tokenAddress; - address stakingAddress; + address tokenAddress; + address stakingAddress; - // Claim related configurations - uint claimBlockDiff = 10; - uint lastClaimBlock = 0; + // Claim related configurations + uint claimBlockDiff = 10; + uint lastClaimBlock = 0; - // 100 AUD - uint fundingAmount = 100 * 10**uint256(DECIMALS); + // 100 AUD + uint fundingAmount = 100 * 10**uint256(DECIMALS); - // Staking contract ref - ERC20Mintable internal audiusToken; + // Staking contract ref + ERC20Mintable internal audiusToken; - constructor( - address _tokenAddress, - address _stakingAddress - ) public { - tokenAddress = _tokenAddress; - stakingAddress = _stakingAddress; - audiusToken = ERC20Mintable(tokenAddress); - // Allow a claim to be funded initially by subtracting the configured difference - lastClaimBlock = block.number - claimBlockDiff; - } + constructor( + address _tokenAddress, + address _stakingAddress + ) public { + tokenAddress = _tokenAddress; + stakingAddress = _stakingAddress; + audiusToken = ERC20Mintable(tokenAddress); + // Allow a claim to be funded initially by subtracting the configured difference + lastClaimBlock = block.number - claimBlockDiff; + } - function getClaimBlockDifference() - external view returns (uint claimBlockDifference) { - return (claimBlockDiff); - } + function getClaimBlockDifference() + external view returns (uint claimBlockDifference) + { + return (claimBlockDiff); + } - function getLastClaimedBlock() - external view returns (uint lastClaimedBlock) { - return (lastClaimBlock); - } + function getLastClaimedBlock() + external view returns (uint lastClaimedBlock) + { + return (lastClaimBlock); + } - function getFundsPerClaim() - external view returns (uint amount) { - return (fundingAmount); - } + function getFundsPerClaim() + external view returns (uint amount) + { + return (fundingAmount); + } - function initiateClaim() external { - require( - block.number - lastClaimBlock > claimBlockDiff, - 'Required block difference not met'); + function initiateClaim() external { + require( + block.number - lastClaimBlock > claimBlockDiff, + "Required block difference not met"); - bool minted = audiusToken.mint(address(this), fundingAmount); - require(minted, 'New tokens must be minted'); + bool minted = audiusToken.mint(address(this), fundingAmount); + require(minted, "New tokens must be minted"); - // Approve token transfer to staking contract address - audiusToken.approve(stakingAddress, fundingAmount); + // Approve token transfer to staking contract address + audiusToken.approve(stakingAddress, fundingAmount); - // Fund staking contract with proceeds - Staking stakingContract = Staking(stakingAddress); - stakingContract.fundNewClaim(fundingAmount); + // Fund staking contract with proceeds + Staking stakingContract = Staking(stakingAddress); + stakingContract.fundNewClaim(fundingAmount); - // Increment by claim difference - // Ensures funding of claims is repeatable given the right block difference - lastClaimBlock = lastClaimBlock + claimBlockDiff; - } + // Increment by claim difference + // Ensures funding of claims is repeatable given the right block difference + lastClaimBlock = lastClaimBlock + claimBlockDiff; + } } diff --git a/eth-contracts/contracts/service/ServiceProviderFactory.sol b/eth-contracts/contracts/service/ServiceProviderFactory.sol index a6da88d0bff..79f128ee804 100644 --- a/eth-contracts/contracts/service/ServiceProviderFactory.sol +++ b/eth-contracts/contracts/service/ServiceProviderFactory.sol @@ -28,7 +28,7 @@ contract ServiceProviderFactory is RegistryContract { mapping(address => uint) spDeployerStake; // % Cut of delegator tokens assigned to sp deployer - mapping(address => uint) spDeployerCut; + mapping(address => uint) spDeployerCut; bytes empty; @@ -175,8 +175,8 @@ contract ServiceProviderFactory is RegistryContract { empty ); - // Update deployer total - spDeployerStake[owner] -= unstakeAmount; + // Update deployer total + spDeployerStake[owner] -= unstakeAmount; } (uint deregisteredID) = ServiceProviderStorageInterface( @@ -311,30 +311,30 @@ contract ServiceProviderFactory is RegistryContract { * @notice Update service provider balance * TODO: Permission to only delegatemanager */ - function updateServiceProviderStake( - address _serviceProvider, - uint _amount - ) external - { + function updateServiceProviderStake( + address _serviceProvider, + uint _amount + ) external + { spDeployerStake[_serviceProvider] = _amount; - } + } /** * @notice Update service provider cut - * SPs will interact with this value as a percent, value translation done client side + * SPs will interact with this value as a percent, value translation done client side */ - function updateServiceProviderCut( + function updateServiceProviderCut( address _serviceProvider, uint _cut ) external { - require( - msg.sender == _serviceProvider, - 'Service Provider cut update operation restricted to deployer'); + require( + msg.sender == _serviceProvider, + "Service Provider cut update operation restricted to deployer"); - require( - _cut <= DEPLOYER_CUT_BASE, - 'Service Provider cut cannot exceed base value'); + require( + _cut <= DEPLOYER_CUT_BASE, + "Service Provider cut cannot exceed base value"); spDeployerCut[_serviceProvider] = _cut; } @@ -350,10 +350,10 @@ contract ServiceProviderFactory is RegistryContract { /** * @notice Represents % taken by sp deployer of rewards */ - function getServiceProviderDeployerCut(address _address) + function getServiceProviderDeployerCut(address _address) external view returns (uint cut) { - return spDeployerCut[_address]; + return spDeployerCut[_address]; } /** @@ -362,7 +362,7 @@ contract ServiceProviderFactory is RegistryContract { function getServiceProviderDeployerCutBase() external pure returns (uint base) { - return DEPLOYER_CUT_BASE; + return DEPLOYER_CUT_BASE; } function getTotalServiceTypeProviders(bytes32 _serviceType) diff --git a/eth-contracts/test/staking.test.js b/eth-contracts/test/staking.test.js index 320edf8304b..5be9324b958 100644 --- a/eth-contracts/test/staking.test.js +++ b/eth-contracts/test/staking.test.js @@ -210,7 +210,8 @@ contract('Staking test', async (accounts) => { await approveAndStake(DEFAULT_AMOUNT, accounts[1]) await approveAndStake(DEFAULT_AMOUNT, accounts[2]) - let initialTotalStake = parseInt(await staking.totalStaked()) + let initialStakeBN = await staking.totalStaked() + let initialTotalStake = parseInt(initialStakeBN) let initialStakeAmount = parseInt(await staking.totalStakedFor(accounts[1])) assert.equal(initialStakeAmount, DEFAULT_AMOUNT) @@ -222,15 +223,15 @@ contract('Staking test', async (accounts) => { accounts[1], treasuryAddress) - // Confirm staked value - let finalStakeAmt = parseInt(await staking.totalStakedFor(accounts[1])) - assert.equal(finalStakeAmt, DEFAULT_AMOUNT / 2) + // Confirm staked value for account + let finalAccountStake = parseInt(await staking.totalStakedFor(accounts[1])) + assert.equal(finalAccountStake, DEFAULT_AMOUNT / 2) - // Confirm total stake is unchanged after slash - assert.equal( - initialTotalStake, - await staking.totalStaked(), - 'Total amount unchanged') + // Confirm total stake is decreased after slash + let finalTotalStake = await staking.totalStaked() + assert.isTrue( + finalTotalStake.eq(initialStakeBN.sub(slashAmount)), + 'Expect total amount decreased') }) it('multiple claims, single fund cycle', async () => { From fa8679493d9cdc7f4d3616e88c232ce5fdff6f63 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Fri, 27 Mar 2020 13:41:58 -0400 Subject: [PATCH 29/39] TESTING multiplier w/slash degradation --- .../contracts/service/DelegateManager.sol | 21 +++ eth-contracts/contracts/staking/Staking.sol | 6 +- eth-contracts/test/delegateManager.test.js | 173 +++++++++++------- 3 files changed, 129 insertions(+), 71 deletions(-) diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 3af9e1638fe..8db75019adf 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -232,6 +232,27 @@ contract DelegateManager is RegistryContract { uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(_slashAddress); require(totalBalanceInSPFactory > 0, "Service Provider stake required"); + // 3/26 - Experimenting with some options to improve the 1:1 + + // Experiment 1: + // Below was an expiremnt to use total balance outside of staking instead of the value inside + /* + uint totalBalanceOutsideStaking = 0; + // Calculate total balance outside staking + uint totalBalanceInDelegateManager = 0; + for (uint i = 0; i < spDelegates[_slashAddress].length; i++) { + address delegator = spDelegates[_slashAddress][i]; + uint delegateStakeToSP = delegateInfo[delegator][_slashAddress]; + totalBalanceInDelegateManager += delegateStakeToSP; + } + uint totalBalanceOutsideStaking = totalBalanceInSPFactory + totalBalanceInDelegateManager; + */ + + // Experiment 2: slash internally FIRST + // Decrease value in Staking contract + // stakingContract.slash(_amount, _slashAddress); + // uint totalBalanceInStakingAfterSlash = stakingContract.totalStakedFor(_slashAddress); + // For each delegator and deployer // newStakeAmount = stakeAmount - (slashAmount * (stakeAmount / totalStakeAmount)) for (uint i = 0; i < spDelegates[_slashAddress].length; i++) { diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index b39375f6638..26578426057 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -68,7 +68,8 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { currentClaimableAmount = 0; // TODO: Finalize multiplier value - uint256 initialMultiplier = 10**uint256(DECIMALS); + // uint256 initialMultiplier = 10**uint256(DECIMALS); + uint256 initialMultiplier = 10**uint256(9); // Initialize multiplier history value stakeMultiplier.add64(getBlockNumber64(), initialMultiplier); } @@ -118,7 +119,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // Transfer slashed tokens to treasury address // Amount is adjusted in _unstakeFor - // TODO: Optionally burn + // TODO: Burn with actual ERC token call _unstakeFor( _slashAddress, treasuryAddress, @@ -366,6 +367,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { { require(_amount > 0, ERROR_AMOUNT_ZERO); // Adjust amount by internal stake multiplier + // Get ceiling div? uint internalStakeAmount = _amount.div(stakeMultiplier.getLatestValue()); // checkpoint updated staking balance diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index af329bc312f..82725da94b4 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -146,6 +146,92 @@ contract('DelegateManager', async (accounts) => { return args } + const ensureValidClaimPeriod = async () => { + let currentBlock = await web3.eth.getBlock('latest') + let currentBlockNum = currentBlock.number + let lastClaimBlock = await claimFactory.getLastClaimedBlock() + let claimDiff = await claimFactory.getClaimBlockDifference() + let nextClaimBlock = lastClaimBlock.add(claimDiff) + while (currentBlockNum < nextClaimBlock) { + await _lib.advanceBlock(web3) + currentBlock = await web3.eth.getBlock('latest') + currentBlockNum = currentBlock.number + } + } + + // Funds a claim, claims value for single SP address + delegators, slashes + // Slashes claim amount + const fundClaimSlash = async () => { + console.log('----') + // Ensure block difference is met prior to any operations + await ensureValidClaimPeriod() + + // Continue + let delegatedStake + let spFactoryStake + let totalInStakingContract + + totalInStakingContract = await staking.totalStakedFor(stakerAccount) + totalInStakingContract = await staking.totalStakedFor(stakerAccount) + delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + + spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + totalInStakingContract = await staking.totalStakedFor(stakerAccount) + + let tokensInStaking = await token.balanceOf(stakingAddress) + console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Staking: ${totalInStakingContract}`) + console.log(`Tokens in staking ${tokensInStaking}`) + + // Update SP Deployer Cut to 10% + await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) + // Fund new claim + await claimFactory.initiateClaim() + + // Perform claim + await delegateManager.makeClaim({ from: stakerAccount }) + console.log('') + console.log('Claimed...') + + // let finalSpStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + // let finalDelegateStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + + spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + totalInStakingContract = await staking.totalStakedFor(stakerAccount) + delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + let outsideStake = spFactoryStake.add(delegatedStake) + // let slashAmount = toWei(100) + let slashAmount = await claimFactory.getFundsPerClaim() // toWei(100) + + let currentMultiplier = await staking.getCurrentStakeMultiplier() + console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) + tokensInStaking = await token.balanceOf(stakingAddress) + console.log(`Tokens in staking ${tokensInStaking}`) + + console.log(`Stake multiplier: ${currentMultiplier}`) + /* + console.log('') + console.log('Slashing...') + console.log(`Slash amount: ${slashAmount}, stake multiplier: ${currentMultiplier}`) + // Perform slash functions + await delegateManager.slash(slashAmount, stakerAccount); + */ + + spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + totalInStakingContract = await staking.totalStakedFor(stakerAccount) + delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + outsideStake = spFactoryStake.add(delegatedStake) + console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) + let stakeDiscrepancy = totalInStakingContract.sub(outsideStake) + console.log(`Stake discrepancy: ${stakeDiscrepancy}`) + + tokensInStaking = await token.balanceOf(stakingAddress) + console.log(`Tokens in staking ${tokensInStaking}`) + + let totalStaked = await staking.totalStaked() + console.log(`Total for all SP ${totalStaked}`) + console.log('----') + } + const increaseRegisteredProviderStake = async (type, endpoint, increase, account) => { // Approve token transfer await token.approve( @@ -194,12 +280,6 @@ contract('DelegateManager', async (accounts) => { return ids } - const serviceProviderIDRegisteredToAccount = async (account, type, id) => { - let ids = await getServiceProviderIdsFromAddress(account, type) - let newIdFound = ids.includes(id) - return newIdFound - } - describe('Delegation flow', () => { let regTx beforeEach(async () => { @@ -352,16 +432,13 @@ contract('DelegateManager', async (accounts) => { assert.isTrue(finalDelegateStake.eq(expectedDelegateStake), 'Expected delegate stake matches found value') }) - it('single delegator + claim + slash', async () => { + it.only('single delegator + claim + slash', async () => { + // TODO: Run claim / clash pattern 10,000x and confirm discrepancy + // Validate discrepancy against some pre-known value, 1AUD or <1AUD // TODO: Validate all // Transfer 1000 tokens to delegator await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) - let delegatedStake - let spFactoryStake - let totalInStakingContract - - totalInStakingContract = await staking.totalStakedFor(stakerAccount) let initialDelegateAmount = toWei(60) // Approve staking transfer @@ -375,68 +452,26 @@ contract('DelegateManager', async (accounts) => { initialDelegateAmount, { from: delegatorAccount1 }) - totalInStakingContract = await staking.totalStakedFor(stakerAccount) - delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - - spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - totalInStakingContract = await staking.totalStakedFor(stakerAccount) - delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Staking: ${totalInStakingContract}`) - - // Update SP Deployer Cut to 10% - await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) - // Fund new claim - await claimFactory.initiateClaim() - - // Perform claim - await delegateManager.makeClaim({ from: stakerAccount }) - console.log('') - console.log('Claimed...') - - // let finalSpStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - // let finalDelegateStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - - spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - totalInStakingContract = await staking.totalStakedFor(stakerAccount) - delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + let total = 100 + let iterations = total + while(iterations > 0) { + console.log(`Round ${(total - iterations) + 1}`) + await fundClaimSlash() + iterations-- + } + // Summarize after execution + let spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + let totalInStakingContract = await staking.totalStakedFor(stakerAccount) + let delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) let outsideStake = spFactoryStake.add(delegatedStake) - let slashAmount = toWei(100) - - let currentMultiplier = await staking.getCurrentStakeMultiplier() - console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) - - console.log('') - console.log('Slashing...') - console.log(`Slash amount: ${slashAmount}, stake multiplier: ${currentMultiplier}`) - // Perform slash functions - await delegateManager.slash(slashAmount, stakerAccount); - - spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - totalInStakingContract = await staking.totalStakedFor(stakerAccount) - delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - outsideStake = spFactoryStake.add(delegatedStake) console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) let stakeDiscrepancy = totalInStakingContract.sub(outsideStake) console.log(`Stake discrepancy: ${stakeDiscrepancy}`) - let totalStaked = await staking.totalStaked() - console.log(`Total for all SP ${totalStaked}`) - // assert.isTrue(finalSpStake.eq(expectedSpStake), 'Expected SP stake matches found value') - // assert.isTrue(finalDelegateStake.eq(expectedDelegateStake), 'Expected delegate stake matches found value') - // - // Fund second claim - await claimFactory.initiateClaim() - console.log('') - console.log(`Funding second claim...`) - // Perform claim - await delegateManager.makeClaim({ from: stakerAccount }) + let oneAud = toWei(1) + console.log(`1 AUD: ${oneAud}`) - spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - totalInStakingContract = await staking.totalStakedFor(stakerAccount) - delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - outsideStake = spFactoryStake.add(delegatedStake) - console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) - currentMultiplier = await staking.getCurrentStakeMultiplier() - console.log(`New stake multiplier: ${currentMultiplier}`) + let tokensInStaking = await token.balanceOf(stakingAddress) + console.log(`Tokens in staking ${tokensInStaking}`) }) // 2 service providers, 1 claim, no delegation From f04861be46571f1d9ebabbd79959bd360b269eee Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Fri, 27 Mar 2020 17:05:49 -0400 Subject: [PATCH 30/39] Fixed slash discrepancy, further testing now --- .../contracts/service/ClaimFactory.sol | 5 +- .../contracts/service/DelegateManager.sol | 56 ++++++------------- eth-contracts/contracts/staking/Staking.sol | 24 +++++--- 3 files changed, 36 insertions(+), 49 deletions(-) diff --git a/eth-contracts/contracts/service/ClaimFactory.sol b/eth-contracts/contracts/service/ClaimFactory.sol index 1fa95d55f95..1aaf23c766d 100644 --- a/eth-contracts/contracts/service/ClaimFactory.sol +++ b/eth-contracts/contracts/service/ClaimFactory.sol @@ -17,8 +17,9 @@ contract ClaimFactory { uint claimBlockDiff = 10; uint lastClaimBlock = 0; - // 100 AUD - uint fundingAmount = 100 * 10**uint256(DECIMALS); + // 20 AUD + // TODO: Make this modifiable based on total staking pool? + uint fundingAmount = 20 * 10**uint256(DECIMALS); // 100 * 10**uint256(DECIMALS); // Staking contract ref ERC20Mintable internal audiusToken; diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 8db75019adf..ea39c734fde 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -209,7 +209,7 @@ contract DelegateManager is RegistryContract { spFactory.updateServiceProviderStake(msg.sender, newSpBalance); } - // TODO: Permission to governance contract only + // TODO: Permission to governance contract only function slash(uint _amount, address _slashAddress) external { @@ -222,58 +222,36 @@ contract DelegateManager is RegistryContract { ); // Amount stored in staking contract for owner - // TODO: See whether totalBalanceOutsideOfStaking is better than val in staking - // Benefit of this value is no need to recompute total value outside of staking - uint totalBalanceInStaking = stakingContract.totalStakedFor(_slashAddress); - require(totalBalanceInStaking > 0, "Stake required prior to slash"); - require(totalBalanceInStaking > _amount, "Cannot slash more than total currently staked"); + uint totalBalanceInStakingPreSlash = stakingContract.totalStakedFor(_slashAddress); + require(totalBalanceInStakingPreSlash > 0, "Stake required prior to slash"); + require(totalBalanceInStakingPreSlash > _amount, "Cannot slash more than total currently staked"); // Amount in sp factory for slash target uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(_slashAddress); require(totalBalanceInSPFactory > 0, "Service Provider stake required"); - // 3/26 - Experimenting with some options to improve the 1:1 - - // Experiment 1: - // Below was an expiremnt to use total balance outside of staking instead of the value inside - /* - uint totalBalanceOutsideStaking = 0; - // Calculate total balance outside staking - uint totalBalanceInDelegateManager = 0; - for (uint i = 0; i < spDelegates[_slashAddress].length; i++) { - address delegator = spDelegates[_slashAddress][i]; - uint delegateStakeToSP = delegateInfo[delegator][_slashAddress]; - totalBalanceInDelegateManager += delegateStakeToSP; - } - uint totalBalanceOutsideStaking = totalBalanceInSPFactory + totalBalanceInDelegateManager; - */ - - // Experiment 2: slash internally FIRST // Decrease value in Staking contract - // stakingContract.slash(_amount, _slashAddress); - // uint totalBalanceInStakingAfterSlash = stakingContract.totalStakedFor(_slashAddress); + stakingContract.slash(_amount, _slashAddress); + uint totalBalanceInStakingAfterSlash = stakingContract.totalStakedFor(_slashAddress); - // For each delegator and deployer - // newStakeAmount = stakeAmount - (slashAmount * (stakeAmount / totalStakeAmount)) + // For each delegator and deployer, recalculate new value + // newStakeAmount = newStakeAmount * (oldStakeAmount / totalBalancePreSlash) for (uint i = 0; i < spDelegates[_slashAddress].length; i++) { address delegator = spDelegates[_slashAddress][i]; - uint delegateStakeToSP = delegateInfo[delegator][_slashAddress]; - uint slashAmountForDelegator = ( - delegateStakeToSP.mul(_amount) - ).div(totalBalanceInStaking); - - // Subtract slashed amount from delegator balances + uint preSlashDelegateStake = delegateInfo[delegator][_slashAddress]; + uint newDelegateStake = ( + totalBalanceInStakingAfterSlash.mul(preSlashDelegateStake) + ).div(totalBalanceInStakingPreSlash); + uint slashAmountForDelegator = preSlashDelegateStake.sub(newDelegateStake); delegateInfo[delegator][_slashAddress] -= (slashAmountForDelegator); delegatorStakeTotal[delegator] -= (slashAmountForDelegator); } - // Substract proportional amount from ServiceProviderFactory - uint spSlashAmount = (totalBalanceInSPFactory.mul(_amount)).div(totalBalanceInStaking); - uint newSpBalance = totalBalanceInSPFactory - spSlashAmount; + // Recalculate SP direct stake + uint newSpBalance = ( + totalBalanceInStakingAfterSlash.mul(totalBalanceInSPFactory) + ).div(totalBalanceInStakingPreSlash); spFactory.updateServiceProviderStake(_slashAddress, newSpBalance); - - // Decrease value in Staking contract - stakingContract.slash(_amount, _slashAddress); } /** diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index 26578426057..e33df45f3f3 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -98,7 +98,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { uint256 newMultiplier = currentMultiplier.add(multiplierDifference); stakeMultiplier.add64(getBlockNumber64(), newMultiplier); - // pull tokens into Staking contract from caller + // Pull tokens into Staking contract from caller stakingToken.safeTransferFrom(msg.sender, address(this), _amount); // Increase total supply by input amount @@ -343,18 +343,21 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // Adjust amount by internal stake multiplier uint internalStakeAmount = _amount.div(stakeMultiplier.getLatestValue()); + // Readjust amount with multiplier + uint internalAmount = internalStakeAmount.mul(stakeMultiplier.getLatestValue()); + // Checkpoint updated staking balance _modifyStakeBalance(_stakeAccount, internalStakeAmount, true); // checkpoint total supply - _modifyTotalStaked(_amount, true); + _modifyTotalStaked(internalAmount, true); // pull tokens into Staking contract - stakingToken.safeTransferFrom(_transferAccount, address(this), _amount); + stakingToken.safeTransferFrom(_transferAccount, address(this), internalAmount); emit Staked( _stakeAccount, - _amount, + internalAmount, totalStakedFor(_stakeAccount), _data); } @@ -367,21 +370,26 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { { require(_amount > 0, ERROR_AMOUNT_ZERO); // Adjust amount by internal stake multiplier - // Get ceiling div? uint internalStakeAmount = _amount.div(stakeMultiplier.getLatestValue()); + // Adjust internal stake amount to the ceiling of the division op + internalStakeAmount += 1; + + // Readjust amount with multiplier + uint internalAmount = internalStakeAmount.mul(stakeMultiplier.getLatestValue()); + // checkpoint updated staking balance _modifyStakeBalance(_stakeAccount, internalStakeAmount, false); // checkpoint total supply - _modifyTotalStaked(_amount, false); + _modifyTotalStaked(internalAmount, false); // transfer tokens - stakingToken.safeTransfer(_transferAccount, _amount); + stakingToken.safeTransfer(_transferAccount, internalAmount); emit Unstaked( _stakeAccount, - _amount, + internalAmount, totalStakedFor(_stakeAccount), _data); } From bf36a5a0f15c063d431200e2f682b0769fbbad50 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Fri, 27 Mar 2020 17:05:57 -0400 Subject: [PATCH 31/39] test checkpoint --- eth-contracts/test/delegateManager.test.js | 37 ++++++++++++++-------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 82725da94b4..b767aad7f45 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -161,7 +161,7 @@ contract('DelegateManager', async (accounts) => { // Funds a claim, claims value for single SP address + delegators, slashes // Slashes claim amount - const fundClaimSlash = async () => { + const fundClaimSlash = async (slash) => { console.log('----') // Ensure block difference is met prior to any operations await ensureValidClaimPeriod() @@ -200,7 +200,6 @@ contract('DelegateManager', async (accounts) => { delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) let outsideStake = spFactoryStake.add(delegatedStake) // let slashAmount = toWei(100) - let slashAmount = await claimFactory.getFundsPerClaim() // toWei(100) let currentMultiplier = await staking.getCurrentStakeMultiplier() console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) @@ -208,25 +207,33 @@ contract('DelegateManager', async (accounts) => { console.log(`Tokens in staking ${tokensInStaking}`) console.log(`Stake multiplier: ${currentMultiplier}`) - /* - console.log('') - console.log('Slashing...') - console.log(`Slash amount: ${slashAmount}, stake multiplier: ${currentMultiplier}`) - // Perform slash functions - await delegateManager.slash(slashAmount, stakerAccount); - */ + if (slash) { + let slashNumerator = web3.utils.toBN(30) + let slashDenominator = web3.utils.toBN(100) + + let slashAmount = (totalInStakingContract.mul(slashNumerator)).div(slashDenominator) + console.log('') + console.log('Slashing...') + console.log(`Slash amount: ${slashAmount}, stake multiplier: ${currentMultiplier}`) + // Perform slash functions + await delegateManager.slash(slashAmount, stakerAccount); + } spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) totalInStakingContract = await staking.totalStakedFor(stakerAccount) delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) outsideStake = spFactoryStake.add(delegatedStake) + console.log('') console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) let stakeDiscrepancy = totalInStakingContract.sub(outsideStake) - console.log(`Stake discrepancy: ${stakeDiscrepancy}`) + console.log(`Internal (Staking) vs External (DelegateManager + SPFactory) Stake discrepancy: ${stakeDiscrepancy}`) tokensInStaking = await token.balanceOf(stakingAddress) console.log(`Tokens in staking ${tokensInStaking}`) + let tokensAvailableVsTotalStaked = tokensInStaking.sub(totalInStakingContract) + console.log(`Tokens available to staking address - total tracked in staking contract = ${tokensAvailableVsTotalStaked}`) + let totalStaked = await staking.totalStaked() console.log(`Total for all SP ${totalStaked}`) console.log('----') @@ -452,11 +459,15 @@ contract('DelegateManager', async (accounts) => { initialDelegateAmount, { from: delegatorAccount1 }) - let total = 100 + let total = 110 let iterations = total - while(iterations > 0) { + while (iterations > 0) { + let slash = false + if (iterations % 10 === 0) { + slash = true + } console.log(`Round ${(total - iterations) + 1}`) - await fundClaimSlash() + await fundClaimSlash(slash) iterations-- } // Summarize after execution From 3e58bad93d2cd69147e802b531cfe42fa77e7609 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Mon, 30 Mar 2020 14:07:25 -0400 Subject: [PATCH 32/39] Checkpointing code ---- MOVING TO ROUNDS BASED CLAIM MODEL --- eth-contracts/contracts/staking/Staking.sol | 38 +++++-- eth-contracts/test/delegateManager.test.js | 113 ++++++++++++-------- 2 files changed, 94 insertions(+), 57 deletions(-) diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index e33df45f3f3..bc19a14dd54 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -34,7 +34,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { } // Multiplier used to increase funds - Checkpointing.History stakeMultiplier; + Checkpointing.History globalStakeMultiplier; ERC20 internal stakingToken; mapping (address => Account) internal accounts; @@ -71,7 +71,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // uint256 initialMultiplier = 10**uint256(DECIMALS); uint256 initialMultiplier = 10**uint256(9); // Initialize multiplier history value - stakeMultiplier.add64(getBlockNumber64(), initialMultiplier); + globalStakeMultiplier.add64(getBlockNumber64(), initialMultiplier); } /* External functions */ @@ -83,9 +83,23 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // TODO: Add additional require statements here... // Update multiplier, total stake - uint256 currentMultiplier = stakeMultiplier.getLatestValue(); + uint256 currentMultiplier = globalStakeMultiplier.getLatestValue(); uint256 totalStake = totalStakedHistory.getLatestValue(); + uint internalClaimAmount = _amount.div(currentMultiplier); + uint internalClaimAdjusted = internalClaimAmount.mul(currentMultiplier); + + // Pull tokens into Staking contract from caller + stakingToken.safeTransferFrom(msg.sender, address(this), internalClaimAdjusted); + + // Increase total supply by input amount + _modifyTotalStaked(internalClaimAdjusted, true); + + uint256 multiplierDifference = (currentMultiplier.mul(internalClaimAdjusted)).div(totalStake); + uint256 newMultiplier = currentMultiplier.add(multiplierDifference); + globalStakeMultiplier.add64(getBlockNumber64(), newMultiplier); + + /* // Calculate and distribute funds by updating multiplier // Proportionally increases multiplier equivalent to incoming token value // newMultiplier = currentMultiplier + ((multiplier * _amount) / total) @@ -94,15 +108,17 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // Incoming claim fund of 100,000 // newMultiplier = 1.0 + ((1.0 * 100,000) / 200,000) = 1.5 // address1 = 200,000 * 1.5 = 300,000 <-- Total value increased by fundAmount, with newMultiplier + uint256 multiplierDifference = (currentMultiplier.mul(_amount)).div(totalStake); uint256 newMultiplier = currentMultiplier.add(multiplierDifference); - stakeMultiplier.add64(getBlockNumber64(), newMultiplier); + globalStakeMultiplier.add64(getBlockNumber64(), newMultiplier); // Pull tokens into Staking contract from caller stakingToken.safeTransferFrom(msg.sender, address(this), _amount); // Increase total supply by input amount _modifyTotalStaked(_amount, true); + */ } /** @@ -261,7 +277,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { * @return Current stake multiplier */ function getCurrentStakeMultiplier() public view isInitialized returns (uint256) { - return stakeMultiplier.getLatestValue(); + return globalStakeMultiplier.getLatestValue(); } /** @@ -296,7 +312,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { * @return The amount of tokens staked by the account at the given block number */ function totalStakedForAt(address _accountAddress, uint256 _blockNumber) external view returns (uint256) { - return (accounts[_accountAddress].stakedHistory.get(_blockNumber)).mul(stakeMultiplier.get(_blockNumber)); + return (accounts[_accountAddress].stakedHistory.get(_blockNumber)).mul(globalStakeMultiplier.get(_blockNumber)); } /** @@ -317,7 +333,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { */ function totalStakedFor(address _accountAddress) public view returns (uint256) { // we assume it's not possible to stake in the future - return (accounts[_accountAddress].stakedHistory.getLatestValue()).mul(stakeMultiplier.getLatestValue()); + return (accounts[_accountAddress].stakedHistory.getLatestValue()).mul(globalStakeMultiplier.getLatestValue()); } /** @@ -341,10 +357,10 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { require(_amount > 0, ERROR_AMOUNT_ZERO); // Adjust amount by internal stake multiplier - uint internalStakeAmount = _amount.div(stakeMultiplier.getLatestValue()); + uint internalStakeAmount = _amount.div(globalStakeMultiplier.getLatestValue()); // Readjust amount with multiplier - uint internalAmount = internalStakeAmount.mul(stakeMultiplier.getLatestValue()); + uint internalAmount = internalStakeAmount.mul(globalStakeMultiplier.getLatestValue()); // Checkpoint updated staking balance _modifyStakeBalance(_stakeAccount, internalStakeAmount, true); @@ -370,13 +386,13 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { { require(_amount > 0, ERROR_AMOUNT_ZERO); // Adjust amount by internal stake multiplier - uint internalStakeAmount = _amount.div(stakeMultiplier.getLatestValue()); + uint internalStakeAmount = _amount.div(globalStakeMultiplier.getLatestValue()); // Adjust internal stake amount to the ceiling of the division op internalStakeAmount += 1; // Readjust amount with multiplier - uint internalAmount = internalStakeAmount.mul(stakeMultiplier.getLatestValue()); + uint internalAmount = internalStakeAmount.mul(globalStakeMultiplier.getLatestValue()); // checkpoint updated staking balance _modifyStakeBalance(_stakeAccount, internalStakeAmount, false); diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index b767aad7f45..ebc7ebc209e 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -67,6 +67,9 @@ contract('DelegateManager', async (accounts) => { const stakerAccount = accounts[1] const delegatorAccount1 = accounts[2] + const stakerAccount2 = accounts[3] + + let slasherAccount = stakerAccount beforeEach(async () => { registry = await Registry.new() @@ -159,6 +162,28 @@ contract('DelegateManager', async (accounts) => { } } + + const printAccountStakeInfo = async (account) => { + console.log('') + let spFactoryStake + let totalInStakingContract + spFactoryStake = await serviceProviderFactory.getServiceProviderStake(account) + totalInStakingContract = await staking.totalStakedFor(account) + + let delegatedStake = web3.utils.toBN(0) + let delegators = await delegateManager.getDelegatorsList(account) + for (var i = 0; i < delegators.length; i++) { + let amountDelegated = await delegateManager.getTotalDelegatorStake(delegators[i]) + delegatedStake = delegatedStake.add(amountDelegated) + } + let outsideStake = spFactoryStake.add(delegatedStake) + // let tokensInStaking = await token.balanceOf(account) + // console.log(`${account} Total balance in stakingContract ${tokensInStaking}`) + console.log(`${account} SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside Stake: ${outsideStake} Staking: ${totalInStakingContract}`) + let stakeDiscrepancy = totalInStakingContract.sub(outsideStake) + console.log(`Internal (Staking) vs External (DelegateManager + SPFactory) Stake discrepancy: ${stakeDiscrepancy}`) + } + // Funds a claim, claims value for single SP address + delegators, slashes // Slashes claim amount const fundClaimSlash = async (slash) => { @@ -167,20 +192,10 @@ contract('DelegateManager', async (accounts) => { await ensureValidClaimPeriod() // Continue - let delegatedStake - let spFactoryStake - let totalInStakingContract - - totalInStakingContract = await staking.totalStakedFor(stakerAccount) - totalInStakingContract = await staking.totalStakedFor(stakerAccount) - delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - - spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - totalInStakingContract = await staking.totalStakedFor(stakerAccount) - - let tokensInStaking = await token.balanceOf(stakingAddress) - console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Staking: ${totalInStakingContract}`) - console.log(`Tokens in staking ${tokensInStaking}`) + await printAccountStakeInfo(stakerAccount) + await printAccountStakeInfo(stakerAccount2) + let totalStaked = await staking.totalStaked() + console.log(`Total tracked stake in Staking.sol: ${totalStaked}`) // Update SP Deployer Cut to 10% await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) @@ -189,53 +204,48 @@ contract('DelegateManager', async (accounts) => { // Perform claim await delegateManager.makeClaim({ from: stakerAccount }) + // Perform claim + await delegateManager.makeClaim({ from: stakerAccount2 }) console.log('') console.log('Claimed...') - - // let finalSpStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - // let finalDelegateStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - - spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - totalInStakingContract = await staking.totalStakedFor(stakerAccount) - delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - let outsideStake = spFactoryStake.add(delegatedStake) // let slashAmount = toWei(100) let currentMultiplier = await staking.getCurrentStakeMultiplier() - console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) - tokensInStaking = await token.balanceOf(stakingAddress) - console.log(`Tokens in staking ${tokensInStaking}`) + await printAccountStakeInfo(stakerAccount) + await printAccountStakeInfo(stakerAccount2) + + console.log('') console.log(`Stake multiplier: ${currentMultiplier}`) if (slash) { let slashNumerator = web3.utils.toBN(30) let slashDenominator = web3.utils.toBN(100) - + let totalInStakingContract = await staking.totalStakedFor(slasherAccount) let slashAmount = (totalInStakingContract.mul(slashNumerator)).div(slashDenominator) - console.log('') - console.log('Slashing...') - console.log(`Slash amount: ${slashAmount}, stake multiplier: ${currentMultiplier}`) + console.log(`Slashing ${slasherAccount} amount: ${slashAmount}`) // Perform slash functions - await delegateManager.slash(slashAmount, stakerAccount); - } + await delegateManager.slash(slashAmount, slasherAccount) - spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - totalInStakingContract = await staking.totalStakedFor(stakerAccount) - delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - outsideStake = spFactoryStake.add(delegatedStake) + // Switch slasher + if (slasherAccount === stakerAccount) { + slasherAccount = stakerAccount2 + } else { + slasherAccount = stakerAccount + } + console.log(`Next slash to ${slasherAccount}`) + } console.log('') - console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) - let stakeDiscrepancy = totalInStakingContract.sub(outsideStake) - console.log(`Internal (Staking) vs External (DelegateManager + SPFactory) Stake discrepancy: ${stakeDiscrepancy}`) - tokensInStaking = await token.balanceOf(stakingAddress) - console.log(`Tokens in staking ${tokensInStaking}`) + await printAccountStakeInfo(stakerAccount) + await printAccountStakeInfo(stakerAccount2) - let tokensAvailableVsTotalStaked = tokensInStaking.sub(totalInStakingContract) - console.log(`Tokens available to staking address - total tracked in staking contract = ${tokensAvailableVsTotalStaked}`) + let tokensInStaking = await token.balanceOf(stakingAddress) + totalStaked = await staking.totalStaked() + console.log(`Total tracked stake in Staking.sol: ${totalStaked}`) + console.log(`Total tokens for stakingAddress ${tokensInStaking}`) - let totalStaked = await staking.totalStaked() - console.log(`Total for all SP ${totalStaked}`) + let tokensAvailableVsTotalStaked = tokensInStaking.sub(totalStaked) + console.log(`Tokens available to staking address - total tracked in staking contract = ${tokensAvailableVsTotalStaked}`) console.log('----') } @@ -292,6 +302,7 @@ contract('DelegateManager', async (accounts) => { beforeEach(async () => { // Transfer 1000 tokens to staker await token.transfer(stakerAccount, INITIAL_BAL, { from: treasuryAddress }) + await token.transfer(stakerAccount2, INITIAL_BAL, { from: treasuryAddress }) let initialBal = await token.balanceOf(stakerAccount) @@ -303,6 +314,12 @@ contract('DelegateManager', async (accounts) => { DEFAULT_AMOUNT, stakerAccount) + await registerServiceProvider( + testDiscProvType, + testEndpoint1, + DEFAULT_AMOUNT, + stakerAccount2) + // Confirm event has correct amount assert.equal(regTx.stakedAmountInt, DEFAULT_AMOUNT) @@ -445,6 +462,9 @@ contract('DelegateManager', async (accounts) => { // TODO: Validate all // Transfer 1000 tokens to delegator await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) + let currentMultiplier = await staking.getCurrentStakeMultiplier() + console.log(`currentMultiplier ${currentMultiplier}`) + return let initialDelegateAmount = toWei(60) @@ -459,17 +479,18 @@ contract('DelegateManager', async (accounts) => { initialDelegateAmount, { from: delegatorAccount1 }) - let total = 110 + let total = 1000 let iterations = total while (iterations > 0) { let slash = false - if (iterations % 10 === 0) { + if (((total - iterations) + 1) % 10 === 0) { slash = true } console.log(`Round ${(total - iterations) + 1}`) await fundClaimSlash(slash) iterations-- } + // Summarize after execution let spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) let totalInStakingContract = await staking.totalStakedFor(stakerAccount) From b5f7146e5199b5ff0f636a82b94a0027f9565a60 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Wed, 1 Apr 2020 14:18:47 -0400 Subject: [PATCH 33/39] [eth-contracts] Rounds-based reward model (#348) * Eliminates stake multiplier in favor of proportional on-demand minting * Fully compatible with delegation working branch --- .../contracts/service/ClaimFactory.sol | 122 +++++-- .../contracts/service/DelegateManager.sol | 155 +++++++-- eth-contracts/contracts/staking/Staking.sol | 118 ++----- .../migrations/6_claim_factory_migration.js | 10 +- .../7_delegate_manager_migration.js | 22 ++ eth-contracts/scripts/truffle-test.sh | 6 +- eth-contracts/test/claimFactory.test.js | 79 +++-- eth-contracts/test/delegateManager.test.js | 314 +++++++++--------- eth-contracts/test/staking.test.js | 33 +- 9 files changed, 487 insertions(+), 372 deletions(-) create mode 100644 eth-contracts/migrations/7_delegate_manager_migration.js diff --git a/eth-contracts/contracts/service/ClaimFactory.sol b/eth-contracts/contracts/service/ClaimFactory.sol index 1aaf23c766d..8073c98c70d 100644 --- a/eth-contracts/contracts/service/ClaimFactory.sol +++ b/eth-contracts/contracts/service/ClaimFactory.sol @@ -1,75 +1,145 @@ pragma solidity ^0.5.0; import "../staking/Staking.sol"; +import "./registry/RegistryContract.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; +import "./interface/registry/RegistryInterface.sol"; // WORKING CONTRACT // Designed to automate claim funding, minting tokens as necessary -contract ClaimFactory { +contract ClaimFactory is RegistryContract { + using SafeMath for uint256; + RegistryInterface registry = RegistryInterface(0); // standard - imitates relationship between Ether and Wei uint8 private constant DECIMALS = 18; address tokenAddress; - address stakingAddress; + bytes32 stakingProxyOwnerKey; // Claim related configurations - uint claimBlockDiff = 10; - uint lastClaimBlock = 0; + uint fundRoundBlockDiff = 10; + uint fundBlock = 0; // 20 AUD - // TODO: Make this modifiable based on total staking pool? + // TODO: Make this modifiable based on total staking pool? uint fundingAmount = 20 * 10**uint256(DECIMALS); // 100 * 10**uint256(DECIMALS); + // Denotes current round + uint roundNumber = 0; + + // Total claimed so far in round + uint totalClaimedInRound = 0; + // Staking contract ref ERC20Mintable internal audiusToken; + event RoundInitiated( + uint _blockNumber, + uint _roundNumber, + uint _fundAmount + ); + + event ClaimProcessed( + address _claimer, + uint _rewards, + uint _oldTotal, + uint _newTotal + ); + constructor( address _tokenAddress, - address _stakingAddress + address _registryAddress, + bytes32 _stakingProxyOwnerKey ) public { tokenAddress = _tokenAddress; - stakingAddress = _stakingAddress; + stakingProxyOwnerKey = _stakingProxyOwnerKey; audiusToken = ERC20Mintable(tokenAddress); + registry = RegistryInterface(_registryAddress); // Allow a claim to be funded initially by subtracting the configured difference - lastClaimBlock = block.number - claimBlockDiff; + fundBlock = block.number - fundRoundBlockDiff; } - function getClaimBlockDifference() - external view returns (uint claimBlockDifference) + function getFundingRoundBlockDiff() + external view returns (uint blockDiff) { - return (claimBlockDiff); + return fundRoundBlockDiff; } - function getLastClaimedBlock() - external view returns (uint lastClaimedBlock) + function getLastFundBlock() + external view returns (uint lastFundBlock) { - return (lastClaimBlock); + return fundBlock; } - function getFundsPerClaim() + function getFundsPerRound() external view returns (uint amount) { - return (fundingAmount); + return fundingAmount; } - function initiateClaim() external { + function getTotalClaimedInRound() + external view returns (uint claimedAmount) + { + return totalClaimedInRound; + } + + // Start a new funding round + // TODO: Permission caller to contract deployer or governance contract + function initiateRound() external { require( - block.number - lastClaimBlock > claimBlockDiff, + block.number - fundBlock > fundRoundBlockDiff, "Required block difference not met"); + fundBlock = block.number; + totalClaimedInRound = 0; + roundNumber += 1; + + emit RoundInitiated( + fundBlock, + roundNumber, + fundingAmount + ); + } + + // TODO: Name this function better + // TODO: Permission caller + function processClaim(address _claimer) external returns (uint newAccountTotal) { + address stakingAddress = registry.getContract(stakingProxyOwnerKey); + Staking stakingContract = Staking(stakingAddress); + // Prevent duplicate claim + uint lastUserClaimBlock = stakingContract.lastClaimedFor(_claimer); + require(lastUserClaimBlock <= fundBlock, "Claim already processed for user"); + + uint totalStakedAtFundBlockForClaimer = stakingContract.totalStakedForAt( + _claimer, + fundBlock); + uint totalStakedAtFundBlock = stakingContract.totalStakedAt(fundBlock); + uint rewardsForClaimer = ( + totalStakedAtFundBlockForClaimer.mul(fundingAmount) + ).div(totalStakedAtFundBlock); - bool minted = audiusToken.mint(address(this), fundingAmount); + bool minted = audiusToken.mint(address(this), rewardsForClaimer); require(minted, "New tokens must be minted"); // Approve token transfer to staking contract address - audiusToken.approve(stakingAddress, fundingAmount); + audiusToken.approve(stakingAddress, rewardsForClaimer); - // Fund staking contract with proceeds - Staking stakingContract = Staking(stakingAddress); - stakingContract.fundNewClaim(fundingAmount); + // Transfer rewards + stakingContract.stakeRewards(rewardsForClaimer, _claimer); + + // Update round claim value + totalClaimedInRound += rewardsForClaimer; + + // Update round claim value + uint newTotal = stakingContract.totalStakedFor(_claimer); + + emit ClaimProcessed( + _claimer, + rewardsForClaimer, + totalStakedAtFundBlockForClaimer, + newTotal + ); - // Increment by claim difference - // Ensures funding of claims is repeatable given the right block difference - lastClaimBlock = lastClaimBlock + claimBlockDiff; + return newTotal; } } diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index ea39c734fde..83b5b12e379 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -7,6 +7,7 @@ import "./interface/registry/RegistryInterface.sol"; import "../staking/Staking.sol"; import "./ServiceProviderFactory.sol"; +import "./ClaimFactory.sol"; // WORKING CONTRACT @@ -20,13 +21,20 @@ contract DelegateManager is RegistryContract { bytes32 stakingProxyOwnerKey; bytes32 serviceProviderFactoryKey; + bytes32 claimFactoryKey; // Staking contract ref ERC20Mintable internal audiusToken; - // Service provider address -> list of delegators - // TODO: Bounded list - mapping (address => address[]) spDelegates; + // Struct representing total delegated to SP and list of delegators + // TODO: Bound list + struct ServiceProviderDelegateInfo { + uint totalDelegatedStake; + address[] delegators; + } + + // Service provider address -> ServiceProviderDelegateInfo + mapping (address => ServiceProviderDelegateInfo) spDelegateInfo; // Total staked for a given delegator mapping (address => uint) delegatorStakeTotal; @@ -42,17 +50,43 @@ contract DelegateManager is RegistryContract { uint256 test, string msg); + event IncreaseDelegatedStake( + address _delegator, + address _serviceProvider, + uint _increaseAmount + ); + + event DecreaseDelegatedStake( + address _delegator, + address _serviceProvider, + uint _decreaseAmount + ); + + event Claim( + address _claimer, + uint _rewards, + uint newTotal + ); + + event Slash( + address _target, + uint _amount, + uint _newTotal + ); + constructor( address _tokenAddress, address _registryAddress, bytes32 _stakingProxyOwnerKey, - bytes32 _serviceProviderFactoryKey + bytes32 _serviceProviderFactoryKey, + bytes32 _claimFactoryKey ) public { tokenAddress = _tokenAddress; audiusToken = ERC20Mintable(tokenAddress); registry = RegistryInterface(_registryAddress); stakingProxyOwnerKey = _stakingProxyOwnerKey; serviceProviderFactoryKey = _serviceProviderFactoryKey; + claimFactoryKey = _claimFactoryKey; } function increaseDelegatedStake( @@ -70,16 +104,25 @@ contract DelegateManager is RegistryContract { // Stake on behalf of target service provider stakingContract.delegateStakeFor( - _target, - delegator, - _amount, - empty + _target, + delegator, + _amount, + empty + ); + + emit IncreaseDelegatedStake( + delegator, + _target, + _amount ); // Update list of delegators to SP if necessary // TODO: Any validation on returned value? updateServiceProviderDelegatorsIfNecessary(delegator, _target); + // Update total delegated for SP + spDelegateInfo[_target].totalDelegatedStake += _amount; + // Update amount staked from this delegator to targeted service provider delegateInfo[delegator][_target] += _amount; @@ -112,39 +155,61 @@ contract DelegateManager is RegistryContract { // Stake on behalf of target service provider stakingContract.undelegateStakeFor( - _target, - delegator, - _amount, - empty + _target, + delegator, + _amount, + empty ); + emit DecreaseDelegatedStake( + delegator, + _target, + _amount); + // Update amount staked from this delegator to targeted service provider delegateInfo[delegator][_target] -= _amount; // Update total delegated stake delegatorStakeTotal[delegator] -= _amount; + // Update total delegated for SP + spDelegateInfo[_target].totalDelegatedStake -= _amount; + // Remove from delegators list if no delegated stake remaining if (delegateInfo[delegator][_target] == 0) { bool foundDelegator; uint delegatorIndex; - for (uint i = 0; i < spDelegates[_target].length; i++) { - if (spDelegates[_target][i] == delegator) { + for (uint i = 0; i < spDelegateInfo[_target].delegators.length; i++) { + if (spDelegateInfo[_target].delegators[i] == delegator) { foundDelegator = true; delegatorIndex = i; } } // Overwrite and shrink delegators list - spDelegates[_target][delegatorIndex] = spDelegates[_target][spDelegates[_target].length - 1]; - spDelegates[_target].length--; + uint lastIndex = spDelegateInfo[_target].delegators.length - 1; + spDelegateInfo[_target].delegators[delegatorIndex] = spDelegateInfo[_target].delegators[lastIndex]; + spDelegateInfo[_target].delegators.length--; } // Return new total return delegateInfo[delegator][_target]; } - function makeClaim() external { + /* + TODO: See if its worth splitting processClaim into a separate tx? + Primary concern is around gas consumption... + This tx ends up minting tokens, transferring to staking, and doing below updates + Can be stress tested and split out if needed + */ + // Distribute proceeds of reward + function claimRewards() external { + ClaimFactory claimFactory = ClaimFactory( + registry.getContract(claimFactoryKey) + ); + // Process claim for msg.sender + claimFactory.processClaim(msg.sender); + // address claimer = msg.sender; ServiceProviderFactory spFactory = ServiceProviderFactory( registry.getContract(serviceProviderFactoryKey) @@ -161,14 +226,7 @@ contract DelegateManager is RegistryContract { require(totalBalanceInSPFactory > 0, "Service Provider stake required"); // Amount in delegate manager staked to service provider - // TODO: Consider caching this value - uint totalBalanceInDelegateManager = 0; - for (uint i = 0; i < spDelegates[msg.sender].length; i++) { - address delegator = spDelegates[msg.sender][i]; - uint delegateStakeToSP = delegateInfo[delegator][msg.sender]; - totalBalanceInDelegateManager += delegateStakeToSP; - } - + uint totalBalanceInDelegateManager = spDelegateInfo[msg.sender].totalDelegatedStake; uint totalBalanceOutsideStaking = totalBalanceInSPFactory + totalBalanceInDelegateManager; // Require claim availability @@ -178,14 +236,18 @@ contract DelegateManager is RegistryContract { // Equal to (balance in staking) - ((balance in sp factory) + (balance in delegate manager)) uint totalRewards = totalBalanceInStaking - totalBalanceOutsideStaking; + // Emit claim event + emit Claim(msg.sender, totalRewards, totalBalanceInStaking); + uint deployerCut = spFactory.getServiceProviderDeployerCut(msg.sender); uint deployerCutBase = spFactory.getServiceProviderDeployerCutBase(); uint spDeployerCutRewards = 0; + uint totalDelegatedStakeIncrease = 0; // Traverse all delegates and calculate their rewards // As each delegate reward is calculated, increment SP cut reward accordingly - for (uint i = 0; i < spDelegates[msg.sender].length; i++) { - address delegator = spDelegates[msg.sender][i]; + for (uint i = 0; i < spDelegateInfo[msg.sender].delegators.length; i++) { + address delegator = spDelegateInfo[msg.sender].delegators[i]; uint delegateStakeToSP = delegateInfo[delegator][msg.sender]; // Calculate rewards by ((delegateStakeToSP / totalBalanceOutsideStaking) * totalRewards) uint rewardsPriorToSPCut = ( @@ -199,8 +261,12 @@ contract DelegateManager is RegistryContract { // delegateReward = rewardsPriorToSPCut - spDeployerCut; delegateInfo[delegator][msg.sender] += (rewardsPriorToSPCut - spDeployerCut); delegatorStakeTotal[delegator] += (rewardsPriorToSPCut - spDeployerCut); + totalDelegatedStakeIncrease += (rewardsPriorToSPCut - spDeployerCut); } + // Update total delegated to this SP + spDelegateInfo[msg.sender].totalDelegatedStake += totalDelegatedStakeIncrease; + // TODO: Validate below with test cases uint spRewardShare = ( totalBalanceInSPFactory.mul(totalRewards) @@ -224,7 +290,9 @@ contract DelegateManager is RegistryContract { // Amount stored in staking contract for owner uint totalBalanceInStakingPreSlash = stakingContract.totalStakedFor(_slashAddress); require(totalBalanceInStakingPreSlash > 0, "Stake required prior to slash"); - require(totalBalanceInStakingPreSlash > _amount, "Cannot slash more than total currently staked"); + require( + totalBalanceInStakingPreSlash > _amount, + "Cannot slash more than total currently staked"); // Amount in sp factory for slash target uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(_slashAddress); @@ -234,10 +302,15 @@ contract DelegateManager is RegistryContract { stakingContract.slash(_amount, _slashAddress); uint totalBalanceInStakingAfterSlash = stakingContract.totalStakedFor(_slashAddress); + // Emit slash event + emit Slash(_slashAddress, _amount, totalBalanceInStakingAfterSlash); + + uint totalDelegatedStakeDecrease = 0; + // For each delegator and deployer, recalculate new value // newStakeAmount = newStakeAmount * (oldStakeAmount / totalBalancePreSlash) - for (uint i = 0; i < spDelegates[_slashAddress].length; i++) { - address delegator = spDelegates[_slashAddress][i]; + for (uint i = 0; i < spDelegateInfo[_slashAddress].delegators.length; i++) { + address delegator = spDelegateInfo[_slashAddress].delegators[i]; uint preSlashDelegateStake = delegateInfo[delegator][_slashAddress]; uint newDelegateStake = ( totalBalanceInStakingAfterSlash.mul(preSlashDelegateStake) @@ -245,8 +318,13 @@ contract DelegateManager is RegistryContract { uint slashAmountForDelegator = preSlashDelegateStake.sub(newDelegateStake); delegateInfo[delegator][_slashAddress] -= (slashAmountForDelegator); delegatorStakeTotal[delegator] -= (slashAmountForDelegator); + // Update total decrease amount + totalDelegatedStakeDecrease += slashAmountForDelegator; } + // Update total delegated to this SP + spDelegateInfo[msg.sender].totalDelegatedStake -= totalDelegatedStakeDecrease; + // Recalculate SP direct stake uint newSpBalance = ( totalBalanceInStakingAfterSlash.mul(totalBalanceInSPFactory) @@ -260,7 +338,16 @@ contract DelegateManager is RegistryContract { function getDelegatorsList(address _sp) external view returns (address[] memory dels) { - return spDelegates[_sp]; + return spDelegateInfo[_sp].delegators; + } + + /** + * @notice Total delegated to a service provider + */ + function getTotalDelegatedToServiceProvider(address _sp) + external view returns (uint total) + { + return spDelegateInfo[_sp].totalDelegatedStake; } /** @@ -286,13 +373,13 @@ contract DelegateManager is RegistryContract { address _serviceProvider ) internal returns (bool exists) { - for (uint i = 0; i < spDelegates[_serviceProvider].length; i++) { - if (spDelegates[_serviceProvider][i] == _delegator) { + for (uint i = 0; i < spDelegateInfo[_serviceProvider].delegators.length; i++) { + if (spDelegateInfo[_serviceProvider].delegators[i] == _delegator) { return true; } } // If not found, update list of delegates - spDelegates[_serviceProvider].push(_delegator); + spDelegateInfo[_serviceProvider].delegators.push(_delegator); return false; } } diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index bc19a14dd54..edf83fd25c0 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -26,16 +26,12 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // Reward tracking info uint256 internal currentClaimBlock; - uint256 internal currentClaimableAmount; struct Account { Checkpointing.History stakedHistory; Checkpointing.History claimHistory; } - // Multiplier used to increase funds - Checkpointing.History globalStakeMultiplier; - ERC20 internal stakingToken; mapping (address => Account) internal accounts; Checkpointing.History internal totalStakedHistory; @@ -62,63 +58,27 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { initialized(); stakingToken = ERC20(_stakingToken); treasuryAddress = _treasuryAddress; - - // Initialize claim values to zero, disabling claim prior to initial funding - currentClaimBlock = 0; - currentClaimableAmount = 0; - - // TODO: Finalize multiplier value - // uint256 initialMultiplier = 10**uint256(DECIMALS); - uint256 initialMultiplier = 10**uint256(9); - // Initialize multiplier history value - globalStakeMultiplier.add64(getBlockNumber64(), initialMultiplier); } /* External functions */ /** - * @notice Funds `_amount` of tokens from msg.sender into treasury stake + * @notice Funds `_amount` of tokens from ClaimFactory to target account */ - function fundNewClaim(uint256 _amount) external isInitialized { + function stakeRewards(uint256 _amount, address _stakerAccount) external isInitialized { // TODO: Add additional require statements here... + // TODO: Permission to claimFactory + // Stake for incoming account + // Transfer from msg.sender, in this case ClaimFactory + // bytes memory empty; + _stakeFor( + _stakerAccount, + msg.sender, + _amount, + bytes('')); // TODO: RM bytes requirement if unused - // Update multiplier, total stake - uint256 currentMultiplier = globalStakeMultiplier.getLatestValue(); - uint256 totalStake = totalStakedHistory.getLatestValue(); - - uint internalClaimAmount = _amount.div(currentMultiplier); - uint internalClaimAdjusted = internalClaimAmount.mul(currentMultiplier); - - // Pull tokens into Staking contract from caller - stakingToken.safeTransferFrom(msg.sender, address(this), internalClaimAdjusted); - - // Increase total supply by input amount - _modifyTotalStaked(internalClaimAdjusted, true); - - uint256 multiplierDifference = (currentMultiplier.mul(internalClaimAdjusted)).div(totalStake); - uint256 newMultiplier = currentMultiplier.add(multiplierDifference); - globalStakeMultiplier.add64(getBlockNumber64(), newMultiplier); - - /* - // Calculate and distribute funds by updating multiplier - // Proportionally increases multiplier equivalent to incoming token value - // newMultiplier = currentMultiplier + ((multiplier * _amount) / total) - // Ex: - // multiplier = 1.0, total = 200,000, address1 = 200,000 * 1.0 (has all value) - // Incoming claim fund of 100,000 - // newMultiplier = 1.0 + ((1.0 * 100,000) / 200,000) = 1.5 - // address1 = 200,000 * 1.5 = 300,000 <-- Total value increased by fundAmount, with newMultiplier - - uint256 multiplierDifference = (currentMultiplier.mul(_amount)).div(totalStake); - uint256 newMultiplier = currentMultiplier.add(multiplierDifference); - globalStakeMultiplier.add64(getBlockNumber64(), newMultiplier); - - // Pull tokens into Staking contract from caller - stakingToken.safeTransferFrom(msg.sender, address(this), _amount); - - // Increase total supply by input amount - _modifyTotalStaked(_amount, true); - */ + // Update claim history even if no value claimed + accounts[_stakerAccount].claimHistory.add64(getBlockNumber64(), _amount); } /** @@ -134,7 +94,6 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { require(_amount > 0, ERROR_AMOUNT_ZERO); // Transfer slashed tokens to treasury address - // Amount is adjusted in _unstakeFor // TODO: Burn with actual ERC token call _unstakeFor( _slashAddress, @@ -273,13 +232,6 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { return true; } - /** - * @return Current stake multiplier - */ - function getCurrentStakeMultiplier() public view isInitialized returns (uint256) { - return globalStakeMultiplier.getLatestValue(); - } - /** * @notice Get last time `_accountAddress` modified its staked balance * @param _accountAddress Account requesting for @@ -298,13 +250,6 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { return accounts[_accountAddress].claimHistory.lastUpdated(); } - /** - * @notice Get info relating to current claim status - */ - function getClaimInfo() external view isInitialized returns (uint256, uint256) { - return (currentClaimableAmount, currentClaimBlock); - } - /** * @notice Get the total amount of tokens staked by `_accountAddress` at block number `_blockNumber` * @param _accountAddress Account requesting for @@ -312,7 +257,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { * @return The amount of tokens staked by the account at the given block number */ function totalStakedForAt(address _accountAddress, uint256 _blockNumber) external view returns (uint256) { - return (accounts[_accountAddress].stakedHistory.get(_blockNumber)).mul(globalStakeMultiplier.get(_blockNumber)); + return accounts[_accountAddress].stakedHistory.get(_blockNumber); } /** @@ -333,7 +278,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { */ function totalStakedFor(address _accountAddress) public view returns (uint256) { // we assume it's not possible to stake in the future - return (accounts[_accountAddress].stakedHistory.getLatestValue()).mul(globalStakeMultiplier.getLatestValue()); + return accounts[_accountAddress].stakedHistory.getLatestValue(); } /** @@ -356,24 +301,18 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { // staking 0 tokens is invalid require(_amount > 0, ERROR_AMOUNT_ZERO); - // Adjust amount by internal stake multiplier - uint internalStakeAmount = _amount.div(globalStakeMultiplier.getLatestValue()); - - // Readjust amount with multiplier - uint internalAmount = internalStakeAmount.mul(globalStakeMultiplier.getLatestValue()); - // Checkpoint updated staking balance - _modifyStakeBalance(_stakeAccount, internalStakeAmount, true); + _modifyStakeBalance(_stakeAccount, _amount, true); // checkpoint total supply - _modifyTotalStaked(internalAmount, true); + _modifyTotalStaked(_amount, true); // pull tokens into Staking contract - stakingToken.safeTransferFrom(_transferAccount, address(this), internalAmount); + stakingToken.safeTransferFrom(_transferAccount, address(this), _amount); emit Staked( _stakeAccount, - internalAmount, + _amount, totalStakedFor(_stakeAccount), _data); } @@ -385,34 +324,24 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { bytes memory _data) internal { require(_amount > 0, ERROR_AMOUNT_ZERO); - // Adjust amount by internal stake multiplier - uint internalStakeAmount = _amount.div(globalStakeMultiplier.getLatestValue()); - - // Adjust internal stake amount to the ceiling of the division op - internalStakeAmount += 1; - - // Readjust amount with multiplier - uint internalAmount = internalStakeAmount.mul(globalStakeMultiplier.getLatestValue()); // checkpoint updated staking balance - _modifyStakeBalance(_stakeAccount, internalStakeAmount, false); + _modifyStakeBalance(_stakeAccount, _amount, false); // checkpoint total supply - _modifyTotalStaked(internalAmount, false); + _modifyTotalStaked(_amount, false); // transfer tokens - stakingToken.safeTransfer(_transferAccount, internalAmount); + stakingToken.safeTransfer(_transferAccount, _amount); emit Unstaked( _stakeAccount, - internalAmount, + _amount, totalStakedFor(_stakeAccount), _data); } - // Note that _by value has been adjusted for the stake multiplier prior to getting passed in function _modifyStakeBalance(address _accountAddress, uint256 _by, bool _increase) internal { - // currentInternalStake represents the internal stake value, without multiplier adjustment uint256 currentInternalStake = accounts[_accountAddress].stakedHistory.getLatestValue(); uint256 newStake; @@ -451,7 +380,6 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { _modifyStakeBalance(_from, _amount, false); _modifyStakeBalance(_to, _amount, true); - // TODO: Emit multiplier, OR adjust and emit correct amount emit StakeTransferred(_from,_amount, _to); } } diff --git a/eth-contracts/migrations/6_claim_factory_migration.js b/eth-contracts/migrations/6_claim_factory_migration.js index 3c1c36ffc7c..b3116290390 100644 --- a/eth-contracts/migrations/6_claim_factory_migration.js +++ b/eth-contracts/migrations/6_claim_factory_migration.js @@ -1,20 +1,28 @@ const AudiusToken = artifacts.require('AudiusToken') const ClaimFactory = artifacts.require('ClaimFactory') const OwnedUpgradeabilityProxy = artifacts.require('OwnedUpgradeabilityProxy') +const Registry = artifacts.require('Registry') +const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') +const claimFactoryKey = web3.utils.utf8ToHex('ClaimFactory') module.exports = (deployer, network, accounts) => { deployer.then(async () => { let proxy = await OwnedUpgradeabilityProxy.deployed() + let registry = await Registry.deployed() let stakingAddress = proxy.address // Deploy new ClaimFactory await deployer.deploy( ClaimFactory, AudiusToken.address, - stakingAddress) + registry.address, + ownedUpgradeabilityProxyKey) let claimFactory = await ClaimFactory.deployed() + // Register claimFactory + await registry.addContract(claimFactoryKey, claimFactory.address) + // Replace AudiusToken artifact with AudiusToken.at('0x...') if needed let audiusToken = await AudiusToken.at(AudiusToken.address) diff --git a/eth-contracts/migrations/7_delegate_manager_migration.js b/eth-contracts/migrations/7_delegate_manager_migration.js new file mode 100644 index 00000000000..6a73ac574bd --- /dev/null +++ b/eth-contracts/migrations/7_delegate_manager_migration.js @@ -0,0 +1,22 @@ +const DelegateManager = artifacts.require('DelegateManager') +const AudiusToken = artifacts.require('AudiusToken') +const Registry = artifacts.require('Registry') +const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') +const claimFactoryKey = web3.utils.utf8ToHex('ClaimFactory') +const serviceProviderFactoryKey = web3.utils.utf8ToHex('ServiceProviderFactory') + +module.exports = (deployer, network, accounts) => { + deployer.then(async () => { + let registry = await Registry.deployed() + let audiusToken = await AudiusToken.at(AudiusToken.address) + + // Deploy DelegateManager + await deployer.deploy( + DelegateManager, + audiusToken.address, + registry.address, + ownedUpgradeabilityProxyKey, + serviceProviderFactoryKey, + claimFactoryKey) + }) +} diff --git a/eth-contracts/scripts/truffle-test.sh b/eth-contracts/scripts/truffle-test.sh index 1b269624901..076eb2a87c1 100755 --- a/eth-contracts/scripts/truffle-test.sh +++ b/eth-contracts/scripts/truffle-test.sh @@ -26,7 +26,11 @@ docker rm -f audius_ganache_cli_eth_contracts_test # echo commands from here out # useful to know what the test script is actually doing set -x -docker run --name audius_ganache_cli_eth_contracts_test -d -p 8556:8545 trufflesuite/ganache-cli:latest -h 0.0.0.0 -l 8000000 +# Ganache parameters +# -h = hostname +# -l = gas limit on block +# -a = number of accounts to generate on startup +docker run --name audius_ganache_cli_eth_contracts_test -d -p 8556:8545 trufflesuite/ganache-cli:latest -h 0.0.0.0 -l 8000000 -a 50 # compile and lint ./node_modules/.bin/truffle compile diff --git a/eth-contracts/test/claimFactory.test.js b/eth-contracts/test/claimFactory.test.js index 5f4aca0f450..1d7f3823771 100644 --- a/eth-contracts/test/claimFactory.test.js +++ b/eth-contracts/test/claimFactory.test.js @@ -1,11 +1,14 @@ import * as _lib from './_lib/lib.js' const AudiusToken = artifacts.require('AudiusToken') +const Registry = artifacts.require('Registry') const ClaimFactory = artifacts.require('ClaimFactory') const OwnedUpgradeabilityProxy = artifacts.require('OwnedUpgradeabilityProxy') const Staking = artifacts.require('Staking') const encodeCall = require('./encodeCall') +const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') + const fromBn = n => parseInt(n.valueOf(), 10) const toWei = (aud) => { @@ -26,8 +29,10 @@ contract('ClaimFactory', async (accounts) => { let proxyOwner = treasuryAddress let claimFactory let token + let registry let staking + let staker let proxy let impl0 let BN = web3.utils.BN @@ -52,8 +57,12 @@ contract('ClaimFactory', async (accounts) => { } beforeEach(async () => { - token = await AudiusToken.new({ from: accounts[0] }) + registry = await Registry.new() proxy = await OwnedUpgradeabilityProxy.new({ from: proxyOwner }) + // Add proxy to registry + await registry.addContract(ownedUpgradeabilityProxyKey, proxy.address) + + token = await AudiusToken.new({ from: accounts[0] }) impl0 = await Staking.new() // Create initialization data @@ -69,11 +78,13 @@ contract('ClaimFactory', async (accounts) => { { from: proxyOwner }) staking = await Staking.at(proxy.address) + staker = accounts[2] // Create new claim factory instance claimFactory = await ClaimFactory.new( token.address, - proxy.address, + registry.address, + ownedUpgradeabilityProxyKey, { from: accounts[0] }) // Register new contract as a minter, from the same address that deployed the contract @@ -91,26 +102,27 @@ contract('ClaimFactory', async (accounts) => { 'Expect zero treasury stake prior to claim funding') // Stake default amount - let staker = accounts[2] await approveTransferAndStake(DEFAULT_AMOUNT, staker) // Get funds per claim - let fundsPerClaim = await claimFactory.getFundsPerClaim() + let fundsPerRound = await claimFactory.getFundsPerRound() + + await claimFactory.initiateRound() + await claimFactory.processClaim(staker) - await claimFactory.initiateClaim() totalStaked = await staking.totalStaked() assert.isTrue( - totalStaked.eq(fundsPerClaim.add(DEFAULT_AMOUNT)), + totalStaked.eq(fundsPerRound.add(DEFAULT_AMOUNT)), 'Expect single round of funding + initial stake at this time') // Confirm another claim cannot be immediately funded await _lib.assertRevert( - claimFactory.initiateClaim(), + claimFactory.initiateRound(), 'Required block difference not met') }) - it('Initiate multiple claims after 1x claim block diff', async () => { + it('Initiate multiple rounds, 1x block diff', async () => { // Get amount staked let totalStaked = await staking.totalStaked() assert.isTrue( @@ -118,29 +130,29 @@ contract('ClaimFactory', async (accounts) => { 'Expect zero stake prior to claim funding') // Stake default amount - let staker = accounts[2] await approveTransferAndStake(DEFAULT_AMOUNT, staker) // Get funds per claim - let fundsPerClaim = await claimFactory.getFundsPerClaim() + let fundsPerClaim = await claimFactory.getFundsPerRound() - // Initiate claim - await claimFactory.initiateClaim() + // Initiate round + await claimFactory.initiateRound() + await claimFactory.processClaim(staker) totalStaked = await staking.totalStaked() assert.isTrue( totalStaked.eq(fundsPerClaim.add(DEFAULT_AMOUNT)), 'Expect single round of funding + initial stake at this time') - // Confirm another claim cannot be immediately funded + // Confirm another round cannot be immediately funded await _lib.assertRevert( - claimFactory.initiateClaim(), + claimFactory.initiateRound(), 'Required block difference not met') let currentBlock = await getLatestBlock() let currentBlockNum = currentBlock.number - let lastClaimBlock = await claimFactory.getLastClaimedBlock() - let claimDiff = await claimFactory.getClaimBlockDifference() + let lastClaimBlock = await claimFactory.getLastFundBlock() + let claimDiff = await claimFactory.getFundingRoundBlockDiff() let nextClaimBlock = lastClaimBlock.add(claimDiff) // Advance blocks to the next valid claim @@ -158,8 +170,9 @@ contract('ClaimFactory', async (accounts) => { let accountStakeBeforeSecondClaim = await staking.totalStakedFor(staker) - // Initiate another claim - await claimFactory.initiateClaim() + // Initiate another round + await claimFactory.initiateRound() + await claimFactory.processClaim(staker) totalStaked = await staking.totalStaked() let finalAcctStake = await staking.totalStakedFor(staker) let expectedFinalValue = accountStakeBeforeSecondClaim.add(fundsPerClaim) @@ -171,9 +184,9 @@ contract('ClaimFactory', async (accounts) => { 'Expect additional increase in stake after 2nd claim') }) - it('Initiate multiple claims consecutively after 2x claim block diff', async () => { + it('Initiate single claim after 2x claim block diff', async () => { // Get funds per claim - let fundsPerClaim = await claimFactory.getFundsPerClaim() + let fundsPerClaim = await claimFactory.getFundsPerRound() // Get amount staked let totalStaked = await staking.totalStaked() assert.isTrue( @@ -181,13 +194,12 @@ contract('ClaimFactory', async (accounts) => { 'Expect zero stake prior to claim funding') // Stake default amount - let staker = accounts[2] await approveTransferAndStake(DEFAULT_AMOUNT, staker) let currentBlock = await getLatestBlock() let currentBlockNum = currentBlock.number - let lastClaimBlock = await claimFactory.getLastClaimedBlock() - let claimDiff = await claimFactory.getClaimBlockDifference() + let lastClaimBlock = await claimFactory.getLastFundBlock() + let claimDiff = await claimFactory.getFundingRoundBlockDiff() let twiceClaimDiff = claimDiff.mul(new BN('2')) let nextClaimBlock = lastClaimBlock.add(twiceClaimDiff) @@ -199,30 +211,17 @@ contract('ClaimFactory', async (accounts) => { } // Initiate claim - await claimFactory.initiateClaim() + await claimFactory.initiateRound() + await claimFactory.processClaim(staker) totalStaked = await staking.totalStaked() assert.isTrue( totalStaked.eq(fundsPerClaim.add(DEFAULT_AMOUNT)), 'Expect single round of funding + initial stake at this time') - let accountStakeBeforeSecondClaim = await staking.totalStakedFor(staker) - - // Initiate another claim - await claimFactory.initiateClaim() - totalStaked = await staking.totalStaked() - let finalAcctStake = await staking.totalStakedFor(staker) - let expectedFinalValue = accountStakeBeforeSecondClaim.add(fundsPerClaim) - - // Note - we convert ouf of BN format here to handle infinitesimal precision loss - assert.equal( - fromBn(finalAcctStake), - fromBn(expectedFinalValue), - 'Expect additional increase in stake after 2nd claim') - - // Confirm another claim cannot be immediately funded + // Confirm another round cannot be immediately funded, despite 2x block diff await _lib.assertRevert( - claimFactory.initiateClaim(), + claimFactory.initiateRound(), 'Required block difference not met') }) }) diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index ebc7ebc209e..dce591c0f8f 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -36,6 +36,7 @@ const getTokenBalance2 = async (token, account) => fromWei(await token.balanceOf const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') const serviceProviderStorageKey = web3.utils.utf8ToHex('ServiceProviderStorage') const serviceProviderFactoryKey = web3.utils.utf8ToHex('ServiceProviderFactory') +const claimFactoryKey = web3.utils.utf8ToHex('ClaimFactory') const testDiscProvType = web3.utils.utf8ToHex('discovery-provider') const testCreatorNodeType = web3.utils.utf8ToHex('creator-node') @@ -76,7 +77,7 @@ contract('DelegateManager', async (accounts) => { proxy = await OwnedUpgradeabilityProxy.new({ from: proxyOwner }) - // Deploy registry + // Add proxy to registry await registry.addContract(ownedUpgradeabilityProxyKey, proxy.address) token = await AudiusToken.new({ from: treasuryAddress }) @@ -117,9 +118,12 @@ contract('DelegateManager', async (accounts) => { // Create new claim factory instance claimFactory = await ClaimFactory.new( token.address, - proxy.address, + registry.address, + ownedUpgradeabilityProxyKey, { from: accounts[0] }) + await registry.addContract(claimFactoryKey, claimFactory.address) + // Register new contract as a minter, from the same address that deployed the contract await token.addMinter(claimFactory.address, { from: accounts[0] }) @@ -127,7 +131,8 @@ contract('DelegateManager', async (accounts) => { token.address, registry.address, ownedUpgradeabilityProxyKey, - serviceProviderFactoryKey) + serviceProviderFactoryKey, + claimFactoryKey) }) /* Helper functions */ @@ -152,9 +157,9 @@ contract('DelegateManager', async (accounts) => { const ensureValidClaimPeriod = async () => { let currentBlock = await web3.eth.getBlock('latest') let currentBlockNum = currentBlock.number - let lastClaimBlock = await claimFactory.getLastClaimedBlock() - let claimDiff = await claimFactory.getClaimBlockDifference() - let nextClaimBlock = lastClaimBlock.add(claimDiff) + let lastFundBlock = await claimFactory.getLastFundBlock() + let claimDiff = await claimFactory.getFundingRoundBlockDiff() + let nextClaimBlock = lastFundBlock.add(claimDiff) while (currentBlockNum < nextClaimBlock) { await _lib.advanceBlock(web3) currentBlock = await web3.eth.getBlock('latest') @@ -162,7 +167,6 @@ contract('DelegateManager', async (accounts) => { } } - const printAccountStakeInfo = async (account) => { console.log('') let spFactoryStake @@ -184,119 +188,6 @@ contract('DelegateManager', async (accounts) => { console.log(`Internal (Staking) vs External (DelegateManager + SPFactory) Stake discrepancy: ${stakeDiscrepancy}`) } - // Funds a claim, claims value for single SP address + delegators, slashes - // Slashes claim amount - const fundClaimSlash = async (slash) => { - console.log('----') - // Ensure block difference is met prior to any operations - await ensureValidClaimPeriod() - - // Continue - await printAccountStakeInfo(stakerAccount) - await printAccountStakeInfo(stakerAccount2) - let totalStaked = await staking.totalStaked() - console.log(`Total tracked stake in Staking.sol: ${totalStaked}`) - - // Update SP Deployer Cut to 10% - await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) - // Fund new claim - await claimFactory.initiateClaim() - - // Perform claim - await delegateManager.makeClaim({ from: stakerAccount }) - // Perform claim - await delegateManager.makeClaim({ from: stakerAccount2 }) - console.log('') - console.log('Claimed...') - // let slashAmount = toWei(100) - - let currentMultiplier = await staking.getCurrentStakeMultiplier() - - await printAccountStakeInfo(stakerAccount) - await printAccountStakeInfo(stakerAccount2) - - console.log('') - console.log(`Stake multiplier: ${currentMultiplier}`) - if (slash) { - let slashNumerator = web3.utils.toBN(30) - let slashDenominator = web3.utils.toBN(100) - let totalInStakingContract = await staking.totalStakedFor(slasherAccount) - let slashAmount = (totalInStakingContract.mul(slashNumerator)).div(slashDenominator) - console.log(`Slashing ${slasherAccount} amount: ${slashAmount}`) - // Perform slash functions - await delegateManager.slash(slashAmount, slasherAccount) - - // Switch slasher - if (slasherAccount === stakerAccount) { - slasherAccount = stakerAccount2 - } else { - slasherAccount = stakerAccount - } - console.log(`Next slash to ${slasherAccount}`) - } - console.log('') - - await printAccountStakeInfo(stakerAccount) - await printAccountStakeInfo(stakerAccount2) - - let tokensInStaking = await token.balanceOf(stakingAddress) - totalStaked = await staking.totalStaked() - console.log(`Total tracked stake in Staking.sol: ${totalStaked}`) - console.log(`Total tokens for stakingAddress ${tokensInStaking}`) - - let tokensAvailableVsTotalStaked = tokensInStaking.sub(totalStaked) - console.log(`Tokens available to staking address - total tracked in staking contract = ${tokensAvailableVsTotalStaked}`) - console.log('----') - } - - const increaseRegisteredProviderStake = async (type, endpoint, increase, account) => { - // Approve token transfer - await token.approve( - stakingAddress, - increase, - { from: account }) - - let tx = await serviceProviderFactory.increaseStake( - increase, - { from: account }) - - let args = tx.logs.find(log => log.event === 'UpdatedStakeAmount').args - // console.dir(args, { depth: 5 }) - } - - const getStakeAmountForAccount = async (account) => { - return fromBn(await staking.totalStakedFor(account)) - } - - const decreaseRegisteredProviderStake = async (decrease, account) => { - // Approve token transfer from staking contract to account - let tx = await serviceProviderFactory.decreaseStake( - decrease, - { from: account }) - - let args = tx.logs.find(log => log.event === 'UpdatedStakeAmount').args - // console.dir(args, { depth: 5 }) - } - - const deregisterServiceProvider = async (type, endpoint, account) => { - let deregTx = await serviceProviderFactory.deregister( - type, - endpoint, - { from: account }) - let args = deregTx.logs.find(log => log.event === 'DeregisteredServiceProvider').args - args.unstakedAmountInt = fromBn(args._unstakeAmount) - args.spID = fromBn(args._spID) - return args - } - - const getServiceProviderIdsFromAddress = async (account, type) => { - // Query and convert returned IDs to bignumber - let ids = ( - await serviceProviderFactory.getServiceProviderIdsFromAddress(account, type) - ).map(x => fromBn(x)) - return ids - } - describe('Delegation flow', () => { let regTx beforeEach(async () => { @@ -326,6 +217,10 @@ contract('DelegateManager', async (accounts) => { // Confirm balance updated for tokens let finalBal = await token.balanceOf(stakerAccount) assert.isTrue(initialBal.eq(finalBal.add(DEFAULT_AMOUNT)), 'Expect funds to be transferred') + + // Update SP Deployer Cut to 10% + await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) + await serviceProviderFactory.updateServiceProviderCut(stakerAccount2, 10, { from: stakerAccount2 }) }) it('initial state + claim', async () => { @@ -333,12 +228,13 @@ contract('DelegateManager', async (accounts) => { let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) let totalStakedForAccount = await staking.totalStakedFor(stakerAccount) - await claimFactory.initiateClaim() + await claimFactory.initiateRound() totalStakedForAccount = await staking.totalStakedFor(stakerAccount) spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - await delegateManager.makeClaim({ from: stakerAccount }) + await delegateManager.claimRewards({ from: stakerAccount }) + totalStakedForAccount = await staking.totalStakedFor(stakerAccount) spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) assert.isTrue( @@ -430,13 +326,17 @@ contract('DelegateManager', async (accounts) => { await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) let deployerCut = await serviceProviderFactory.getServiceProviderDeployerCut(stakerAccount) let deployerCutBase = await serviceProviderFactory.getServiceProviderDeployerCutBase() - await claimFactory.initiateClaim() + + // Initiate round + await claimFactory.initiateRound() let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + let totalStake = await staking.totalStaked() totalStakedForSP = await staking.totalStakedFor(stakerAccount) delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) let totalValueOutsideStaking = spStake.add(delegatedStake) - let totalRewards = totalStakedForSP.sub(totalValueOutsideStaking) + let fundingAmount = await claimFactory.getFundsPerRound() + let totalRewards = (totalStakedForSP.mul(fundingAmount)).div(totalStake) // Manually calculate expected value prior to making claim // Identical math as contract @@ -444,11 +344,11 @@ contract('DelegateManager', async (accounts) => { let spDeployerCut = (delegateRewardsPriorToSPCut.mul(deployerCut)).div(deployerCutBase) let delegateRewards = delegateRewardsPriorToSPCut.sub(spDeployerCut) let expectedDelegateStake = delegatedStake.add(delegateRewards) - let spRewardShare = (spStake.mul(totalRewards)).div(totalValueOutsideStaking) let expectedSpStake = spStake.add(spRewardShare.add(spDeployerCut)) - await delegateManager.makeClaim({ from: stakerAccount }) + await delegateManager.claimRewards({ from: stakerAccount }) + let finalSpStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) let finalDelegateStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) @@ -456,15 +356,10 @@ contract('DelegateManager', async (accounts) => { assert.isTrue(finalDelegateStake.eq(expectedDelegateStake), 'Expected delegate stake matches found value') }) - it.only('single delegator + claim + slash', async () => { - // TODO: Run claim / clash pattern 10,000x and confirm discrepancy - // Validate discrepancy against some pre-known value, 1AUD or <1AUD + it('single delegator + claim + slash', async () => { // TODO: Validate all // Transfer 1000 tokens to delegator await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) - let currentMultiplier = await staking.getCurrentStakeMultiplier() - console.log(`currentMultiplier ${currentMultiplier}`) - return let initialDelegateAmount = toWei(60) @@ -479,32 +374,147 @@ contract('DelegateManager', async (accounts) => { initialDelegateAmount, { from: delegatorAccount1 }) - let total = 1000 - let iterations = total - while (iterations > 0) { - let slash = false - if (((total - iterations) + 1) % 10 === 0) { - slash = true - } - console.log(`Round ${(total - iterations) + 1}`) - await fundClaimSlash(slash) - iterations-- - } + // Fund new claim + await claimFactory.initiateRound() + + // Get rewards + await delegateManager.claimRewards({ from: stakerAccount }) + await delegateManager.claimRewards({ from: stakerAccount2 }) + + // Slash 30% of total + let slashNumerator = web3.utils.toBN(30) + let slashDenominator = web3.utils.toBN(100) + let totalInStakingContract = await staking.totalStakedFor(slasherAccount) + let slashAmount = (totalInStakingContract.mul(slashNumerator)).div(slashDenominator) + + // Perform slash functions + await delegateManager.slash(slashAmount, slasherAccount) // Summarize after execution let spFactoryStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) - let totalInStakingContract = await staking.totalStakedFor(stakerAccount) + let totalInStakingAfterSlash = await staking.totalStakedFor(stakerAccount) let delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) let outsideStake = spFactoryStake.add(delegatedStake) - console.log(`SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside stake: ${outsideStake}, Staking: ${totalInStakingContract}`) - let stakeDiscrepancy = totalInStakingContract.sub(outsideStake) - console.log(`Stake discrepancy: ${stakeDiscrepancy}`) - let oneAud = toWei(1) - console.log(`1 AUD: ${oneAud}`) - - let tokensInStaking = await token.balanceOf(stakingAddress) - console.log(`Tokens in staking ${tokensInStaking}`) + let stakeDiscrepancy = totalInStakingAfterSlash.sub(outsideStake) + let totalStaked = await staking.totalStaked() + let tokensAtStakingAddress = await token.balanceOf(stakingAddress) + + assert.equal(stakeDiscrepancy, 0, 'Equal tokens expected inside/outside Staking') + assert.isTrue(totalStaked.eq(tokensAtStakingAddress), 'Expect equivalency between Staking contract and ERC') + assert.isTrue(totalInStakingAfterSlash.eq(outsideStake), 'Expected SP/delegatemanager to equal staking') + assert.isTrue((totalInStakingContract.sub(slashAmount)).eq(totalInStakingAfterSlash), 'Expected slash value') + }) + + it('40 delegators to one SP + claim', async () => { + // TODO: Validate all + let totalStakedForSP = await staking.totalStakedFor(stakerAccount) + + let numDelegators = 40 + if (accounts.length < numDelegators) { + // Disabled for CI, pending modification of total accounts + console.log(`Insufficient accounts found - required ${numDelegators}, found ${accounts.length}`) + return + } + + let delegateAccountOffset = 4 + let delegatorAccounts = accounts.slice(delegateAccountOffset, delegateAccountOffset + numDelegators) + let totalDelegationAmount = DEFAULT_AMOUNT + let singleDelegateAmount = totalDelegationAmount.div(web3.utils.toBN(numDelegators)) + + for (var delegator of delegatorAccounts) { + // Transfer 1000 tokens to each delegator + await token.transfer(delegator, INITIAL_BAL, { from: treasuryAddress }) + // Approve staking transfer + await token.approve( + stakingAddress, + singleDelegateAmount, + { from: delegator }) + + await delegateManager.increaseDelegatedStake( + stakerAccount, + singleDelegateAmount, + { from: delegator }) + + let delegatorStake = await delegateManager.getTotalDelegatorStake(delegator) + let delegatorStakeForSP = await delegateManager.getDelegatorStakeForServiceProvider( + delegator, + stakerAccount) + assert.isTrue( + delegatorStake.eq(singleDelegateAmount), + 'Expected total delegator stake to match input') + assert.isTrue( + delegatorStakeForSP.eq(singleDelegateAmount), + 'Expected total delegator stake to SP to match input') + } + + let totalSPStakeAfterDelegation = await staking.totalStakedFor(stakerAccount) + let expectedTotalStakeAfterDelegation = totalStakedForSP.add(totalDelegationAmount) + assert.isTrue( + totalSPStakeAfterDelegation.eq(expectedTotalStakeAfterDelegation), + `Total value inconsistent after all delegation. Expected ${fromBn(expectedTotalStakeAfterDelegation)}, found ${fromBn(totalSPStakeAfterDelegation)}`) + + // Initiate round + await claimFactory.initiateRound() + + let deployerCut = await serviceProviderFactory.getServiceProviderDeployerCut(stakerAccount) + let deployerCutBase = await serviceProviderFactory.getServiceProviderDeployerCutBase() + + // Calculating expected values + let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + let totalStake = await staking.totalStaked() + totalStakedForSP = await staking.totalStakedFor(stakerAccount) + let totalDelegatedStake = web3.utils.toBN(0) + for (let delegator of delegatorAccounts) { + let delegatorStake = await delegateManager.getTotalDelegatorStake(delegator) + totalDelegatedStake = totalDelegatedStake.add(delegatorStake) + } + + let totalValueOutsideStaking = spStake.add(totalDelegatedStake) + assert.isTrue( + totalStakedForSP.eq(totalValueOutsideStaking), + 'Expect equivalent value between staking contract and protocol contracts') + + let fundingAmount = await claimFactory.getFundsPerRound() + let totalRewards = (totalStakedForSP.mul(fundingAmount)).div(totalStake) + + let spDelegationRewards = web3.utils.toBN(0) + // Expected value for each delegator + let expectedDelegateStakeDictionary = {} + for (let delegator of delegatorAccounts) { + let delegatorStake = await delegateManager.getTotalDelegatorStake(delegator) + let delegateRewardsPriorToSPCut = (delegatorStake.mul(totalRewards)).div(totalValueOutsideStaking) + let spDeployerCut = (delegateRewardsPriorToSPCut.mul(deployerCut)).div(deployerCutBase) + let delegateRewards = delegateRewardsPriorToSPCut.sub(spDeployerCut) + // Update dictionary of expected values + let expectedDelegateStake = delegatorStake.add(delegateRewards) + expectedDelegateStakeDictionary[delegator] = expectedDelegateStake + spDelegationRewards = spDelegationRewards.add(spDeployerCut) + } + + // Expected value for SP + let spRewardShare = (spStake.mul(totalRewards)).div(totalValueOutsideStaking) + let expectedSpStake = spStake.add(spRewardShare.add(spDelegationRewards)) + + // Perform claim + let claimTx = await delegateManager.claimRewards({ from: stakerAccount }) + // console.dir(claimTx, { depth: 5 }) + totalStakedForSP = await staking.totalStakedFor(stakerAccount) + + // Validate final SP value vs expected + let finalSpStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) + assert.isTrue(finalSpStake.eq(expectedSpStake), 'Expected SP stake matches found value') + // Validate each delegate value against expected + for (let delegator of delegatorAccounts) { + let finalDelegatorStake = await delegateManager.getTotalDelegatorStake(delegator) + let expectedDelegatorStake = expectedDelegateStakeDictionary[delegator] + assert.isTrue( + finalDelegatorStake.eq(expectedDelegatorStake), + 'Unexpected delegator stake after claim is made') + } }) + // TODO: What happens when someone delegates after a funding round has started...? + // Do they still get rewards or not? + // Potential idea - just lockup delegation for some inteval // 2 service providers, 1 claim, no delegation // 2 service providers, 1 claim, delegation to first SP diff --git a/eth-contracts/test/staking.test.js b/eth-contracts/test/staking.test.js index 5be9324b958..343232be250 100644 --- a/eth-contracts/test/staking.test.js +++ b/eth-contracts/test/staking.test.js @@ -6,7 +6,6 @@ const Staking = artifacts.require('Staking') const fromBn = n => parseInt(n.valueOf(), 10) const getTokenBalance = async (token, account) => fromBn(await token.balanceOf(account)) -const claimBlockDiff = 46000 const toWei = (aud) => { let amountInAudWei = web3.utils.toWei( @@ -45,29 +44,12 @@ contract('Staking test', async (accounts) => { { from: testStakingCallerAddress }) } - const approveAndFundNewClaim = async (amount, from) => { - // allow Staking app to move owner tokens - await token.approve(stakingAddress, amount, { from }) - let receipt = await staking.fundNewClaim(amount, { from }) - return receipt - } - - const getLatestBlock = async () => { - let block = await web3.eth.getBlock('latest') - // console.log(`Latest block: ${block.number}`) - return parseInt(block.number) - } - const getStakedAmountForAcct = async (acct) => { let stakeValue = (await staking.totalStakedFor(acct)).valueOf() // console.log(`${acct} : ${stakeValue}`) return parseInt(stakeValue) } - const getInstance = (receipt) => { - return receipt.logs.find(log => log.event === 'NewStaking').args.instance - } - const slashAccount = async (amount, slashAddr, slasherAddress) => { return await staking.slash( amount, @@ -241,8 +223,7 @@ contract('Staking test', async (accounts) => { const spAccount3 = accounts[3] const funderAccount = accounts[4] - // TODO: Confirm that historic values for a single account can be recalculated by validating with blocknumber - // + // TODO: Confirm that historic values for a single account can be recalculated by validating with blocknumber // Transfer DEFAULLT tokens to accts 1, 2, 3 await token.transfer(spAccount1, DEFAULT_AMOUNT, { from: treasuryAddress }) await token.transfer(spAccount2, DEFAULT_AMOUNT, { from: treasuryAddress }) @@ -268,11 +249,17 @@ contract('Staking test', async (accounts) => { // Transfer 120AUD tokens to staking contract await token.transfer(funderAccount, FIRST_CLAIM_FUND, { from: treasuryAddress }) - // Transfer funds for claiming to contract - await approveAndFundNewClaim(FIRST_CLAIM_FUND, funderAccount) + // allow Staking app to move owner tokens + let sp1Rewards = FIRST_CLAIM_FUND.div(web3.utils.toBN(2)) + let sp2Rewards = sp1Rewards + await token.approve(stakingAddress, sp1Rewards, { from: funderAccount }) + let receipt = await staking.stakeRewards(sp1Rewards, spAccount1, { from: funderAccount }) + + await token.approve(stakingAddress, sp2Rewards, { from: funderAccount }) + receipt = await staking.stakeRewards(sp2Rewards, spAccount2, { from: funderAccount }) // Initial val should be first claim fund / 2 - let expectedValueAfterFirstFund = DEFAULT_AMOUNT.add(FIRST_CLAIM_FUND.div(web3.utils.toBN(2))) + let expectedValueAfterFirstFund = DEFAULT_AMOUNT.add(sp1Rewards) // Confirm value added to account 1 let acct1StakeAfterFund = await getStakedAmountForAcct(spAccount1) From e6aa8009dbc5714c2539d1471d5de2eef092fae1 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Mon, 6 Apr 2020 10:27:20 -0400 Subject: [PATCH 34/39] [eth-contracts] Lockup for undelegate operations (#359) * Enforces a block difference for undelegate operations, locking funds * Handles reward negation for locked up funds * Slash operations reset undelegate requests * Disabled delegate operations while claim is pending --- .../contracts/service/ClaimFactory.sol | 24 +- .../contracts/service/DelegateManager.sol | 249 +++++++++++--- eth-contracts/test/_lib/lib.js | 10 + eth-contracts/test/claimFactory.test.js | 8 +- eth-contracts/test/delegateManager.test.js | 324 +++++++++++++++--- 5 files changed, 511 insertions(+), 104 deletions(-) diff --git a/eth-contracts/contracts/service/ClaimFactory.sol b/eth-contracts/contracts/service/ClaimFactory.sol index 8073c98c70d..3d8be77ca93 100644 --- a/eth-contracts/contracts/service/ClaimFactory.sol +++ b/eth-contracts/contracts/service/ClaimFactory.sol @@ -56,8 +56,7 @@ contract ClaimFactory is RegistryContract { stakingProxyOwnerKey = _stakingProxyOwnerKey; audiusToken = ERC20Mintable(tokenAddress); registry = RegistryInterface(_registryAddress); - // Allow a claim to be funded initially by subtracting the configured difference - fundBlock = block.number - fundRoundBlockDiff; + fundBlock = 0; } function getFundingRoundBlockDiff() @@ -103,19 +102,27 @@ contract ClaimFactory is RegistryContract { // TODO: Name this function better // TODO: Permission caller - function processClaim(address _claimer) external returns (uint newAccountTotal) { + function processClaim( + address _claimer, + uint _totalLockedForSP + ) external returns (uint newAccountTotal) + { address stakingAddress = registry.getContract(stakingProxyOwnerKey); Staking stakingContract = Staking(stakingAddress); // Prevent duplicate claim uint lastUserClaimBlock = stakingContract.lastClaimedFor(_claimer); require(lastUserClaimBlock <= fundBlock, "Claim already processed for user"); - uint totalStakedAtFundBlockForClaimer = stakingContract.totalStakedForAt( _claimer, fundBlock); + + // Subtract total locked amount for SP from stake at fund block + uint claimerTotalStake = totalStakedAtFundBlockForClaimer - _totalLockedForSP; uint totalStakedAtFundBlock = stakingContract.totalStakedAt(fundBlock); + + // Calculate claimer rewards uint rewardsForClaimer = ( - totalStakedAtFundBlockForClaimer.mul(fundingAmount) + claimerTotalStake.mul(fundingAmount) ).div(totalStakedAtFundBlock); bool minted = audiusToken.mint(address(this), rewardsForClaimer); @@ -142,4 +149,11 @@ contract ClaimFactory is RegistryContract { return newTotal; } + + function claimPending(address _sp) external view returns (bool pending) { + address stakingAddress = registry.getContract(stakingProxyOwnerKey); + Staking stakingContract = Staking(stakingAddress); + uint lastClaimedForSP = stakingContract.lastClaimedFor(_sp); + return (lastClaimedForSP < fundBlock); + } } diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 83b5b12e379..86701dcdf83 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -23,14 +23,27 @@ contract DelegateManager is RegistryContract { bytes32 serviceProviderFactoryKey; bytes32 claimFactoryKey; + // Number of blocks an undelegate operation has to wait + // TODO: Expose CRUD + // TODO: Move this value to Staking.sol as SPFactory may need as well + uint undelegateLockupDuration = 10; + // Staking contract ref ERC20Mintable internal audiusToken; // Struct representing total delegated to SP and list of delegators // TODO: Bound list struct ServiceProviderDelegateInfo { - uint totalDelegatedStake; - address[] delegators; + uint totalDelegatedStake; + uint totalLockedUpStake; + address[] delegators; + } + + // Data structures for lockup during withdrawal + struct UndelegateStakeRequest { + address serviceProvider; + uint amount; + uint lockupExpiryBlock; } // Service provider address -> ServiceProviderDelegateInfo @@ -43,6 +56,9 @@ contract DelegateManager is RegistryContract { // delegator -> (service provider -> delegatedStake) mapping (address => mapping(address => uint)) delegateInfo; + // Requester to pending undelegate request + mapping (address => UndelegateStakeRequest) undelegateRequests; + // TODO: Evaluate whether this is necessary bytes empty; @@ -89,14 +105,18 @@ contract DelegateManager is RegistryContract { claimFactoryKey = _claimFactoryKey; } - function increaseDelegatedStake( + // TODO: Require _target is a valid SP + // TODO: Validate sp account total balance + // TODO: Enforce min _amount? + function delegateStake( address _target, uint _amount ) external returns (uint delegeatedAmountForSP) { - // TODO: Require _target is a valid SP - // TODO: Validate sp account total balance - // TODO: Enforce min _amount? + require( + claimPending(_target) == false, + "Delegation not permitted for SP pending claim" + ); address delegator = msg.sender; Staking stakingContract = Staking( registry.getContract(stakingProxyOwnerKey) @@ -133,67 +153,143 @@ contract DelegateManager is RegistryContract { return delegateInfo[delegator][_target]; } - function decreaseDelegatedStake( + // Submit request for undelegation + function requestUndelegateStake( address _target, uint _amount - ) external returns (uint delegateAmount) + ) external returns (uint newDelegateAmount) { - address delegator = msg.sender; - Staking stakingContract = Staking( - registry.getContract(stakingProxyOwnerKey) - ); - bool delegatorRecordExists = updateServiceProviderDelegatorsIfNecessary( - delegator, - _target + require( + claimPending(_target) == false, + "Undelegate request not permitted for SP pending claim" ); - require(delegatorRecordExists, "Delegator must exist to decrease stake"); + address delegator = msg.sender; + bool exists = delegatorExistsForSP(delegator, _target); + require(exists, "Delegator must be staked for SP"); + + // Confirm no pending delegation request + require( + undelegateRequests[delegator].lockupExpiryBlock == 0, "No pending lockup expiry allowed"); + require(undelegateRequests[delegator].amount == 0, "No pending lockup amount allowed"); + require( + undelegateRequests[delegator].serviceProvider == address(0), "No pending lockup SP allowed"); + // Ensure valid bounds uint currentlyDelegatedToSP = delegateInfo[delegator][_target]; require( _amount <= currentlyDelegatedToSP, "Cannot decrease greater than currently staked for this ServiceProvider"); + uint expiryBlock = block.number + undelegateLockupDuration; + + undelegateRequests[delegator] = UndelegateStakeRequest({ + lockupExpiryBlock: expiryBlock, + amount: _amount, + serviceProvider: _target + }); + + // Update total locked for this service provider + spDelegateInfo[_target].totalLockedUpStake += _amount; + + return delegatorStakeTotal[delegator] - _amount; + } + + // Cancel undelegation request + function cancelUndelegateStake() external { + address delegator = msg.sender; + // Confirm pending delegation request + require( + undelegateRequests[delegator].lockupExpiryBlock != 0, "Pending lockup expiry expected"); + require(undelegateRequests[delegator].amount != 0, "Pending lockup amount expected"); + require( + undelegateRequests[delegator].serviceProvider != address(0), "Pending lockup SP expected"); + // Remove pending request + undelegateRequests[delegator] = UndelegateStakeRequest({ + lockupExpiryBlock: 0, + amount: 0, + serviceProvider: address(0) + }); + } + + // Finalize undelegation request and withdraw stake + function undelegateStake() external returns (uint newTotal) { + address delegator = msg.sender; + + // Confirm pending delegation request + require( + undelegateRequests[delegator].lockupExpiryBlock != 0, "Pending lockup expiry expected"); + require(undelegateRequests[delegator].amount != 0, "Pending lockup amount expected"); + require( + undelegateRequests[delegator].serviceProvider != address(0), "Pending lockup SP expected"); + + // Confirm lockup expiry has expired + require( + undelegateRequests[delegator].lockupExpiryBlock <= block.number, "Lockup must be expired"); + + // Confirm no pending claim for this service provider + require( + claimPending(undelegateRequests[delegator].serviceProvider) == false, + "Undelegate not permitted for SP pending claim" + ); + + address serviceProvider = undelegateRequests[delegator].serviceProvider; + uint unstakeAmount = undelegateRequests[delegator].amount; + + bool exists = delegatorExistsForSP(delegator, serviceProvider); + require(exists, "Delegator must be staked for SP"); + + Staking stakingContract = Staking( + registry.getContract(stakingProxyOwnerKey) + ); + // Stake on behalf of target service provider stakingContract.undelegateStakeFor( - _target, + serviceProvider, delegator, - _amount, + unstakeAmount, empty ); - emit DecreaseDelegatedStake( - delegator, - _target, - _amount); - // Update amount staked from this delegator to targeted service provider - delegateInfo[delegator][_target] -= _amount; + delegateInfo[delegator][serviceProvider] -= unstakeAmount; // Update total delegated stake - delegatorStakeTotal[delegator] -= _amount; + delegatorStakeTotal[delegator] -= unstakeAmount; // Update total delegated for SP - spDelegateInfo[_target].totalDelegatedStake -= _amount; + spDelegateInfo[serviceProvider].totalDelegatedStake -= unstakeAmount; // Remove from delegators list if no delegated stake remaining - if (delegateInfo[delegator][_target] == 0) { + if (delegateInfo[delegator][serviceProvider] == 0) { bool foundDelegator; uint delegatorIndex; - for (uint i = 0; i < spDelegateInfo[_target].delegators.length; i++) { - if (spDelegateInfo[_target].delegators[i] == delegator) { + for (uint i = 0; i < spDelegateInfo[serviceProvider].delegators.length; i++) { + if (spDelegateInfo[serviceProvider].delegators[i] == delegator) { foundDelegator = true; delegatorIndex = i; } } - // Overwrite and shrink delegators list - uint lastIndex = spDelegateInfo[_target].delegators.length - 1; - spDelegateInfo[_target].delegators[delegatorIndex] = spDelegateInfo[_target].delegators[lastIndex]; - spDelegateInfo[_target].delegators.length--; + if (foundDelegator) { + // Overwrite and shrink delegators list + uint lastIndex = spDelegateInfo[serviceProvider].delegators.length - 1; + spDelegateInfo[serviceProvider].delegators[delegatorIndex] = spDelegateInfo[serviceProvider].delegators[lastIndex]; + spDelegateInfo[serviceProvider].delegators.length--; + } } + // Update total locked for this service provider + spDelegateInfo[serviceProvider].totalLockedUpStake -= unstakeAmount; + + // Reset lockup information + undelegateRequests[delegator] = UndelegateStakeRequest({ + lockupExpiryBlock: 0, + amount: 0, + serviceProvider: address(0) + }); + // Return new total - return delegateInfo[delegator][_target]; + return delegateInfo[delegator][serviceProvider]; } /* @@ -207,8 +303,10 @@ contract DelegateManager is RegistryContract { ClaimFactory claimFactory = ClaimFactory( registry.getContract(claimFactoryKey) ); + // Pass in locked amount for claimer + uint totalLockedForClaimer = spDelegateInfo[msg.sender].totalLockedUpStake; // Process claim for msg.sender - claimFactory.processClaim(msg.sender); + claimFactory.processClaim(msg.sender, totalLockedForClaimer); // address claimer = msg.sender; ServiceProviderFactory spFactory = ServiceProviderFactory( @@ -227,7 +325,9 @@ contract DelegateManager is RegistryContract { // Amount in delegate manager staked to service provider uint totalBalanceInDelegateManager = spDelegateInfo[msg.sender].totalDelegatedStake; - uint totalBalanceOutsideStaking = totalBalanceInSPFactory + totalBalanceInDelegateManager; + uint totalBalanceOutsideStaking = ( + totalBalanceInSPFactory + totalBalanceInDelegateManager + ); // Require claim availability require(totalBalanceInStaking > totalBalanceOutsideStaking, "No stake available to claim"); @@ -244,15 +344,25 @@ contract DelegateManager is RegistryContract { uint spDeployerCutRewards = 0; uint totalDelegatedStakeIncrease = 0; + // Total valid funds used to calculate rewards distribution + uint totalActiveFunds = totalBalanceOutsideStaking - totalLockedForClaimer; + // Traverse all delegates and calculate their rewards // As each delegate reward is calculated, increment SP cut reward accordingly for (uint i = 0; i < spDelegateInfo[msg.sender].delegators.length; i++) { address delegator = spDelegateInfo[msg.sender].delegators[i]; uint delegateStakeToSP = delegateInfo[delegator][msg.sender]; + + // Subtract any locked up stake + if (undelegateRequests[delegator].serviceProvider == msg.sender) { + delegateStakeToSP = delegateStakeToSP - undelegateRequests[delegator].amount; + } + // Calculate rewards by ((delegateStakeToSP / totalBalanceOutsideStaking) * totalRewards) uint rewardsPriorToSPCut = ( delegateStakeToSP.mul(totalRewards) - ).div(totalBalanceOutsideStaking); + ).div(totalActiveFunds); + // Multiply by deployer cut fraction to calculate reward for SP uint spDeployerCut = (rewardsPriorToSPCut.mul(deployerCut)).div(deployerCutBase); spDeployerCutRewards += spDeployerCut; @@ -270,7 +380,7 @@ contract DelegateManager is RegistryContract { // TODO: Validate below with test cases uint spRewardShare = ( totalBalanceInSPFactory.mul(totalRewards) - ).div(totalBalanceOutsideStaking); + ).div(totalActiveFunds); uint newSpBalance = totalBalanceInSPFactory + spRewardShare + spDeployerCutRewards; spFactory.updateServiceProviderStake(msg.sender, newSpBalance); } @@ -306,20 +416,33 @@ contract DelegateManager is RegistryContract { emit Slash(_slashAddress, _amount, totalBalanceInStakingAfterSlash); uint totalDelegatedStakeDecrease = 0; - // For each delegator and deployer, recalculate new value // newStakeAmount = newStakeAmount * (oldStakeAmount / totalBalancePreSlash) for (uint i = 0; i < spDelegateInfo[_slashAddress].delegators.length; i++) { address delegator = spDelegateInfo[_slashAddress].delegators[i]; uint preSlashDelegateStake = delegateInfo[delegator][_slashAddress]; uint newDelegateStake = ( - totalBalanceInStakingAfterSlash.mul(preSlashDelegateStake) + totalBalanceInStakingAfterSlash.mul(preSlashDelegateStake) ).div(totalBalanceInStakingPreSlash); uint slashAmountForDelegator = preSlashDelegateStake.sub(newDelegateStake); delegateInfo[delegator][_slashAddress] -= (slashAmountForDelegator); delegatorStakeTotal[delegator] -= (slashAmountForDelegator); // Update total decrease amount totalDelegatedStakeDecrease += slashAmountForDelegator; + // Check for any locked up funds for this slashed delegator + // Slash overrides any pending withdrawal requests + if (undelegateRequests[delegator].amount != 0) { + address unstakeSP = undelegateRequests[delegator].serviceProvider; + uint unstakeAmount = undelegateRequests[delegator].amount; + // Reset total locked up stake + spDelegateInfo[unstakeSP].totalLockedUpStake -= unstakeAmount; + // Remove pending request + undelegateRequests[delegator] = UndelegateStakeRequest({ + lockupExpiryBlock: 0, + amount: 0, + serviceProvider: address(0) + }); + } } // Update total delegated to this SP @@ -350,6 +473,15 @@ contract DelegateManager is RegistryContract { return spDelegateInfo[_sp].totalDelegatedStake; } + /** + * @notice Total delegated stake locked up for a service provider + */ + function getTotalLockedDelegationForServiceProvider(address _sp) + external view returns (uint total) + { + return spDelegateInfo[_sp].totalLockedUpStake; + } + /** * @notice Total currently staked for a delegator, across service providers */ @@ -368,19 +500,48 @@ contract DelegateManager is RegistryContract { return delegateInfo[_delegator][_serviceProvider]; } - function updateServiceProviderDelegatorsIfNecessary ( + /** + * @notice Get status of pending undelegate request + */ + function getPendingUndelegateRequest(address _delegator) + external view returns (address target, uint amount, uint lockupExpiryBlock) + { + UndelegateStakeRequest memory req = undelegateRequests[_delegator]; + return (req.serviceProvider, req.amount, req.lockupExpiryBlock); + } + + function delegatorExistsForSP( address _delegator, address _serviceProvider - ) internal returns (bool exists) + ) internal view returns (bool exists) { for (uint i = 0; i < spDelegateInfo[_serviceProvider].delegators.length; i++) { if (spDelegateInfo[_serviceProvider].delegators[i] == _delegator) { return true; } } - // If not found, update list of delegates - spDelegateInfo[_serviceProvider].delegators.push(_delegator); + // Not found return false; } + + function updateServiceProviderDelegatorsIfNecessary ( + address _delegator, + address _serviceProvider + ) internal returns (bool exists) + { + bool delegatorFound = delegatorExistsForSP(_delegator, _serviceProvider); + if (!delegatorFound) { + // If not found, update list of delegates + spDelegateInfo[_serviceProvider].delegators.push(_delegator); + } + return delegatorFound; + } + + function claimPending(address _sp) internal view returns (bool pending) { + ClaimFactory claimFactory = ClaimFactory( + registry.getContract(claimFactoryKey) + ); + return claimFactory.claimPending(_sp); + } } diff --git a/eth-contracts/test/_lib/lib.js b/eth-contracts/test/_lib/lib.js index 6d3d411b263..c6df7f0061b 100644 --- a/eth-contracts/test/_lib/lib.js +++ b/eth-contracts/test/_lib/lib.js @@ -79,3 +79,13 @@ export const advanceBlock = (web3) => { }) }) } + +export const advanceToTargetBlock = async (targetBlockNumber, web3) => { + let currentBlock = await web3.eth.getBlock('latest') + let currentBlockNum = currentBlock.number + while (currentBlockNum < targetBlockNumber) { + await advanceBlock(web3) + currentBlock = await web3.eth.getBlock('latest') + currentBlockNum = currentBlock.number + } +} diff --git a/eth-contracts/test/claimFactory.test.js b/eth-contracts/test/claimFactory.test.js index 1d7f3823771..6cd6b02f283 100644 --- a/eth-contracts/test/claimFactory.test.js +++ b/eth-contracts/test/claimFactory.test.js @@ -108,7 +108,7 @@ contract('ClaimFactory', async (accounts) => { let fundsPerRound = await claimFactory.getFundsPerRound() await claimFactory.initiateRound() - await claimFactory.processClaim(staker) + await claimFactory.processClaim(staker, 0) totalStaked = await staking.totalStaked() @@ -137,7 +137,7 @@ contract('ClaimFactory', async (accounts) => { // Initiate round await claimFactory.initiateRound() - await claimFactory.processClaim(staker) + await claimFactory.processClaim(staker, 0) totalStaked = await staking.totalStaked() assert.isTrue( @@ -172,7 +172,7 @@ contract('ClaimFactory', async (accounts) => { // Initiate another round await claimFactory.initiateRound() - await claimFactory.processClaim(staker) + await claimFactory.processClaim(staker, 0) totalStaked = await staking.totalStaked() let finalAcctStake = await staking.totalStakedFor(staker) let expectedFinalValue = accountStakeBeforeSecondClaim.add(fundsPerClaim) @@ -212,7 +212,7 @@ contract('ClaimFactory', async (accounts) => { // Initiate claim await claimFactory.initiateRound() - await claimFactory.processClaim(staker) + await claimFactory.processClaim(staker, 0) totalStaked = await staking.totalStaked() assert.isTrue( diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index dce591c0f8f..418b248c5be 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -7,16 +7,10 @@ const OwnedUpgradeabilityProxy = artifacts.require('OwnedUpgradeabilityProxy') const ServiceProviderFactory = artifacts.require('ServiceProviderFactory') const ServiceProviderStorage = artifacts.require('ServiceProviderStorage') const Staking = artifacts.require('Staking') - const DelegateManager = artifacts.require('DelegateManager') - const ClaimFactory = artifacts.require('ClaimFactory') const fromBn = n => parseInt(n.valueOf(), 10) - -const getTokenBalance = async (token, account) => fromBn(await token.balanceOf(account)) -const claimBlockDiff = 46000 - const toWei = (aud) => { let amountInAudWei = web3.utils.toWei( aud.toString(), @@ -31,24 +25,18 @@ const fromWei = (wei) => { return web3.utils.fromWei(wei) } -const getTokenBalance2 = async (token, account) => fromWei(await token.balanceOf(account)) - const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') const serviceProviderStorageKey = web3.utils.utf8ToHex('ServiceProviderStorage') const serviceProviderFactoryKey = web3.utils.utf8ToHex('ServiceProviderFactory') const claimFactoryKey = web3.utils.utf8ToHex('ClaimFactory') const testDiscProvType = web3.utils.utf8ToHex('discovery-provider') -const testCreatorNodeType = web3.utils.utf8ToHex('creator-node') const testEndpoint = 'https://localhost:5000' const testEndpoint1 = 'https://localhost:5001' -const MIN_STAKE_AMOUNT = 10 - // 1000 AUD converted to AUDWei, multiplying by 10^18 const INITIAL_BAL = toWei(1000) const DEFAULT_AMOUNT = toWei(120) -const MAX_STAKE_AMOUNT = DEFAULT_AMOUNT * 100 contract('DelegateManager', async (accounts) => { let treasuryAddress = accounts[0] @@ -59,7 +47,6 @@ contract('DelegateManager', async (accounts) => { let token let registry let stakingAddress - let tokenAddress let serviceProviderStorage let serviceProviderFactory @@ -81,7 +68,6 @@ contract('DelegateManager', async (accounts) => { await registry.addContract(ownedUpgradeabilityProxyKey, proxy.address) token = await AudiusToken.new({ from: treasuryAddress }) - tokenAddress = token.address impl0 = await Staking.new() // Create initialization data @@ -154,46 +140,53 @@ contract('DelegateManager', async (accounts) => { return args } - const ensureValidClaimPeriod = async () => { - let currentBlock = await web3.eth.getBlock('latest') - let currentBlockNum = currentBlock.number - let lastFundBlock = await claimFactory.getLastFundBlock() - let claimDiff = await claimFactory.getFundingRoundBlockDiff() - let nextClaimBlock = lastFundBlock.add(claimDiff) - while (currentBlockNum < nextClaimBlock) { - await _lib.advanceBlock(web3) - currentBlock = await web3.eth.getBlock('latest') - currentBlockNum = currentBlock.number - } - } - - const printAccountStakeInfo = async (account) => { - console.log('') + const getAccountStakeInfo = async (account, print = false) => { let spFactoryStake let totalInStakingContract spFactoryStake = await serviceProviderFactory.getServiceProviderStake(account) totalInStakingContract = await staking.totalStakedFor(account) - let delegatedStake = web3.utils.toBN(0) + let delegatedStake = await delegateManager.getTotalDelegatedToServiceProvider(account) + let lockedUpStake = await delegateManager.getTotalLockedDelegationForServiceProvider(account) + let delegatorInfo = {} let delegators = await delegateManager.getDelegatorsList(account) for (var i = 0; i < delegators.length; i++) { let amountDelegated = await delegateManager.getTotalDelegatorStake(delegators[i]) - delegatedStake = delegatedStake.add(amountDelegated) + let amountDelegatedtoSP = await delegateManager.getDelegatorStakeForServiceProvider(delegators[i], account) + let pendingUndelegateRequest = await delegateManager.getPendingUndelegateRequest(delegators[i]) + delegatorInfo[delegators[i]] = { + amountDelegated, + amountDelegatedtoSP, + pendingUndelegateRequest + } } let outsideStake = spFactoryStake.add(delegatedStake) - // let tokensInStaking = await token.balanceOf(account) - // console.log(`${account} Total balance in stakingContract ${tokensInStaking}`) - console.log(`${account} SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}, Outside Stake: ${outsideStake} Staking: ${totalInStakingContract}`) + let totalActiveStake = outsideStake.sub(lockedUpStake) let stakeDiscrepancy = totalInStakingContract.sub(outsideStake) - console.log(`Internal (Staking) vs External (DelegateManager + SPFactory) Stake discrepancy: ${stakeDiscrepancy}`) + if (print) { + console.log(`${account} SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}`) + console.log(`${account} Outside Stake: ${outsideStake} Staking: ${totalInStakingContract}`) + console.log(`(Staking) vs (DelegateManager + SPFactory) Stake discrepancy: ${stakeDiscrepancy}`) + } + return { + totalInStakingContract, + delegatedStake, + spFactoryStake, + delegatorInfo, + outsideStake, + lockedUpStake, + totalActiveStake + } } - describe('Delegation flow', () => { + describe('Delegation tests', () => { let regTx beforeEach(async () => { - // Transfer 1000 tokens to staker + // Transfer 1000 tokens to stakers await token.transfer(stakerAccount, INITIAL_BAL, { from: treasuryAddress }) await token.transfer(stakerAccount2, INITIAL_BAL, { from: treasuryAddress }) + // Transfer 1000 tokens to delegator + await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) let initialBal = await token.balanceOf(stakerAccount) @@ -258,7 +251,7 @@ contract('DelegateManager', async (accounts) => { { from: delegatorAccount1 }) let delegators = await delegateManager.getDelegatorsList(stakerAccount) - await delegateManager.increaseDelegatedStake( + await delegateManager.delegateStake( stakerAccount, initialDelegateAmount, { from: delegatorAccount1 }) @@ -284,17 +277,69 @@ contract('DelegateManager', async (accounts) => { totalStakedForSP.eq(spStake.add(delegatedStake)), 'Sum of Staking.sol equals SPFactory and DelegateManager' ) - await delegateManager.decreaseDelegatedStake( + + // Submit request to undelegate + await delegateManager.requestUndelegateStake( stakerAccount, initialDelegateAmount, - { from: delegatorAccount1 }) + { from: delegatorAccount1 } + ) + + // Confirm lockup amount is registered + let undelegateRequestInfo = await delegateManager.getPendingUndelegateRequest(delegatorAccount1) + assert.isTrue( + undelegateRequestInfo.amount.eq(initialDelegateAmount), + 'Expected amount not found in lockup') + + let totalLockedDelegation = + await delegateManager.getTotalLockedDelegationForServiceProvider(stakerAccount) + assert.isTrue( + totalLockedDelegation.eq(initialDelegateAmount), + 'Expected amount not found in total lockup for SP') + + // Try to undelegate stake immediately, confirm failure + await _lib.assertRevert( + delegateManager.undelegateStake({ from: delegatorAccount1 }), + 'Lockup must be expired' + ) + // Try to submit another request, expect revert + await _lib.assertRevert( + delegateManager.requestUndelegateStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 }), + 'No pending lockup expiry allowed' + ) + + // Advance to valid block + await _lib.advanceToTargetBlock( + fromBn(undelegateRequestInfo.lockupExpiryBlock), + web3 + ) + + // Undelegate stake + delegateManager.undelegateStake({ from: delegatorAccount1 }) + + // Confirm all state change operations have occurred + undelegateRequestInfo = await delegateManager.getPendingUndelegateRequest(delegatorAccount1) totalStakedForSP = await staking.totalStakedFor(stakerAccount) delegators = await delegateManager.getDelegatorsList(stakerAccount) + delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) + totalLockedDelegation = + await delegateManager.getTotalLockedDelegationForServiceProvider(stakerAccount) assert.equal( delegators.length, 0, 'Expect no remaining delegators') + assert.equal( + delegatedStake, + 0, + 'Expect no remaining total delegate stake') + assert.equal( + totalLockedDelegation, + 0, + 'Expect no remaining locked stake for SP') assert.isTrue( initialSpStake.eq(totalStakedForSP), 'Staking.sol back to initial value') @@ -314,22 +359,23 @@ contract('DelegateManager', async (accounts) => { initialDelegateAmount, { from: delegatorAccount1 }) - await delegateManager.increaseDelegatedStake( + await delegateManager.delegateStake( stakerAccount, initialDelegateAmount, { from: delegatorAccount1 }) totalStakedForSP = await staking.totalStakedFor(stakerAccount) let delegatedStake = await delegateManager.getTotalDelegatorStake(delegatorAccount1) - - // Update SP Deployer Cut to 10% - await serviceProviderFactory.updateServiceProviderCut(stakerAccount, 10, { from: stakerAccount }) let deployerCut = await serviceProviderFactory.getServiceProviderDeployerCut(stakerAccount) let deployerCutBase = await serviceProviderFactory.getServiceProviderDeployerCutBase() // Initiate round await claimFactory.initiateRound() + // Confirm claim is pending + let pendingClaim = await claimFactory.claimPending(stakerAccount) + assert.isTrue(pendingClaim, 'ClaimFactory expected to consider claim pending') + let spStake = await serviceProviderFactory.getServiceProviderStake(stakerAccount) let totalStake = await staking.totalStaked() totalStakedForSP = await staking.totalStakedFor(stakerAccount) @@ -369,7 +415,7 @@ contract('DelegateManager', async (accounts) => { initialDelegateAmount, { from: delegatorAccount1 }) - await delegateManager.increaseDelegatedStake( + await delegateManager.delegateStake( stakerAccount, initialDelegateAmount, { from: delegatorAccount1 }) @@ -430,7 +476,7 @@ contract('DelegateManager', async (accounts) => { singleDelegateAmount, { from: delegator }) - await delegateManager.increaseDelegatedStake( + await delegateManager.delegateStake( stakerAccount, singleDelegateAmount, { from: delegator }) @@ -512,11 +558,187 @@ contract('DelegateManager', async (accounts) => { 'Unexpected delegator stake after claim is made') } }) - // TODO: What happens when someone delegates after a funding round has started...? - // Do they still get rewards or not? - // Potential idea - just lockup delegation for some inteval - // 2 service providers, 1 claim, no delegation - // 2 service providers, 1 claim, delegation to first SP + // Confirm a pending undelegate operation negates any claimed value + it('single delegator + undelegate + claim', async () => { + // TODO: Validate all + // Transfer 1000 tokens to delegator + await token.transfer(delegatorAccount1, INITIAL_BAL, { from: treasuryAddress }) + + let initialDelegateAmount = toWei(60) + + // Approve staking transfer + await token.approve( + stakingAddress, + initialDelegateAmount, + { from: delegatorAccount1 }) + + await delegateManager.delegateStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 }) + + // Submit request to undelegate + await delegateManager.requestUndelegateStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 } + ) + + let preRewardInfo = await getAccountStakeInfo(stakerAccount, false) + + // Initiate round + await claimFactory.initiateRound() + await delegateManager.claimRewards({ from: stakerAccount }) + let postRewardInfo = await getAccountStakeInfo(stakerAccount, false) + + let preRewardDelegation = preRewardInfo.delegatorInfo[delegatorAccount1].amountDelegated + let postRewardDelegation = postRewardInfo.delegatorInfo[delegatorAccount1].amountDelegated + assert.isTrue( + preRewardDelegation.eq(postRewardDelegation), + 'Confirm no reward issued to delegator') + let preRewardStake = preRewardInfo.totalInStakingContract + let postRewardStake = postRewardInfo.totalInStakingContract + assert.isTrue( + postRewardStake.gt(preRewardStake), + 'Confirm reward issued to service provider') + }) + + // Confirm a pending undelegate operation negates any claimed value + it('single delegator + undelegate + slash', async () => { + let initialDelegateAmount = toWei(60) + let slashAmount = toWei(100) + + // Approve staking transfer + await token.approve( + stakingAddress, + initialDelegateAmount, + { from: delegatorAccount1 }) + + await delegateManager.delegateStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 }) + + // Submit request to undelegate + await delegateManager.requestUndelegateStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 } + ) + + let preSlashInfo = await getAccountStakeInfo(stakerAccount, false) + let preSlashLockupStake = preSlashInfo.lockedUpStake + assert.isTrue( + preSlashLockupStake.eq(initialDelegateAmount), + 'Initial delegate amount not found') + + // Perform slash functions + await delegateManager.slash(slashAmount, slasherAccount) + + let postRewardInfo = await getAccountStakeInfo(stakerAccount, false) + + let postSlashLockupStake = postRewardInfo.lockedUpStake + assert.equal( + postSlashLockupStake, + 0, + 'Expect no lockup funds to carry over') + }) + + it('3 delegators + pending claim + undelegate restrictions', async () => { + const delegatorAccount2 = accounts[5] + const delegatorAccount3 = accounts[6] + // Transfer 1000 tokens to delegator2, delegator3 + await token.transfer(delegatorAccount2, INITIAL_BAL, { from: treasuryAddress }) + await token.transfer(delegatorAccount3, INITIAL_BAL, { from: treasuryAddress }) + let initialDelegateAmount = toWei(60) + + // Approve staking transfer for delegator 1 + await token.approve( + stakingAddress, + initialDelegateAmount, + { from: delegatorAccount1 }) + + // Stake initial value for delegator 1 + await delegateManager.delegateStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 }) + + // Submit request to undelegate + await delegateManager.requestUndelegateStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 } + ) + + // Approve staking transfer for delegator 3 + await token.approve( + stakingAddress, + initialDelegateAmount, + { from: delegatorAccount3 }) + + // Stake initial value for delegator 3 + await delegateManager.delegateStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount3 }) + + // Confirm lockup amount is registered + let undelegateRequestInfo = await delegateManager.getPendingUndelegateRequest(delegatorAccount1) + assert.isTrue( + undelegateRequestInfo.amount.eq(initialDelegateAmount), + 'Expect request to match undelegate amount') + + // Advance to valid block + await _lib.advanceToTargetBlock( + fromBn(undelegateRequestInfo.lockupExpiryBlock), + web3 + ) + let currentBlock = await web3.eth.getBlock('latest') + let currentBlockNum = currentBlock.number + assert.isTrue( + (web3.utils.toBN(currentBlockNum)).gte(undelegateRequestInfo.lockupExpiryBlock), + 'Confirm expired lockup period') + + // Initiate round + await claimFactory.initiateRound() + + // Confirm claim is pending + let pendingClaim = await claimFactory.claimPending(stakerAccount) + assert.isTrue(pendingClaim, 'ClaimFactory expected to consider claim pending') + + // Attempt to finalize undelegate stake request + await _lib.assertRevert( + delegateManager.undelegateStake({ from: delegatorAccount1 }), + 'Undelegate not permitted for SP pending claim' + ) + + // Approve staking transfer for delegator 2 + await token.approve( + stakingAddress, + initialDelegateAmount, + { from: delegatorAccount2 }) + + // Attempt to delegate + await _lib.assertRevert( + delegateManager.delegateStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount1 }), + 'Delegation not permitted for SP pending claim' + ) + + // Submit request to undelegate for delegator 3 + await _lib.assertRevert( + delegateManager.requestUndelegateStake( + stakerAccount, + initialDelegateAmount, + { from: delegatorAccount3 }), + 'Undelegate request not permitted for SP' + ) + + await delegateManager.claimRewards({ from: stakerAccount }) + }) }) }) From 889d3baec73460f6a059fd6b29ddfc5d39062f21 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Thu, 9 Apr 2020 18:48:13 -0400 Subject: [PATCH 35/39] [eth-contracts] Valid stake bounds indicator + direct deployer stake requirement (#360) * Prevent reward operations if SP dips below account configured bounds after slash * Require minimum direct deployer stake to prevent SP from withdrawing all funds --- .../contracts/service/ClaimFactory.sol | 22 +- .../contracts/service/DelegateManager.sol | 22 +- .../service/MockServiceProviderFactory.sol | 23 ++ .../service/ServiceProviderFactory.sol | 128 +++++++++--- .../migrations/6_claim_factory_migration.js | 4 +- eth-contracts/test/claimFactory.test.js | 7 + eth-contracts/test/delegateManager.test.js | 196 +++++++++++++++++- 7 files changed, 361 insertions(+), 41 deletions(-) create mode 100644 eth-contracts/contracts/service/MockServiceProviderFactory.sol diff --git a/eth-contracts/contracts/service/ClaimFactory.sol b/eth-contracts/contracts/service/ClaimFactory.sol index 3d8be77ca93..af461b56cb1 100644 --- a/eth-contracts/contracts/service/ClaimFactory.sol +++ b/eth-contracts/contracts/service/ClaimFactory.sol @@ -4,6 +4,7 @@ import "./registry/RegistryContract.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; import "./interface/registry/RegistryInterface.sol"; +import "./ServiceProviderFactory.sol"; // WORKING CONTRACT @@ -16,6 +17,7 @@ contract ClaimFactory is RegistryContract { address tokenAddress; bytes32 stakingProxyOwnerKey; + bytes32 serviceProviderFactoryKey; // Claim related configurations uint fundRoundBlockDiff = 10; @@ -50,10 +52,12 @@ contract ClaimFactory is RegistryContract { constructor( address _tokenAddress, address _registryAddress, - bytes32 _stakingProxyOwnerKey + bytes32 _stakingProxyOwnerKey, + bytes32 _serviceProviderFactoryKey ) public { tokenAddress = _tokenAddress; stakingProxyOwnerKey = _stakingProxyOwnerKey; + serviceProviderFactoryKey = _serviceProviderFactoryKey; audiusToken = ERC20Mintable(tokenAddress); registry = RegistryInterface(_registryAddress); fundBlock = 0; @@ -92,7 +96,6 @@ contract ClaimFactory is RegistryContract { fundBlock = block.number; totalClaimedInRound = 0; roundNumber += 1; - emit RoundInitiated( fundBlock, roundNumber, @@ -116,6 +119,16 @@ contract ClaimFactory is RegistryContract { _claimer, fundBlock); + (uint spMin, uint spMax) = ServiceProviderFactory( + registry.getContract(serviceProviderFactoryKey) + ).getAccountStakeBounds(_claimer); + require( + (totalStakedAtFundBlockForClaimer >= spMin), + 'Minimum stake bounds violated at fund block'); + require( + (totalStakedAtFundBlockForClaimer <= spMax), + 'Maximum stake bounds violated at fund block'); + // Subtract total locked amount for SP from stake at fund block uint claimerTotalStake = totalStakedAtFundBlockForClaimer - _totalLockedForSP; uint totalStakedAtFundBlock = stakingContract.totalStakedAt(fundBlock); @@ -125,8 +138,9 @@ contract ClaimFactory is RegistryContract { claimerTotalStake.mul(fundingAmount) ).div(totalStakedAtFundBlock); - bool minted = audiusToken.mint(address(this), rewardsForClaimer); - require(minted, "New tokens must be minted"); + require( + audiusToken.mint(address(this), rewardsForClaimer), + "New tokens must be minted"); // Approve token transfer to staking contract address audiusToken.approve(stakingAddress, rewardsForClaimer); diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 86701dcdf83..3c69aaef0de 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -149,6 +149,11 @@ contract DelegateManager is RegistryContract { // Update total delegated stake delegatorStakeTotal[delegator] += _amount; + // Validate balance + ServiceProviderFactory( + registry.getContract(serviceProviderFactoryKey) + ).validateAccountStakeBalance(_target); + // Return new total return delegateInfo[delegator][_target]; } @@ -288,6 +293,11 @@ contract DelegateManager is RegistryContract { serviceProvider: address(0) }); + // Validate balance + ServiceProviderFactory( + registry.getContract(serviceProviderFactoryKey) + ).validateAccountStakeBalance(serviceProvider); + // Return new total return delegateInfo[delegator][serviceProvider]; } @@ -305,14 +315,20 @@ contract DelegateManager is RegistryContract { ); // Pass in locked amount for claimer uint totalLockedForClaimer = spDelegateInfo[msg.sender].totalLockedUpStake; - // Process claim for msg.sender - claimFactory.processClaim(msg.sender, totalLockedForClaimer); // address claimer = msg.sender; ServiceProviderFactory spFactory = ServiceProviderFactory( registry.getContract(serviceProviderFactoryKey) ); + // Confirm service provider is valid + require( + spFactory.isServiceProviderWithinBounds(msg.sender), + 'Service provider must be within bounds'); + + // Process claim for msg.sender + claimFactory.processClaim(msg.sender, totalLockedForClaimer); + // Amount stored in staking contract for owner uint totalBalanceInStaking = Staking( registry.getContract(stakingProxyOwnerKey) @@ -323,6 +339,7 @@ contract DelegateManager is RegistryContract { uint totalBalanceInSPFactory = spFactory.getServiceProviderStake(msg.sender); require(totalBalanceInSPFactory > 0, "Service Provider stake required"); + // Amount in delegate manager staked to service provider uint totalBalanceInDelegateManager = spDelegateInfo[msg.sender].totalDelegatedStake; uint totalBalanceOutsideStaking = ( @@ -377,7 +394,6 @@ contract DelegateManager is RegistryContract { // Update total delegated to this SP spDelegateInfo[msg.sender].totalDelegatedStake += totalDelegatedStakeIncrease; - // TODO: Validate below with test cases uint spRewardShare = ( totalBalanceInSPFactory.mul(totalRewards) ).div(totalActiveFunds); diff --git a/eth-contracts/contracts/service/MockServiceProviderFactory.sol b/eth-contracts/contracts/service/MockServiceProviderFactory.sol new file mode 100644 index 00000000000..83af002b1d3 --- /dev/null +++ b/eth-contracts/contracts/service/MockServiceProviderFactory.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.5.0; + +import "./registry/RegistryContract.sol"; + + +// Test contract used in claim factory scenarios +// Eliminates requirement for full SPFactory +contract MockServiceProviderFactory is RegistryContract { + uint max; + + constructor() public + { + // Configure test max + max = 100000000 * 10**uint256(18); + } + + /// @notice Calculate the stake for an account based on total number of registered services + function getAccountStakeBounds(address sp) + external view returns (uint minStake, uint maxStake) + { + return (0, max); + } +} diff --git a/eth-contracts/contracts/service/ServiceProviderFactory.sol b/eth-contracts/contracts/service/ServiceProviderFactory.sol index 79f128ee804..0c560f74931 100644 --- a/eth-contracts/contracts/service/ServiceProviderFactory.sol +++ b/eth-contracts/contracts/service/ServiceProviderFactory.sol @@ -22,13 +22,24 @@ contract ServiceProviderFactory is RegistryContract { } mapping(bytes32 => ServiceInstanceStakeRequirements) serviceTypeStakeRequirements; - // END Temporary data structures - // Maps directly staked amount by SP, not including delegators - mapping(address => uint) spDeployerStake; + // Stores following entities + // 1) Directly staked amount by SP, not including delegators + // 2) % Cut of delegator tokens taken during reward + // 3) Bool indicating whether this SP has met min/max requirements + struct ServiceProviderDetails { + uint deployerStake; + uint deployerCut; + bool validBounds; + } + + mapping(address => ServiceProviderDetails) spDetails; - // % Cut of delegator tokens assigned to sp deployer - mapping(address => uint) spDeployerCut; + // Minimum direct staked by service provider + // Static regardless of total number of endpoints for a given account + uint minDirectDeployerStake; + + // END Temporary data structures bytes empty; @@ -61,11 +72,11 @@ contract ServiceProviderFactory is RegistryContract { ); event UpdateEndpoint( - bytes32 _serviceType, - address _owner, - string _oldEndpoint, - string _newEndpoint, - uint spId + bytes32 _serviceType, + address _owner, + string _oldEndpoint, + string _newEndpoint, + uint spId ); constructor( @@ -102,6 +113,9 @@ contract ServiceProviderFactory is RegistryContract { minStake: 10 * 10**uint256(DECIMALS), maxStake: 10000000 * 10**uint256(DECIMALS) }); + + // Configure direct minimum stake for deployer + minDirectDeployerStake = 5 * 10**uint256(DECIMALS); } function register( @@ -135,9 +149,9 @@ contract ServiceProviderFactory is RegistryContract { ); // Update deployer total - spDeployerStake[owner] += _stakeAmount; + spDetails[owner].deployerStake += _stakeAmount; - uint currentlyStakedForOwner = validateAccountStakeBalances(owner); + uint currentlyStakedForOwner = this.validateAccountStakeBalance(owner); emit RegisteredServiceProvider( newServiceProviderID, @@ -147,6 +161,10 @@ contract ServiceProviderFactory is RegistryContract { currentlyStakedForOwner ); + // Confirm both aggregate account balance and directly staked amount are valid + this.validateAccountStakeBalance(owner); + validateServiceProviderDirectStake(owner); + return newServiceProviderID; } @@ -163,6 +181,7 @@ contract ServiceProviderFactory is RegistryContract { // Unstake on deregistration if and only if this is the last service endpoint uint unstakeAmount = 0; + bool unstaked = false; // owned by the user if (numberOfEndpoints == 1) { unstakeAmount = Staking( @@ -176,7 +195,8 @@ contract ServiceProviderFactory is RegistryContract { ); // Update deployer total - spDeployerStake[owner] -= unstakeAmount; + spDetails[owner].deployerStake -= unstakeAmount; + unstaked = true; } (uint deregisteredID) = ServiceProviderStorageInterface( @@ -193,7 +213,12 @@ contract ServiceProviderFactory is RegistryContract { _endpoint, unstakeAmount); - validateAccountStakeBalances(owner); + // Confirm both aggregate account balance and directly staked amount are valid + // Only if unstake operation has not occurred + if (!unstaked) { + this.validateAccountStakeBalance(owner); + validateServiceProviderDirectStake(owner); + } return deregisteredID; } @@ -221,10 +246,12 @@ contract ServiceProviderFactory is RegistryContract { newStakeAmount ); - validateAccountStakeBalances(owner); - // Update deployer total - spDeployerStake[owner] += _increaseStakeAmount; + spDetails[owner].deployerStake += _increaseStakeAmount; + + // Confirm both aggregate account balance and directly staked amount are valid + this.validateAccountStakeBalance(owner); + validateServiceProviderDirectStake(owner); return newStakeAmount; } @@ -262,10 +289,12 @@ contract ServiceProviderFactory is RegistryContract { newStakeAmount ); - validateAccountStakeBalances(owner); - // Update deployer total - spDeployerStake[owner] -= _decreaseStakeAmount; + spDetails[owner].deployerStake -= _decreaseStakeAmount; + + // Confirm both aggregate account balance and directly staked amount are valid + this.validateAccountStakeBalance(owner); + validateServiceProviderDirectStake(owner); return newStakeAmount; } @@ -316,7 +345,26 @@ contract ServiceProviderFactory is RegistryContract { uint _amount ) external { - spDeployerStake[_serviceProvider] = _amount; + // Update SP tracked total + spDetails[_serviceProvider].deployerStake = _amount; + this.updateServiceProviderBoundStatus(_serviceProvider); + } + + /** + * @notice Update service provider bound status + * TODO: Permission to only delegatemanager OR this + */ + function updateServiceProviderBoundStatus(address _serviceProvider) external { + Staking stakingContract = Staking( + registry.getContract(stakingProxyOwnerKey) + ); + // Validate bounds for total stake + uint totalSPStake = stakingContract.totalStakedFor(_serviceProvider); + (uint minStake, uint maxStake) = this.getAccountStakeBounds(_serviceProvider); + if (totalSPStake < minStake || totalSPStake > maxStake) { + // Indicate this service provider is out of bounds + spDetails[_serviceProvider].validBounds = false; + } } /** @@ -335,7 +383,7 @@ contract ServiceProviderFactory is RegistryContract { require( _cut <= DEPLOYER_CUT_BASE, "Service Provider cut cannot exceed base value"); - spDeployerCut[_serviceProvider] = _cut; + spDetails[_serviceProvider].deployerCut = _cut; } /** @@ -344,7 +392,7 @@ contract ServiceProviderFactory is RegistryContract { function getServiceProviderStake(address _address) external view returns (uint stake) { - return spDeployerStake[_address]; + return spDetails[_address].deployerStake; } /** @@ -353,7 +401,7 @@ contract ServiceProviderFactory is RegistryContract { function getServiceProviderDeployerCut(address _address) external view returns (uint cut) { - return spDeployerCut[_address]; + return spDetails[_address].deployerCut; } /** @@ -394,6 +442,12 @@ contract ServiceProviderFactory is RegistryContract { ).getServiceProviderIdFromEndpoint(_endpoint); } + function getMinDirectDeployerStake() + external view returns (uint min) + { + return minDirectDeployerStake; + } + function getServiceProviderIdsFromAddress(address _ownerAddress, bytes32 _serviceType) external view returns (uint[] memory spIds) { @@ -448,6 +502,7 @@ contract ServiceProviderFactory is RegistryContract { } /// @notice Calculate the stake for an account based on total number of registered services + // TODO: Cache value function getAccountStakeBounds(address sp) external view returns (uint min, uint max) { @@ -464,9 +519,17 @@ contract ServiceProviderFactory is RegistryContract { return (minStake, maxStake); } + // @notice Returns status of service provider total stake and relation to bounds + function isServiceProviderWithinBounds(address sp) + external view returns (bool isValid) + { + return spDetails[sp].validBounds; + } + /// @notice Validate that the service provider is between the min and max stakes for all their registered services - function validateAccountStakeBalances(address sp) - internal view returns (uint stakedForOwner) + // Permission to 'this' contract or delegate manager + function validateAccountStakeBalance(address sp) + external returns (uint stakedForOwner) { Staking stakingContract = Staking( registry.getContract(stakingProxyOwnerKey) @@ -481,6 +544,19 @@ contract ServiceProviderFactory is RegistryContract { require( currentlyStakedForOwner <= maxStakeAmount, "Maximum stake amount exceeded"); + + // Indicate this service provider is within bounds + spDetails[sp].validBounds = true; + return currentlyStakedForOwner; } + + function validateServiceProviderDirectStake(address sp) + internal view returns (uint directStake) + { + require( + spDetails[sp].deployerStake >= minDirectDeployerStake, + "Direct stake restriction violated for this service provider"); + return spDetails[sp].deployerStake; + } } diff --git a/eth-contracts/migrations/6_claim_factory_migration.js b/eth-contracts/migrations/6_claim_factory_migration.js index b3116290390..d076e5b2035 100644 --- a/eth-contracts/migrations/6_claim_factory_migration.js +++ b/eth-contracts/migrations/6_claim_factory_migration.js @@ -4,6 +4,7 @@ const OwnedUpgradeabilityProxy = artifacts.require('OwnedUpgradeabilityProxy') const Registry = artifacts.require('Registry') const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') const claimFactoryKey = web3.utils.utf8ToHex('ClaimFactory') +const serviceProviderFactoryKey = web3.utils.utf8ToHex('ServiceProviderFactory') module.exports = (deployer, network, accounts) => { deployer.then(async () => { @@ -16,7 +17,8 @@ module.exports = (deployer, network, accounts) => { ClaimFactory, AudiusToken.address, registry.address, - ownedUpgradeabilityProxyKey) + ownedUpgradeabilityProxyKey, + serviceProviderFactoryKey) let claimFactory = await ClaimFactory.deployed() diff --git a/eth-contracts/test/claimFactory.test.js b/eth-contracts/test/claimFactory.test.js index 6cd6b02f283..4596fbaec96 100644 --- a/eth-contracts/test/claimFactory.test.js +++ b/eth-contracts/test/claimFactory.test.js @@ -4,10 +4,12 @@ const AudiusToken = artifacts.require('AudiusToken') const Registry = artifacts.require('Registry') const ClaimFactory = artifacts.require('ClaimFactory') const OwnedUpgradeabilityProxy = artifacts.require('OwnedUpgradeabilityProxy') +const MockServiceProviderFactory = artifacts.require('MockServiceProviderFactory') const Staking = artifacts.require('Staking') const encodeCall = require('./encodeCall') const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') +const serviceProviderFactoryKey = web3.utils.utf8ToHex('ServiceProviderFactory') const fromBn = n => parseInt(n.valueOf(), 10) @@ -80,11 +82,16 @@ contract('ClaimFactory', async (accounts) => { staking = await Staking.at(proxy.address) staker = accounts[2] + // Mock SP for test + let mockSPFactory = await MockServiceProviderFactory.new({ from: accounts[0] }) + await registry.addContract(serviceProviderFactoryKey, mockSPFactory.address) + // Create new claim factory instance claimFactory = await ClaimFactory.new( token.address, registry.address, ownedUpgradeabilityProxyKey, + serviceProviderFactoryKey, { from: accounts[0] }) // Register new contract as a minter, from the same address that deployed the contract diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 418b248c5be..a07893879b2 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -33,6 +33,7 @@ const claimFactoryKey = web3.utils.utf8ToHex('ClaimFactory') const testDiscProvType = web3.utils.utf8ToHex('discovery-provider') const testEndpoint = 'https://localhost:5000' const testEndpoint1 = 'https://localhost:5001' +const testEndpoint3 = 'https://localhost:5002' // 1000 AUD converted to AUDWei, multiplying by 10^18 const INITIAL_BAL = toWei(1000) @@ -106,6 +107,7 @@ contract('DelegateManager', async (accounts) => { token.address, registry.address, ownedUpgradeabilityProxyKey, + serviceProviderFactoryKey, { from: accounts[0] }) await registry.addContract(claimFactoryKey, claimFactory.address) @@ -140,6 +142,31 @@ contract('DelegateManager', async (accounts) => { return args } + const increaseRegisteredProviderStake = async (increase, account) => { + // Approve token transfer + await token.approve( + stakingAddress, + increase, + { from: account }) + + let tx = await serviceProviderFactory.increaseStake( + increase, + { from: account }) + + let args = tx.logs.find(log => log.event === 'UpdatedStakeAmount').args + // console.dir(args, { depth: 5 }) + } + + const decreaseRegisteredProviderStake = async (decrease, account) => { + // Approve token transfer from staking contract to account + let tx = await serviceProviderFactory.decreaseStake( + decrease, + { from: account }) + + let args = tx.logs.find(log => log.event === 'UpdatedStakeAmount').args + // console.dir(args, { depth: 5 }) + } + const getAccountStakeInfo = async (account, print = false) => { let spFactoryStake let totalInStakingContract @@ -163,12 +190,7 @@ contract('DelegateManager', async (accounts) => { let outsideStake = spFactoryStake.add(delegatedStake) let totalActiveStake = outsideStake.sub(lockedUpStake) let stakeDiscrepancy = totalInStakingContract.sub(outsideStake) - if (print) { - console.log(`${account} SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}`) - console.log(`${account} Outside Stake: ${outsideStake} Staking: ${totalInStakingContract}`) - console.log(`(Staking) vs (DelegateManager + SPFactory) Stake discrepancy: ${stakeDiscrepancy}`) - } - return { + let accountSummary = { totalInStakingContract, delegatedStake, spFactoryStake, @@ -177,6 +199,14 @@ contract('DelegateManager', async (accounts) => { lockedUpStake, totalActiveStake } + + if (print) { + console.log(`${account} SpFactory: ${spFactoryStake}, DelegateManager: ${delegatedStake}`) + console.log(`${account} Outside Stake: ${outsideStake} Staking: ${totalInStakingContract}`) + console.log(`(Staking) vs (DelegateManager + SPFactory) Stake discrepancy: ${stakeDiscrepancy}`) + console.dir(accountSummary, { depth: 5 }) + } + return accountSummary } describe('Delegation tests', () => { @@ -604,7 +634,7 @@ contract('DelegateManager', async (accounts) => { 'Confirm reward issued to service provider') }) - // Confirm a pending undelegate operation negates any claimed value + // Confirm a pending undelegate operation is negated by a slash to the account it('single delegator + undelegate + slash', async () => { let initialDelegateAmount = toWei(60) let slashAmount = toWei(100) @@ -740,5 +770,157 @@ contract('DelegateManager', async (accounts) => { await delegateManager.claimRewards({ from: stakerAccount }) }) + + it('slash below sp bounds', async () => { + let preSlashInfo = await getAccountStakeInfo(stakerAccount, false) + // Set slash amount to all but 1 AUD for this SP + let diffAmount = toWei(1) + let slashAmount = (preSlashInfo.spFactoryStake).sub(diffAmount) + + // Perform slash functions + await delegateManager.slash(slashAmount, slasherAccount) + + let isWithinBounds = await serviceProviderFactory.isServiceProviderWithinBounds(slasherAccount) + assert.isFalse( + isWithinBounds, + 'Bound violation expected') + + // Initiate round + await claimFactory.initiateRound() + + // Confirm claim is pending + let pendingClaim = await claimFactory.claimPending(stakerAccount) + assert.isTrue(pendingClaim, 'ClaimFactory expected to consider claim pending') + + // Confirm claim fails due to bound violation + await _lib.assertRevert( + delegateManager.claimRewards({ from: stakerAccount }), + 'Service provider must be within bounds' + ) + + // Try to increase by diffAmount, but expect rejection since lower bound is unmet + await _lib.assertRevert( + increaseRegisteredProviderStake( + diffAmount, + stakerAccount), + 'Minimum stake threshold exceeded') + + // Increase to minimum + let bounds = await serviceProviderFactory.getAccountStakeBounds(stakerAccount) + let info = await getAccountStakeInfo(stakerAccount, false) + let increase = (bounds.min).sub(info.spFactoryStake) + // Increase to minimum bound + await increaseRegisteredProviderStake( + increase, + stakerAccount) + + // Validate increase + isWithinBounds = await serviceProviderFactory.isServiceProviderWithinBounds(slasherAccount) + assert.isTrue( + isWithinBounds, + 'Valid bound expected') + + // Confirm claim STILL fails due to bound violation at fundblock + await _lib.assertRevert( + delegateManager.claimRewards({ from: stakerAccount }), + 'Minimum stake bounds violated at fund block' + ) + }) + + it('delegator increase/decrease + SP direct stake bound validation', async () => { + let bounds = await serviceProviderFactory.getAccountStakeBounds(stakerAccount) + let delegateAmount = bounds.min + let info = await getAccountStakeInfo(stakerAccount, false) + let failedIncreaseAmount = bounds.max + // Transfer sufficient funds + await token.transfer(delegatorAccount1, failedIncreaseAmount, { from: treasuryAddress }) + // Approve staking transfer + await token.approve(stakingAddress, failedIncreaseAmount, { from: delegatorAccount1 }) + await _lib.assertRevert( + delegateManager.delegateStake( + stakerAccount, + failedIncreaseAmount, + { from: delegatorAccount1 }), + 'Maximum stake amount exceeded' + ) + let infoAfterFailure = await getAccountStakeInfo(stakerAccount, false) + assert.isTrue( + (info.delegatedStake).eq(infoAfterFailure.delegatedStake), + 'No increase in delegated stake expected') + + // Delegate min stake amount + await token.approve( + stakingAddress, + delegateAmount, + { from: delegatorAccount1 }) + delegateManager.delegateStake( + stakerAccount, + delegateAmount, + { from: delegatorAccount1 }) + + // Remove deployer direct stake + // Decrease by all but 1 AUD direct stake + let spFactoryStake = infoAfterFailure.spFactoryStake + let diff = toWei(1) + // Confirm failure as direct stake threshold is violated + // Due to the total delegated stake equal to min bounds, total account stake balance will NOT violate bounds + await _lib.assertRevert( + decreaseRegisteredProviderStake(spFactoryStake.sub(diff), stakerAccount), + 'Direct stake restriction violated for this service provider' + ) + + // Decrease to min + let spInfo = await getAccountStakeInfo(stakerAccount, false) + let minDirectStake = await serviceProviderFactory.getMinDirectDeployerStake() + let diffToMin = (spInfo.spFactoryStake).sub(minDirectStake) + await decreaseRegisteredProviderStake(diffToMin, stakerAccount) + let infoAfterDecrease = await getAccountStakeInfo(stakerAccount, false) + assert.isTrue( + (infoAfterDecrease.spFactoryStake).eq(minDirectStake), + 'Expect min direct stake while within total account bounds') + + // At this point we have a total stake of 2x the minimum for this SP + // 1x Min directly from SP + // 1x Min from our single delegator + // So - a service provider should be able to register with NO additional stake and still be within bounds + await registerServiceProvider( + testDiscProvType, + testEndpoint3, + toWei(0), + stakerAccount) + + let infoAfterSecondEndpoint = await getAccountStakeInfo(stakerAccount, false) + assert.isTrue( + (infoAfterSecondEndpoint.totalInStakingContract).eq(infoAfterDecrease.totalInStakingContract), + 'Expect static total stake after new SP endpoint' + ) + + // Now, initiate a request to undelegate for this SP + await delegateManager.requestUndelegateStake( + stakerAccount, + delegateAmount, + { from: delegatorAccount1 } + ) + // Confirm lockup amount is registered + let undelegateRequestInfo = await delegateManager.getPendingUndelegateRequest(delegatorAccount1) + assert.isTrue( + undelegateRequestInfo.amount.eq(delegateAmount), + 'Expect request to match undelegate amount') + + // Advance to valid block + await _lib.advanceToTargetBlock( + fromBn(undelegateRequestInfo.lockupExpiryBlock), + web3 + ) + let currentBlock = await web3.eth.getBlock('latest') + let currentBlockNum = currentBlock.number + assert.isTrue( + (web3.utils.toBN(currentBlockNum)).gte(undelegateRequestInfo.lockupExpiryBlock), + 'Confirm expired lockup period') + // Try to execute undelegate stake, but fail due to min bound violation + await _lib.assertRevert( + delegateManager.undelegateStake({ from: delegatorAccount1 }), + 'Minimum stake threshold exceeded') + }) }) }) From 4287f941643f4f084f8e958536784785e4fa2e26 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Thu, 9 Apr 2020 20:57:23 -0400 Subject: [PATCH 36/39] Finishing touches for v0 --- .../contracts/service/DelegateManager.sol | 7 ------- .../service/MockServiceProviderFactory.sol | 2 +- eth-contracts/contracts/staking/Staking.sol | 1 - .../7_delegate_manager_migration.js | 4 ++++ eth-contracts/test/delegateManager.test.js | 20 +++++++++++++++++++ 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 3c69aaef0de..930ee6b0a07 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -62,10 +62,6 @@ contract DelegateManager is RegistryContract { // TODO: Evaluate whether this is necessary bytes empty; - event Test( - uint256 test, - string msg); - event IncreaseDelegatedStake( address _delegator, address _serviceProvider, @@ -105,9 +101,6 @@ contract DelegateManager is RegistryContract { claimFactoryKey = _claimFactoryKey; } - // TODO: Require _target is a valid SP - // TODO: Validate sp account total balance - // TODO: Enforce min _amount? function delegateStake( address _target, uint _amount diff --git a/eth-contracts/contracts/service/MockServiceProviderFactory.sol b/eth-contracts/contracts/service/MockServiceProviderFactory.sol index 83af002b1d3..62c7f3b1dd9 100644 --- a/eth-contracts/contracts/service/MockServiceProviderFactory.sol +++ b/eth-contracts/contracts/service/MockServiceProviderFactory.sol @@ -15,7 +15,7 @@ contract MockServiceProviderFactory is RegistryContract { } /// @notice Calculate the stake for an account based on total number of registered services - function getAccountStakeBounds(address sp) + function getAccountStakeBounds(address) external view returns (uint minStake, uint maxStake) { return (0, max); diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index edf83fd25c0..16bf6f35a30 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -146,7 +146,6 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { * @param _amount Number of tokens staked * @param _data Used in Unstaked event, to add signalling information in more complex staking applications */ - // TODO: Convert to internal model w/transfer address and account address function unstake(uint256 _amount, bytes calldata _data) external isInitialized { _unstakeFor( msg.sender, diff --git a/eth-contracts/migrations/7_delegate_manager_migration.js b/eth-contracts/migrations/7_delegate_manager_migration.js index 6a73ac574bd..d1cab30cd5c 100644 --- a/eth-contracts/migrations/7_delegate_manager_migration.js +++ b/eth-contracts/migrations/7_delegate_manager_migration.js @@ -4,6 +4,7 @@ const Registry = artifacts.require('Registry') const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') const claimFactoryKey = web3.utils.utf8ToHex('ClaimFactory') const serviceProviderFactoryKey = web3.utils.utf8ToHex('ServiceProviderFactory') +const delegateManagerKey = web3.utils.utf8ToHex('DelegateManager') module.exports = (deployer, network, accounts) => { deployer.then(async () => { @@ -18,5 +19,8 @@ module.exports = (deployer, network, accounts) => { ownedUpgradeabilityProxyKey, serviceProviderFactoryKey, claimFactoryKey) + + let delegateManager = await DelegateManager.deployed() + await registry.addContract(delegateManagerKey, delegateManager.address) }) } diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index a07893879b2..81748fa44a7 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -675,6 +675,26 @@ contract('DelegateManager', async (accounts) => { 'Expect no lockup funds to carry over') }) + it('single delegator to invalid SP', async () => { + let initialDelegateAmount = toWei(60) + + // Approve staking transfer + await token.approve( + stakingAddress, + initialDelegateAmount, + { from: delegatorAccount1 }) + + // Confirm maximum bounds exceeded for SP w/zero endpoints + await _lib.assertRevert( + delegateManager.delegateStake( + accounts[8], + initialDelegateAmount, + { from: delegatorAccount1 }), + 'Maximum stake amount exceeded' + ) + }) + + it('3 delegators + pending claim + undelegate restrictions', async () => { const delegatorAccount2 = accounts[5] const delegatorAccount3 = accounts[6] From 4523b348f560a61c1768f1fe436d12120114a83c Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Thu, 9 Apr 2020 20:58:17 -0400 Subject: [PATCH 37/39] RM test --- eth-contracts/contracts/staking/Staking.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index 16bf6f35a30..24e2323d254 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -49,10 +49,6 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { uint256 amountClaimed ); - event Test( - uint256 test, - string msg); - function initialize(address _stakingToken, address _treasuryAddress) external onlyInit { require(isContract(_stakingToken), ERROR_TOKEN_NOT_CONTRACT); initialized(); From 00312e43d22bd8f08d6de884bf40255fc18b9af0 Mon Sep 17 00:00:00 2001 From: Sid Sethi Date: Fri, 10 Apr 2020 14:48:23 -0400 Subject: [PATCH 38/39] Generic Governance v1 (Registry integration, DelegationManager integration, Slash changed to burn) (#365) --- eth-contracts/contracts/Governance.sol | 367 ++++++++++++++ eth-contracts/contracts/erc20/AudiusToken.sol | 4 +- eth-contracts/contracts/staking/Staking.sol | 48 +- .../migrations/8_governance_migration.js | 23 + eth-contracts/package-lock.json | 7 + eth-contracts/package.json | 3 +- eth-contracts/test/_lib/lib.js | 44 +- eth-contracts/test/audiusToken.test.js | 66 ++- eth-contracts/test/delegateManager.test.js | 1 + eth-contracts/test/governance.test.js | 472 ++++++++++++++++++ eth-contracts/test/registry.test.js | 9 + eth-contracts/test/serviceProvider.test.js | 4 +- eth-contracts/test/staking.test.js | 50 +- 13 files changed, 1036 insertions(+), 62 deletions(-) create mode 100644 eth-contracts/contracts/Governance.sol create mode 100644 eth-contracts/migrations/8_governance_migration.js create mode 100644 eth-contracts/test/governance.test.js diff --git a/eth-contracts/contracts/Governance.sol b/eth-contracts/contracts/Governance.sol new file mode 100644 index 00000000000..f7245b0e08b --- /dev/null +++ b/eth-contracts/contracts/Governance.sol @@ -0,0 +1,367 @@ +pragma solidity ^0.5.0; + +import "./service/registry/RegistryContract.sol"; +import "./staking/Staking.sol"; +import "./service/interface/registry/RegistryInterface.sol"; + + +contract Governance { + RegistryInterface registry; + bytes32 stakingProxyOwnerKey; + + uint256 votingPeriod; + uint256 votingQuorum; + + /***** Enums *****/ + enum Outcome {InProgress, No, Yes, Invalid} + // Enum values map to uints, so first value in Enum always is 0. + enum Vote {None, No, Yes} + + struct Proposal { + uint256 proposalId; + address proposer; + uint256 startBlockNumber; + bytes32 targetContractRegistryKey; + address targetContractAddress; + uint callValue; + string signature; + bytes callData; + Outcome outcome; + uint256 voteMagnitudeYes; + uint256 voteMagnitudeNo; + uint256 numVotes; + mapping(address => Vote) votes; + } + + /***** Proposal storage *****/ + uint256 lastProposalId = 0; + mapping(uint256 => Proposal) proposals; + + /***** Events *****/ + event ProposalSubmitted( + uint256 indexed proposalId, + address indexed proposer, + uint256 startBlockNumber, + string description + ); + event ProposalVoteSubmitted( + uint256 indexed proposalId, + address indexed voter, + Vote indexed vote, + uint256 voterStake, + Vote previousVote + ); + event ProposalOutcomeEvaluated( + uint256 indexed proposalId, + Outcome indexed outcome, + uint256 voteMagnitudeYes, + uint256 voteMagnitudeNo, + uint256 numVotes + ); + event TransactionExecuted( + bytes32 indexed txHash, + address targetContractAddress, + uint callValue, + string signature, + bytes callData, + bytes returnData + ); + + constructor( + address _registryAddress, + bytes32 _stakingProxyOwnerKey, + uint256 _votingPeriod, + uint256 _votingQuorum + ) public { + require(_registryAddress != address(0x00), "Requires non-zero _registryAddress"); + registry = RegistryInterface(_registryAddress); + + stakingProxyOwnerKey = _stakingProxyOwnerKey; + + require(_votingPeriod > 0, "Requires non-zero _votingPeriod"); + votingPeriod = _votingPeriod; + + require(_votingQuorum > 0, "Requires non-zero _votingQuorum"); + votingQuorum = _votingQuorum; + } + + // ========================================= Governance Actions ========================================= + + function submitProposal( + bytes32 _targetContractRegistryKey, + uint256 _callValue, + string calldata _signature, + bytes calldata _callData, + string calldata _description + ) external returns (uint256 proposalId) + { + address proposer = msg.sender; + + // Require proposer is active Staker + Staking stakingContract = Staking(registry.getContract(stakingProxyOwnerKey)); + require( + stakingContract.totalStakedFor(proposer) > 0, + "Proposer must be active staker with non-zero stake." + ); + + // Require _targetContractRegistryKey points to a valid registered contract + address targetContractAddress = registry.getContract(_targetContractRegistryKey); + require( + targetContractAddress != address(0x00), + "_targetContractRegistryKey must point to valid registered contract" + ); + + // set proposalId + uint256 newProposalId = lastProposalId + 1; + + // Store new Proposal obj in proposals mapping + proposals[newProposalId] = Proposal({ + proposalId: newProposalId, + proposer: proposer, + startBlockNumber: block.number, + targetContractRegistryKey: _targetContractRegistryKey, + targetContractAddress: targetContractAddress, + callValue: _callValue, + signature: _signature, + callData: _callData, + outcome: Outcome.InProgress, + voteMagnitudeYes: 0, + voteMagnitudeNo: 0, + numVotes: 0 + /** votes: mappings are auto-initialized to default state */ + }); + + emit ProposalSubmitted( + newProposalId, + proposer, + block.number, + _description + ); + + lastProposalId += 1; + + return newProposalId; + } + + function submitProposalVote(uint256 _proposalId, Vote _vote) external { + address voter = msg.sender; + + require( + _proposalId <= lastProposalId && _proposalId > 0, + "Must provide valid non-zero _proposalId" + ); + + // Require voter is active Staker + get voterStake. + Staking stakingContract = Staking(registry.getContract(stakingProxyOwnerKey)); + uint256 voterStake = stakingContract.totalStakedForAt( + voter, + proposals[_proposalId].startBlockNumber + ); + require(voterStake > 0, "Voter must be active staker with non-zero stake."); + + // Require proposal votingPeriod is still active. + uint256 startBlockNumber = proposals[_proposalId].startBlockNumber; + uint256 endBlockNumber = startBlockNumber + votingPeriod; + require( + block.number > startBlockNumber && block.number <= endBlockNumber, + "Proposal votingPeriod has ended" + ); + + // Require vote is not None. + require(_vote != Vote.None, "Cannot submit None vote"); + + // Record previous vote. + Vote previousVote = proposals[_proposalId].votes[voter]; + + // Will override staker's previous vote if present. + proposals[_proposalId].votes[voter] = _vote; + + /** Update voteMagnitudes accordingly */ + + // New voter (Vote enum defaults to 0) + if (previousVote == Vote.None) { + if (_vote == Vote.Yes) { + proposals[_proposalId].voteMagnitudeYes += voterStake; + } else { + proposals[_proposalId].voteMagnitudeNo += voterStake; + } + proposals[_proposalId].numVotes += 1; + } else { // Repeat voter + if (previousVote == Vote.Yes && _vote == Vote.No) { + proposals[_proposalId].voteMagnitudeYes -= voterStake; + proposals[_proposalId].voteMagnitudeNo += voterStake; + } else if (previousVote == Vote.No && _vote == Vote.Yes) { + proposals[_proposalId].voteMagnitudeYes += voterStake; + proposals[_proposalId].voteMagnitudeNo -= voterStake; + } + // If _vote == previousVote, no changes needed to vote magnitudes. + } + + emit ProposalVoteSubmitted( + _proposalId, + voter, + _vote, + voterStake, + previousVote + ); + } + + function evaluateProposalOutcome(uint256 _proposalId) + external returns (Outcome proposalOutcome) + { + require( + _proposalId <= lastProposalId && _proposalId > 0, + "Must provide valid non-zero _proposalId" + ); + + // Require msg.sender is active Staker. + Staking stakingContract = Staking(registry.getContract(stakingProxyOwnerKey)); + require( + stakingContract.totalStakedForAt( + msg.sender, proposals[_proposalId].startBlockNumber + ) > 0, + "Caller must be active staker with non-zero stake." + ); + + // Require proposal votingPeriod has ended. + uint256 startBlockNumber = proposals[_proposalId].startBlockNumber; + uint256 endBlockNumber = startBlockNumber + votingPeriod; + require( + block.number > endBlockNumber, + "Proposal votingPeriod must end before evaluation." + ); + + // Require registered contract address for provided registryKey has not changed. + address targetContractAddress = registry.getContract( + proposals[_proposalId].targetContractRegistryKey + ); + require( + targetContractAddress == proposals[_proposalId].targetContractAddress, + "Registered contract address for targetContractRegistryKey has changed" + ); + + // Calculate outcome + Outcome outcome; + if (proposals[_proposalId].numVotes < votingQuorum) { + outcome = Outcome.Invalid; + } else if ( + proposals[_proposalId].voteMagnitudeYes >= proposals[_proposalId].voteMagnitudeNo + ) { + outcome = Outcome.Yes; + + _executeTransaction( + proposals[_proposalId].targetContractAddress, + proposals[_proposalId].callValue, + proposals[_proposalId].signature, + proposals[_proposalId].callData + ); + } else { + outcome = Outcome.No; + } + + // Record outcome + proposals[_proposalId].outcome = outcome; + + emit ProposalOutcomeEvaluated( + _proposalId, + outcome, + proposals[_proposalId].voteMagnitudeYes, + proposals[_proposalId].voteMagnitudeNo, + proposals[_proposalId].numVotes + ); + + return outcome; + } + + // ========================================= Getters ========================================= + + function getProposalById(uint256 _proposalId) + external view returns ( + uint256 proposalId, + address proposer, + uint256 startBlockNumber, + bytes32 targetContractRegistryKey, + address targetContractAddress, + uint callValue, + string memory signature, + bytes memory callData, + Outcome outcome, + uint256 voteMagnitudeYes, + uint256 voteMagnitudeNo, + uint256 numVotes + ) + { + require( + _proposalId <= lastProposalId && _proposalId > 0, + "Must provide valid non-zero _proposalId" + ); + + Proposal memory proposal = proposals[_proposalId]; + return ( + proposal.proposalId, + proposal.proposer, + proposal.startBlockNumber, + proposal.targetContractRegistryKey, + proposal.targetContractAddress, + proposal.callValue, + proposal.signature, + proposal.callData, + proposal.outcome, + proposal.voteMagnitudeYes, + proposal.voteMagnitudeNo, + proposal.numVotes + /** @notice - votes mapping cannot be returned by external function */ + ); + } + + function getVoteByProposalAndVoter(uint256 _proposalId, address _voter) + external view returns (Vote vote) + { + require( + _proposalId <= lastProposalId && _proposalId > 0, + "Must provide valid non-zero _proposalId" + ); + return proposals[_proposalId].votes[_voter]; + } + + // ========================================= Private ========================================= + + function _executeTransaction( + address _targetContractAddress, + uint256 _callValue, + string memory _signature, + bytes memory _callData + ) internal returns (bytes memory /** returnData */) + { + bytes32 txHash = keccak256( + abi.encode( + _targetContractAddress, _callValue, _signature, _callData + ) + ); + + bytes memory callData; + + if (bytes(_signature).length == 0) { + callData = _callData; + } else { + callData = abi.encodePacked(bytes4(keccak256(bytes(_signature))), _callData); + } + + (bool success, bytes memory returnData) = ( + // solium-disable-next-line security/no-call-value + _targetContractAddress.call.value(_callValue)(callData) + ); + require(success, "Governance::executeTransaction:Transaction execution reverted."); + + emit TransactionExecuted( + txHash, + _targetContractAddress, + _callValue, + _signature, + _callData, + returnData + ); + + return returnData; + } +} \ No newline at end of file diff --git a/eth-contracts/contracts/erc20/AudiusToken.sol b/eth-contracts/contracts/erc20/AudiusToken.sol index 97384eaba01..3e1ff2f42a5 100644 --- a/eth-contracts/contracts/erc20/AudiusToken.sol +++ b/eth-contracts/contracts/erc20/AudiusToken.sol @@ -4,9 +4,10 @@ import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20Pausable.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/ERC20Burnable.sol"; -contract AudiusToken is ERC20, ERC20Detailed, ERC20Mintable, ERC20Pausable { +contract AudiusToken is ERC20, ERC20Detailed, ERC20Mintable, ERC20Pausable, ERC20Burnable { string constant NAME = "TestAudius"; string constant SYMBOL = "TAUDS"; // standard - imitates relationship between Ether and Wei @@ -22,6 +23,7 @@ contract AudiusToken is ERC20, ERC20Detailed, ERC20Mintable, ERC20Pausable { ERC20Mintable() // ERC20Detailed provides setters/getters for name, symbol, decimals properties ERC20Detailed(NAME, SYMBOL, DECIMALS) + // ERC20Burnable has no constructor ERC20() public { diff --git a/eth-contracts/contracts/staking/Staking.sol b/eth-contracts/contracts/staking/Staking.sol index 24e2323d254..025f5b525bf 100644 --- a/eth-contracts/contracts/staking/Staking.sol +++ b/eth-contracts/contracts/staking/Staking.sol @@ -9,6 +9,7 @@ import "./res/IsContract.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/ERC20Burnable.sol"; contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { @@ -49,6 +50,8 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { uint256 amountClaimed ); + event Slashed(address indexed user, uint256 amount, uint256 total); + function initialize(address _stakingToken, address _treasuryAddress) external onlyInit { require(isContract(_stakingToken), ERROR_TOKEN_NOT_CONTRACT); initialized(); @@ -83,19 +86,25 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { * @param _amount Number of tokens slashed * @param _slashAddress address being slashed */ - function slash(uint256 _amount, address _slashAddress) external isInitialized { + function slash( + uint256 _amount, + address _slashAddress + ) external isInitialized + { // TODO: restrict functionality to delegate manager // require(msg.sender == treasuryAddress, "Slashing functionality locked to treasury owner"); + // unstaking 0 tokens is not allowed require(_amount > 0, ERROR_AMOUNT_ZERO); - // Transfer slashed tokens to treasury address - // TODO: Burn with actual ERC token call - _unstakeFor( + // Burn slashed tokens from account + _burnFor(_slashAddress, _amount); + + emit Slashed( _slashAddress, - treasuryAddress, _amount, - bytes('')); + totalStakedFor(_slashAddress) + ); } /** @@ -313,10 +322,11 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { } function _unstakeFor( - address _stakeAccount, - address _transferAccount, - uint256 _amount, - bytes memory _data) internal + address _stakeAccount, + address _transferAccount, + uint256 _amount, + bytes memory _data + ) internal { require(_amount > 0, ERROR_AMOUNT_ZERO); @@ -333,7 +343,23 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IsContract { _stakeAccount, _amount, totalStakedFor(_stakeAccount), - _data); + _data + ); + } + + function _burnFor(address _stakeAccount, uint256 _amount) internal { + require(_amount > 0, ERROR_AMOUNT_ZERO); + + // checkpoint updated staking balance + _modifyStakeBalance(_stakeAccount, _amount, false); + + // checkpoint total supply + _modifyTotalStaked(_amount, false); + + // burn + ERC20Burnable(address(stakingToken)).burn(_amount); + + /** No event emitted since token.burn() call already emits a Transfer event */ } function _modifyStakeBalance(address _accountAddress, uint256 _by, bool _increase) internal { diff --git a/eth-contracts/migrations/8_governance_migration.js b/eth-contracts/migrations/8_governance_migration.js new file mode 100644 index 00000000000..195c4073906 --- /dev/null +++ b/eth-contracts/migrations/8_governance_migration.js @@ -0,0 +1,23 @@ +const Registry = artifacts.require('Registry') +const Governance = artifacts.require('Governance') + +const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') + +// 48hr * 60 min/hr * 60 sec/min / ~15 sec/block = 11520 blocks +const VotingPeriod = 11520 +// Required number of votes on proposal +const VotingQuorum = 1 + +module.exports = (deployer, network, accounts) => { + deployer.then(async () => { + const registry = await Registry.deployed() + + await deployer.deploy( + Governance, + registry.address, + ownedUpgradeabilityProxyKey, + VotingPeriod, + VotingQuorum + ) + }) +} \ No newline at end of file diff --git a/eth-contracts/package-lock.json b/eth-contracts/package-lock.json index a56e0031256..6161d10f8ac 100644 --- a/eth-contracts/package-lock.json +++ b/eth-contracts/package-lock.json @@ -1024,6 +1024,12 @@ "tweetnacl": "^0.14.3" } }, + "bignumber.js": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-8.0.1.tgz", + "integrity": "sha512-zAySveTJXkgLYCBi0b14xzfnOs+f3G6x36I8w2a1+PFQpWk/dp0mI0F+ZZK2bu+3ELewDcSyP+Cfq++NcHX7sg==", + "dev": true + }, "binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", @@ -9086,6 +9092,7 @@ "requires": { "debug": "^2.2.0", "es5-ext": "^0.10.50", + "gulp": "^4.0.2", "nan": "^2.14.0", "typedarray-to-buffer": "^3.1.5", "yaeti": "^0.0.6" diff --git a/eth-contracts/package.json b/eth-contracts/package.json index 83989593a42..9367b729f9d 100644 --- a/eth-contracts/package.json +++ b/eth-contracts/package.json @@ -33,7 +33,8 @@ "devDependencies": { "standard": "^12.0.1", "async": "^2.6.1", - "babel-register": "^6.26.0" + "babel-register": "^6.26.0", + "bignumber.js": "8.0.1" }, "//": { "dependenciesComments": { diff --git a/eth-contracts/test/_lib/lib.js b/eth-contracts/test/_lib/lib.js index c6df7f0061b..e20b997aa80 100644 --- a/eth-contracts/test/_lib/lib.js +++ b/eth-contracts/test/_lib/lib.js @@ -24,27 +24,49 @@ export const strings = { return web3New.utils.hexToUtf8(arg) } +/** TODO - change all duplicate func declarations to reference this */ +export const getLatestBlock = async (web3) => { + return web3.eth.getBlock('latest') +} + /** Returns formatted transaction receipt object with event and arg info * @param {object} txReceipt - transaction receipt object * @returns {object} w/event + args array from txReceipt */ -export const parseTx = (txReceipt) => { +export const parseTx = (txReceipt, multipleEvents = false) => { if (!txReceipt.logs.length >= 1) { throw new Error('Invalid txReceipt length') } - - if (!(txReceipt.logs[0].hasOwnProperty('event'))) { - throw new Error('Missing event log in tx receipt') - } - - return { - 'event': { - 'name': txReceipt.logs[0].event, - 'args': txReceipt.logs[0].args + + if (multipleEvents) { + let resp = [] + for (const log of txReceipt.logs) { + if (!log.hasOwnProperty('event')) { + throw new Error('Missing event log in tx receipt') + } + resp.push({ + 'event': { + 'name': log.event, + 'args': log.args + } + }) + } + return resp + } else { + if (!(txReceipt.logs[0].hasOwnProperty('event'))) { + throw new Error('Missing event log in tx receipt') + } + + return { + 'event': { + 'name': txReceipt.logs[0].event, + 'args': txReceipt.logs[0].args + } } } } +/** */ export const assertThrows = async (blockOrPromise, expectedErrorCode, expectedReason) => { try { (typeof blockOrPromise === 'function') ? await blockOrPromise() : await blockOrPromise @@ -56,6 +78,7 @@ export const assertThrows = async (blockOrPromise, expectedErrorCode, expectedRe assert(false, `Expected "${expectedErrorCode}"${expectedReason ? ` (with reason: "${expectedReason}")` : ''} but it did not fail`) } +/** */ export const assertRevert = async (blockOrPromise, expectedReason) => { const error = await assertThrows(blockOrPromise, 'revert', expectedReason) if (!expectedReason) { @@ -65,6 +88,7 @@ export const assertRevert = async (blockOrPromise, expectedReason) => { assert.isTrue(expectedMsgFound, `Expected revert reason not found. Expected '${expectedReason}'. Found '${error.message}'`) } +/** */ export const advanceBlock = (web3) => { return new Promise((resolve, reject) => { web3.currentProvider.send({ diff --git a/eth-contracts/test/audiusToken.test.js b/eth-contracts/test/audiusToken.test.js index 6e0cb7da5c2..27826c1f7c0 100644 --- a/eth-contracts/test/audiusToken.test.js +++ b/eth-contracts/test/audiusToken.test.js @@ -8,9 +8,10 @@ contract('AudiusToken', async (accounts) => { const INITIAL_SUPPLY = Math.pow(10,27) // 10^27 = 1 billion tokens, 18 decimal places let token + const treasuryAddress = accounts[0] beforeEach(async () => { - token = await AudiusToken.new({ from: accounts[0] }) + token = await AudiusToken.new({ from: treasuryAddress }) }) it('Initial token properties', async () => { @@ -21,14 +22,14 @@ contract('AudiusToken', async (accounts) => { }) it('initial account balances', async () => { - assert.equal(await token.balanceOf(accounts[0]), INITIAL_SUPPLY) + assert.equal(await token.balanceOf(treasuryAddress), INITIAL_SUPPLY) assert.equal(await token.balanceOf(accounts[1]), 0) }) it('Transfers', async () => { // transfer - await token.transfer(accounts[1], 1000, {from: accounts[0]}) - assert.equal(await token.balanceOf(accounts[0]), INITIAL_SUPPLY - 1000) + await token.transfer(accounts[1], 1000, {from: treasuryAddress}) + assert.equal(await token.balanceOf(treasuryAddress), INITIAL_SUPPLY - 1000) assert.equal(await token.balanceOf(accounts[1]), 1000) // fail to transfer above balance @@ -47,10 +48,45 @@ contract('AudiusToken', async (accounts) => { assert.isTrue(caughtError) }) + it('Burn from treasury', async () => { + const burnAmount = Math.pow(10,3) + + // Confirm token state before burn + assert.equal(await token.balanceOf(treasuryAddress), INITIAL_SUPPLY) + assert.equal(await token.totalSupply(), INITIAL_SUPPLY) + + // Decrease total supply by burning from treasury + await token.burn(burnAmount, { from: treasuryAddress }) + + // Confirm token state after burn + assert.equal(await token.balanceOf(treasuryAddress), INITIAL_SUPPLY - burnAmount) + assert.equal(await token.totalSupply(), INITIAL_SUPPLY - burnAmount) + }) + + it('Burn from account', async () => { + const amount = Math.pow(10,3) + const account = accounts[1] + + // Confirm token state before burn + await token.transfer(account, amount, {from: treasuryAddress}) + assert.equal(await token.balanceOf(treasuryAddress), INITIAL_SUPPLY - amount) + assert.equal(await token.balanceOf(account), amount) + assert.equal(await token.totalSupply(), INITIAL_SUPPLY) + + // Decrease total supply by burning from account + await token.approve(treasuryAddress, amount, { from: account }) + await token.burnFrom(account, amount, { from: treasuryAddress }) + + // Confirm token state after burn + assert.equal(await token.balanceOf(treasuryAddress), INITIAL_SUPPLY - amount) + assert.equal(await token.balanceOf(account), 0) + assert.equal(await token.totalSupply(), INITIAL_SUPPLY - amount) + }) + it('Mint', async () => { // mint tokens - await token.mint(accounts[1], 1000, {from: accounts[0]}) - assert.equal(await token.balanceOf(accounts[0]), INITIAL_SUPPLY) + await token.mint(accounts[1], 1000, {from: treasuryAddress}) + assert.equal(await token.balanceOf(treasuryAddress), INITIAL_SUPPLY) assert.equal(await token.balanceOf(accounts[1]), 1000) assert.equal(await token.totalSupply(), INITIAL_SUPPLY + 1000) @@ -70,18 +106,18 @@ contract('AudiusToken', async (accounts) => { assert.isTrue(caughtError) // add new minter - await token.addMinter(accounts[2], {from: accounts[0]}) + await token.addMinter(accounts[2], {from: treasuryAddress}) assert.isTrue(await token.isMinter(accounts[2])) assert.isFalse(await token.isMinter(accounts[3])) await token.mint(accounts[2], 1000, {from: accounts[2]}) // renounce minter - await token.renounceMinter({from: accounts[0]}) + await token.renounceMinter({from: treasuryAddress}) // fail to mint from renounced minter caughtError = false try { - await token.mint(accounts[4], 1000, {from: accounts[0]}) + await token.mint(accounts[4], 1000, {from: treasuryAddress}) } catch (e) { // catch expected error if (e.message.indexOf('MinterRole: caller does not have the Minter role') >= 0) { @@ -96,13 +132,13 @@ contract('AudiusToken', async (accounts) => { it('Pause', async () => { // pause contract - await token.pause({from: accounts[0]}) + await token.pause({from: treasuryAddress}) assert.isTrue(await token.paused()) // fail to transfer while contract paused let caughtError = false try { - await token.transfer(accounts[1], 1000, {from: accounts[0]}) + await token.transfer(accounts[1], 1000, {from: treasuryAddress}) } catch (e) { // catch expected error if (e.message.indexOf('Pausable: paused') >= 0) { @@ -116,7 +152,7 @@ contract('AudiusToken', async (accounts) => { // add new pauser await token.addPauser(accounts[5]) - assert.isTrue(await token.isPauser(accounts[0])) + assert.isTrue(await token.isPauser(treasuryAddress)) assert.isTrue(await token.isPauser(accounts[5])) // unpause contract @@ -139,14 +175,14 @@ contract('AudiusToken', async (accounts) => { assert.isTrue(caughtError) // renounce pauser - await token.renouncePauser({from: accounts[0]}) - assert.isFalse(await token.isPauser(accounts[0])) + await token.renouncePauser({from: treasuryAddress}) + assert.isFalse(await token.isPauser(treasuryAddress)) assert.isTrue(await token.isPauser(accounts[5])) // fail to pause contract from renounced pauser caughtError = false try { - await token.pause({from: accounts[0]}) + await token.pause({from: treasuryAddress}) } catch (e) { // catch expected error if (e.message.indexOf('PauserRole: caller does not have the Pauser role') >= 0) { diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index 81748fa44a7..ae4ae159b1c 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -211,6 +211,7 @@ contract('DelegateManager', async (accounts) => { describe('Delegation tests', () => { let regTx + beforeEach(async () => { // Transfer 1000 tokens to stakers await token.transfer(stakerAccount, INITIAL_BAL, { from: treasuryAddress }) diff --git a/eth-contracts/test/governance.test.js b/eth-contracts/test/governance.test.js new file mode 100644 index 00000000000..545299c9189 --- /dev/null +++ b/eth-contracts/test/governance.test.js @@ -0,0 +1,472 @@ +const ethers = require('ethers') +const BigNum = require('bignumber.js') +const util = require('util') + +import * as _lib from './_lib/lib.js' +const encodeCall = require('./encodeCall') + +const Registry = artifacts.require('Registry') +const AudiusToken = artifacts.require('AudiusToken') +const OwnedUpgradeabilityProxy = artifacts.require('OwnedUpgradeabilityProxy') +const Staking = artifacts.require('Staking') +const Governance = artifacts.require('Governance') +const ServiceProviderFactory = artifacts.require('ServiceProviderFactory') +const ServiceProviderStorage = artifacts.require('ServiceProviderStorage') +const DelegateManager = artifacts.require('DelegateManager') +const ClaimFactory = artifacts.require('ClaimFactory') + +const ownedUpgradeabilityProxyKey = web3.utils.utf8ToHex('OwnedUpgradeabilityProxy') +const serviceProviderStorageKey = web3.utils.utf8ToHex('ServiceProviderStorage') +const serviceProviderFactoryKey = web3.utils.utf8ToHex('ServiceProviderFactory') +const claimFactoryKey = web3.utils.utf8ToHex('ClaimFactory') +const delegateManagerKey = web3.utils.utf8ToHex('DelegateManagerKey') + +const fromBn = n => parseInt(n.valueOf(), 10) + +const audToWei = (aud) => { + return web3.utils.toBN( + web3.utils.toWei( + aud.toString(), 'ether' + ) + ) +} + +const bigNumberify = (num) => { + return ethers.utils.bigNumberify(new BigNum(num).toFixed()); +} + +const abiEncode = (types, values) => { + const abi = new ethers.utils.AbiCoder() + return abi.encode(types, values) +} + +const abiDecode = (types, data) => { + const abi = new ethers.utils.AbiCoder() + return abi.decode(types, data) +} + +const keccak256 = (values) => { + return ethers.utils.keccak256(values); +} + +const Outcome = Object.freeze({ + InProgress: 0, + No: 1, + Yes: 2, + Invalid: 3 +}) +const Vote = Object.freeze({ + None: 0, + No: 1, + Yes: 2 +}) + +contract('Governance.sol', async (accounts) => { + let proxyContract + let tokenContract + let stakingContract + let registryContract + let serviceProviderStorageContract + let serviceProviderFactoryContract + let claimFactoryContract + let delegateManagerContract + let governanceContract + + const votingPeriod = 10 + const votingQuorum = 1 + const protocolOwnerAddress = accounts[0] + const treasuryAddress = protocolOwnerAddress + const testDiscProvType = web3.utils.utf8ToHex('discovery-provider') + const testEndpoint1 = 'https://localhost:5000' + const testEndpoint2 = 'https://localhost:5001' + + const registerServiceProvider = async (type, endpoint, amount, account) => { + // Approve staking transfer + await tokenContract.approve(stakingContract.address, amount, { from: account }) + + const tx = await serviceProviderFactoryContract.register( + type, + endpoint, + amount, + account, + { from: account } + ) + + const args = tx.logs.find(log => log.event === 'RegisteredServiceProvider').args + args.stakedAmountInt = fromBn(args._stakeAmount) + args.spID = fromBn(args._spID) + return args + } + + /** + * Deploy Registry, OwnedUpgradeabilityProxy, AudiusToken, Staking, and Governance contracts. + */ + beforeEach(async () => { + registryContract = await Registry.new({ from: protocolOwnerAddress }) + proxyContract = await OwnedUpgradeabilityProxy.new({ from: protocolOwnerAddress }) + await registryContract.addContract(ownedUpgradeabilityProxyKey, proxyContract.address, { from: protocolOwnerAddress }) + + tokenContract = await AudiusToken.new({ from: protocolOwnerAddress }) + + const stakingContract0 = await Staking.new({ from: protocolOwnerAddress }) + // Create initialization data + const initializeData = encodeCall( + 'initialize', + ['address', 'address'], + [tokenContract.address, protocolOwnerAddress] + ) + + // Initialize staking contract + await proxyContract.upgradeToAndCall( + stakingContract0.address, + initializeData, + { from: protocolOwnerAddress } + ) + + stakingContract = await Staking.at(proxyContract.address) + + // Deploy + Registery ServiceProviderStorage contract + serviceProviderStorageContract = await ServiceProviderStorage.new(registryContract.address, { from: protocolOwnerAddress }) + await registryContract.addContract(serviceProviderStorageKey, serviceProviderStorageContract.address, { from: protocolOwnerAddress }) + + // Deploy + Register ServiceProviderFactory contract + serviceProviderFactoryContract = await ServiceProviderFactory.new( + registryContract.address, + ownedUpgradeabilityProxyKey, + serviceProviderStorageKey + ) + await registryContract.addContract(serviceProviderFactoryKey, serviceProviderFactoryContract.address, { from: protocolOwnerAddress }) + + // Permission sp factory as caller, from the treasuryAddress, which is proxy owner + await stakingContract.setStakingOwnerAddress(serviceProviderFactoryContract.address, { from: protocolOwnerAddress }) + + // Deploy + Register ClaimFactory contract + claimFactoryContract = await ClaimFactory.new( + tokenContract.address, + registryContract.address, + ownedUpgradeabilityProxyKey, + serviceProviderFactoryKey, + { from: protocolOwnerAddress } + ) + await registryContract.addContract(claimFactoryKey, claimFactoryContract.address, { from: protocolOwnerAddress }) + + // Register new contract as a minter, from the same address that deployed the contract + await tokenContract.addMinter(claimFactoryContract.address, { from: protocolOwnerAddress }) + + // Deploy DelegateManager contract + delegateManagerContract = await DelegateManager.new( + tokenContract.address, + registryContract.address, + ownedUpgradeabilityProxyKey, + serviceProviderFactoryKey, + claimFactoryKey, + { from: protocolOwnerAddress } + ) + await registryContract.addContract(delegateManagerKey, delegateManagerContract.address, { from: protocolOwnerAddress }) + + // Deploy Governance contract + governanceContract = await Governance.new( + registryContract.address, + ownedUpgradeabilityProxyKey, + votingPeriod, + votingQuorum, + { from: protocolOwnerAddress } + ) + }) + + describe('Slash proposal', async () => { + const defaultStakeAmount = audToWei(1000) + const proposalDescription = "TestDescription" + const stakerAccount1 = accounts[1] + const stakerAccount2 = accounts[2] + const delegatorAccount1 = accounts[3] + + beforeEach(async () => { + // Transfer 1000 tokens to stakerAccount1, stakerAccount2, and delegatorAccount1 + await tokenContract.transfer(stakerAccount1, defaultStakeAmount, { from: treasuryAddress }) + await tokenContract.transfer(stakerAccount2, defaultStakeAmount, { from: treasuryAddress }) + await tokenContract.transfer(delegatorAccount1, defaultStakeAmount, { from: treasuryAddress }) + + // Record initial staker account token balance + const initialBalance = await tokenContract.balanceOf(stakerAccount1) + + // Register two SPs with stake + const tx1 = await registerServiceProvider( + testDiscProvType, + testEndpoint1, + defaultStakeAmount, + stakerAccount1 + ) + const tx2 = await registerServiceProvider( + testDiscProvType, + testEndpoint2, + defaultStakeAmount, + stakerAccount2 + ) + + // Confirm event has correct amount + assert.equal(tx1.stakedAmountInt, defaultStakeAmount) + + // Confirm new token balances + const finalBalance = await tokenContract.balanceOf(stakerAccount1) + assert.isTrue( + initialBalance.eq(finalBalance.add(defaultStakeAmount)), + "Expected balances to be equal" + ) + }) + + it('Initial state - Ensure no Proposals exist yet', async () => { + await _lib.assertRevert(governanceContract.getProposalById(0), 'Must provide valid non-zero _proposalId') + await _lib.assertRevert(governanceContract.getProposalById(1), 'Must provide valid non-zero _proposalId') + }) + + it('Should fail to Submit Proposal for unregistered target contract', async () => { + const proposerAddress = accounts[1] + const slashAmount = 1 + const targetAddress = accounts[2] + const targetContractRegistryKey = web3.utils.utf8ToHex("blahblah") + const callValue = bigNumberify(0) + const signature = 'slash(uint256,address)' + const callData = abiEncode(['uint256', 'address'], [slashAmount, targetAddress]) + + await _lib.assertRevert( + governanceContract.submitProposal( + targetContractRegistryKey, + callValue, + signature, + callData, + proposalDescription, + { from: proposerAddress } + ), + "_targetContractRegistryKey must point to valid registered contract" + ) + }) + + it('Submit Proposal for Slash', async () => { + const proposalId = 1 + const proposerAddress = accounts[1] + const slashAmount = 1 + const targetAddress = accounts[2] + const lastBlock = (await _lib.getLatestBlock(web3)).number + const targetContractRegistryKey = delegateManagerKey + const targetContractAddress = delegateManagerContract.address + const callValue = bigNumberify(0) + const signature = 'slash(uint256,address)' + const callData = abiEncode(['uint256', 'address'], [slashAmount, targetAddress]) + + // Call submitProposal + const txReceipt = await governanceContract.submitProposal( + targetContractRegistryKey, + callValue, + signature, + callData, + proposalDescription, + { from: proposerAddress } + ) + + // Confirm event log + const txParsed = _lib.parseTx(txReceipt) + assert.equal(txParsed.event.name, 'ProposalSubmitted', 'Expected same event name') + assert.equal(parseInt(txParsed.event.args.proposalId), proposalId, 'Expected same event.args.proposalId') + assert.equal(txParsed.event.args.proposer, proposerAddress, 'Expected same event.args.proposer') + assert.isTrue(parseInt(txParsed.event.args.startBlockNumber) > lastBlock, 'Expected event.args.startBlockNumber > lastBlock') + assert.equal(txParsed.event.args.description, proposalDescription, "Expected same event.args.description") + + // Call getProposalById() and confirm same values + const proposal = await governanceContract.getProposalById.call(proposalId) + assert.equal(parseInt(proposal.proposalId), proposalId, 'Expected same proposalId') + assert.equal(proposal.proposer, proposerAddress, 'Expected same proposer') + assert.isTrue(parseInt(proposal.startBlockNumber) > lastBlock, 'Expected startBlockNumber > lastBlock') + assert.equal(_lib.toStr(proposal.targetContractRegistryKey), _lib.toStr(targetContractRegistryKey), 'Expected same proposal.targetContractRegistryKey') + assert.equal(proposal.targetContractAddress, targetContractAddress, 'Expected same proposal.targetContractAddress') + assert.equal(fromBn(proposal.callValue), callValue, 'Expected same proposal.callValue') + assert.equal(proposal.signature, signature, 'Expected same proposal.signature') + assert.equal(proposal.callData, callData, 'Expected same proposal.callData') + assert.equal(proposal.outcome, Outcome.InProgress, 'Expected same outcome') + assert.equal(parseInt(proposal.voteMagnitudeYes), 0, 'Expected same voteMagnitudeYes') + assert.equal(parseInt(proposal.voteMagnitudeNo), 0, 'Expected same voteMagnitudeNo') + assert.equal(parseInt(proposal.numVotes), 0, 'Expected same numVotes') + + // Confirm all vote states - all Vote.None + for (const account of accounts) { + const vote = await governanceContract.getVoteByProposalAndVoter.call(proposalId, account) + assert.equal(vote, Vote.None) + } + }) + + it('Vote on Proposal for Slash', async () => { + const proposalId = 1 + const proposerAddress = stakerAccount1 + const slashAmount = 1 + const targetAddress = stakerAccount2 + const voterAddress = stakerAccount1 + const vote = Vote.No + const defaultVote = Vote.None + const lastBlock = (await _lib.getLatestBlock(web3)).number + const targetContractRegistryKey = delegateManagerKey + const targetContractAddress = delegateManagerContract.address + const callValue = bigNumberify(0) + const signature = 'slash(uint256,address)' + const callData = abiEncode(['uint256', 'address'], [slashAmount, targetAddress]) + + // Call submitProposal + await governanceContract.submitProposal( + targetContractRegistryKey, + callValue, + signature, + callData, + proposalDescription, + { from: proposerAddress } + ) + + // Call submitProposalVote() + const txReceipt = await governanceContract.submitProposalVote(proposalId, vote, { from: voterAddress }) + + // Confirm event log + const txParsed = _lib.parseTx(txReceipt) + assert.equal(txParsed.event.name, 'ProposalVoteSubmitted', 'Expected same event name') + assert.equal(parseInt(txParsed.event.args.proposalId), proposalId, 'Expected same event.args.proposalId') + assert.equal(txParsed.event.args.voter, voterAddress, 'Expected same event.args.voter') + assert.equal(parseInt(txParsed.event.args.vote), vote, 'Expected same event.args.vote') + assert.equal((parseInt(txParsed.event.args.voterStake)), fromBn(defaultStakeAmount), 'Expected same event.args.voterStake') + assert.equal(parseInt(txParsed.event.args.previousVote), defaultVote, 'Expected same event.args.previousVote') + + // Call getProposalById() and confirm same values + const proposal = await governanceContract.getProposalById.call(proposalId) + assert.equal(parseInt(proposal.proposalId), proposalId, 'Expected same proposalId') + assert.equal(proposal.proposer, proposerAddress, 'Expected same proposer') + assert.isTrue(parseInt(proposal.startBlockNumber) > lastBlock, 'Expected startBlockNumber > lastBlock') + assert.equal(_lib.toStr(proposal.targetContractRegistryKey), _lib.toStr(targetContractRegistryKey), 'Expected same proposal.targetContractRegistryKey') + assert.equal(proposal.targetContractAddress, targetContractAddress, 'Expected same proposal.targetContractAddress') + assert.equal(fromBn(proposal.callValue), callValue, 'Expected same proposal.callValue') + assert.equal(proposal.signature, signature, 'Expected same proposal.signature') + assert.equal(proposal.callData, callData, 'Expected same proposal.callData') + assert.equal(proposal.outcome, Outcome.InProgress, 'Expected same outcome') + assert.equal(parseInt(proposal.voteMagnitudeYes), 0, 'Expected same voteMagnitudeYes') + assert.equal(parseInt(proposal.voteMagnitudeNo), defaultStakeAmount, 'Expected same voteMagnitudeNo') + assert.equal(parseInt(proposal.numVotes), 1, 'Expected same numVotes') + + // Confirm all vote states - Vote.No for Voter, Vote.None for all others + for (const account of accounts) { + const voterVote = await governanceContract.getVoteByProposalAndVoter.call(proposalId, account) + if (account == voterAddress) { + assert.equal(voterVote, vote) + } else { + assert.equal(voterVote, defaultVote) + } + } + }) + + it('Evaluate successful Proposal + execute Slash', async () => { + const proposalId = 1 + const proposerAddress = stakerAccount1 + const slashAmount = 1 + const targetAddress = stakerAccount2 + const voterAddress = stakerAccount1 + const vote = Vote.Yes + const defaultVote = Vote.None + const lastBlock = (await _lib.getLatestBlock(web3)).number + const targetContractRegistryKey = delegateManagerKey + const targetContractAddress = delegateManagerContract.address + const callValue = bigNumberify(0) + const signature = 'slash(uint256,address)' + const callData = abiEncode(['uint256', 'address'], [slashAmount, targetAddress]) + const outcome = Outcome.Yes + const txHash = keccak256( + abiEncode( + ['address', 'uint256', 'string', 'bytes'], + [targetContractAddress, callValue, signature, callData] + ) + ) + const returnData = null + + // Confirm initial Stake state + const initialTotalStake = parseInt(await stakingContract.totalStaked()) + assert.equal(initialTotalStake, defaultStakeAmount * 2) + const initialStakeAcct2 = parseInt(await stakingContract.totalStakedFor(targetAddress)) + assert.equal(initialStakeAcct2, defaultStakeAmount) + const initialTokenSupply = await tokenContract.totalSupply() + + // Call submitProposal + submitProposalVote + const submitProposalTxReceipt = await governanceContract.submitProposal( + targetContractRegistryKey, + callValue, + signature, + callData, + proposalDescription, + { from: proposerAddress } + ) + await governanceContract.submitProposalVote(proposalId, vote, { from: voterAddress }) + + // Advance blocks to the next valid claim + const proposalStartBlockNumber = parseInt(_lib.parseTx(submitProposalTxReceipt).event.args.startBlockNumber) + await _lib.advanceToTargetBlock(proposalStartBlockNumber + votingPeriod, web3) + + // Call evaluateProposalOutcome() + const evaluateTxReceipt = await governanceContract.evaluateProposalOutcome(proposalId, { from: proposerAddress }) + + // Confirm event logs (2 events) + const [txParsedEvent0, txParsedEvent1] = _lib.parseTx(evaluateTxReceipt, true) + assert.equal(txParsedEvent0.event.name, 'TransactionExecuted', 'Expected same event name') + assert.equal(txParsedEvent0.event.args.txHash, txHash, 'Expected same txParsedEvent0.event.args.txHash') + assert.equal(txParsedEvent0.event.args.targetContractAddress, targetContractAddress, 'Expected same txParsedEvent0.event.args.targetContractAddress') + assert.equal(fromBn(txParsedEvent0.event.args.callValue), callValue, 'Expected same txParsedEvent0.event.args.callValue') + assert.equal(txParsedEvent0.event.args.signature, signature, 'Expected same txParsedEvent0.event.args.signature') + assert.equal(txParsedEvent0.event.args.callData, callData, 'Expected same txParsedEvent0.event.args.callData') + assert.equal(txParsedEvent0.event.args.returnData, returnData, 'Expected same txParsedEvent0.event.args.returnData') + assert.equal(txParsedEvent1.event.name, 'ProposalOutcomeEvaluated', 'Expected same event name') + assert.equal(parseInt(txParsedEvent1.event.args.proposalId), proposalId, 'Expected same event.args.proposalId') + assert.equal(parseInt(txParsedEvent1.event.args.outcome), outcome, 'Expected same event.args.outcome') + assert.equal(parseInt(txParsedEvent1.event.args.voteMagnitudeYes), fromBn(defaultStakeAmount), 'Expected same event.args.voteMagnitudeYes') + assert.equal(parseInt(txParsedEvent1.event.args.voteMagnitudeNo), 0, 'Expected same event.args.voteMagnitudeNo') + assert.equal(parseInt(txParsedEvent1.event.args.numVotes), 1, 'Expected same event.args.numVotes') + + // Call getProposalById() and confirm same values + const proposal = await governanceContract.getProposalById.call(proposalId) + assert.equal(parseInt(proposal.proposalId), proposalId, 'Expected same proposalId') + assert.equal(proposal.proposer, proposerAddress, 'Expected same proposer') + assert.isTrue(parseInt(proposal.startBlockNumber) > lastBlock, 'Expected startBlockNumber > lastBlock') + assert.equal(_lib.toStr(proposal.targetContractRegistryKey), _lib.toStr(targetContractRegistryKey), 'Expected same proposal.targetContractRegistryKey') + assert.equal(proposal.targetContractAddress, targetContractAddress, 'Expected same proposal.targetContractAddress') + assert.equal(fromBn(proposal.callValue), callValue, 'Expected same proposal.callValue') + assert.equal(proposal.signature, signature, 'Expected same proposal.signature') + assert.equal(proposal.callData, callData, 'Expected same proposal.callData') + assert.equal(proposal.outcome, outcome, 'Expected same outcome') + assert.equal(parseInt(proposal.voteMagnitudeYes), defaultStakeAmount, 'Expected same voteMagnitudeYes') + assert.equal(parseInt(proposal.voteMagnitudeNo), 0, 'Expected same voteMagnitudeNo') + assert.equal(parseInt(proposal.numVotes), 1, 'Expected same numVotes') + + // Confirm all vote states - Vote.No for Voter, Vote.None for all others + for (const account of accounts) { + const voterVote = await governanceContract.getVoteByProposalAndVoter.call(proposalId, account) + if (account == voterAddress) { + assert.equal(voterVote, vote) + } else { + assert.equal(voterVote, defaultVote) + } + } + + // Confirm Slash action succeeded by checking new Stake + Token values + const finalStakeAcct2 = parseInt(await stakingContract.totalStakedFor(targetAddress)) + assert.equal(finalStakeAcct2, defaultStakeAmount - slashAmount) + assert.equal( + initialTotalStake, + await stakingContract.totalStaked(), + 'Expected same total stake amount' + ) + assert.equal( + await tokenContract.totalSupply(), + initialTokenSupply - slashAmount, + "Expected same token total supply" + ) + }) + }) + + describe.skip('Upgrade contract', async () => { + // example upgradeProxy.test.js:63 + }) + + describe.skip('Fail to execute proposal after targetContract is upgraded', async () => { + /** TODO */ + }) +}) \ No newline at end of file diff --git a/eth-contracts/test/registry.test.js b/eth-contracts/test/registry.test.js index ce3b446db63..73d1c223158 100644 --- a/eth-contracts/test/registry.test.js +++ b/eth-contracts/test/registry.test.js @@ -13,6 +13,15 @@ contract('Registry', async (accounts) => { registry = await Registry.new() }) + it('Confirm unregistered contract request returns 0 address', async () => { + const contractAddress = await registry.getContract.call(contractName) + assert.equal(parseInt(contractAddress), 0x0, "Expected same contract address") + }) + + it('Should fail to register a non-contract address', async () => { + /** TODO */ + }) + it('Should add newly deployed contract to Registry', async () => { let testContract = await TestContract.new(registry.address) let testContractAddress = testContract.address diff --git a/eth-contracts/test/serviceProvider.test.js b/eth-contracts/test/serviceProvider.test.js index 3555046d10d..c0cc5c996e5 100644 --- a/eth-contracts/test/serviceProvider.test.js +++ b/eth-contracts/test/serviceProvider.test.js @@ -68,9 +68,9 @@ contract('ServiceProvider test', async (accounts) => { token = await AudiusToken.new({ from: treasuryAddress }) tokenAddress = token.address - // console.log(`AudiusToken Address : ${tokenAddress}`) + let initialTokenBal = fromBn(await token.balanceOf(accounts[0])) - // console.log(`AudiusToken Balance: ${initialTokenBal}`) + impl0 = await Staking.new() // Create initialization data diff --git a/eth-contracts/test/staking.test.js b/eth-contracts/test/staking.test.js index 343232be250..1d033fc2123 100644 --- a/eth-contracts/test/staking.test.js +++ b/eth-contracts/test/staking.test.js @@ -67,12 +67,14 @@ contract('Staking test', async (accounts) => { let initializeData = encodeCall( 'initialize', ['address', 'address'], - [token.address, treasuryAddress]) + [token.address, treasuryAddress] + ) await proxy.upgradeToAndCall( impl0.address, initializeData, - { from: proxyOwner }) + { from: proxyOwner } + ) staking = await Staking.at(proxy.address) // Reset min for test purposes @@ -82,6 +84,7 @@ contract('Staking test', async (accounts) => { // Permission test address as caller await staking.setStakingOwnerAddress(testStakingCallerAddress, { from: treasuryAddress }) }) + it('has correct initial state', async () => { assert.equal(await staking.token(), tokenAddress, 'Token is wrong') assert.equal((await staking.totalStaked()).valueOf(), 0, 'Initial total staked amount should be zero') @@ -183,37 +186,40 @@ contract('Staking test', async (accounts) => { 'Final stake amount must be 2x default stake') }) - it('slash functioning as expected', async () => { - // Transfer 1000 tokens to accounts[1], accounts[2] - await token.transfer(accounts[1], DEFAULT_AMOUNT, { from: treasuryAddress }) - await token.transfer(accounts[2], DEFAULT_AMOUNT, { from: treasuryAddress }) + it('slash account', async () => { + const account = accounts[1] + const slashAmount = web3.utils.toBN(DEFAULT_AMOUNT / 2) - // Stake w/both accounts - await approveAndStake(DEFAULT_AMOUNT, accounts[1]) - await approveAndStake(DEFAULT_AMOUNT, accounts[2]) + // Transfer & stake + await token.transfer(account, DEFAULT_AMOUNT, { from: treasuryAddress }) + await approveAndStake(DEFAULT_AMOUNT, account) - let initialStakeBN = await staking.totalStaked() - let initialTotalStake = parseInt(initialStakeBN) - let initialStakeAmount = parseInt(await staking.totalStakedFor(accounts[1])) + // Confirm initial Staking state + const initialStakeBN = await staking.totalStaked() + const tokenInitialSupply = await token.totalSupply() + const initialStakeAmount = parseInt(await staking.totalStakedFor(account)) assert.equal(initialStakeAmount, DEFAULT_AMOUNT) - let slashAmount = web3.utils.toBN(DEFAULT_AMOUNT / 2) - - // Slash 1/2 value from treasury - await slashAccount( - slashAmount, - accounts[1], - treasuryAddress) + // Slash account's stake + await slashAccount(slashAmount, account, treasuryAddress) // Confirm staked value for account - let finalAccountStake = parseInt(await staking.totalStakedFor(accounts[1])) + const finalAccountStake = parseInt(await staking.totalStakedFor(account)) assert.equal(finalAccountStake, DEFAULT_AMOUNT / 2) // Confirm total stake is decreased after slash - let finalTotalStake = await staking.totalStaked() + const finalTotalStake = await staking.totalStaked() assert.isTrue( finalTotalStake.eq(initialStakeBN.sub(slashAmount)), - 'Expect total amount decreased') + 'Expect total amount decreased' + ) + + // Confirm token total supply decreased after burn + assert.equal( + await token.totalSupply(), + tokenInitialSupply - slashAmount, + "ruh roh" + ) }) it('multiple claims, single fund cycle', async () => { From 146e6cc9154a8d216b7910c1c35c3df8ebf98646 Mon Sep 17 00:00:00 2001 From: Hareesh Nagaraj Date: Fri, 10 Apr 2020 15:48:54 -0400 Subject: [PATCH 39/39] [eth-contracts] Address PR comments (#371) * lint * Address PR comments --- .../contracts/service/ClaimFactory.sol | 8 +-- .../contracts/service/DelegateManager.sol | 6 +- .../service/ServiceProviderFactory.sol | 68 +++++++++++-------- eth-contracts/package-lock.json | 1 - eth-contracts/test/delegateManager.test.js | 2 +- 5 files changed, 47 insertions(+), 38 deletions(-) diff --git a/eth-contracts/contracts/service/ClaimFactory.sol b/eth-contracts/contracts/service/ClaimFactory.sol index af461b56cb1..36d117c3eaf 100644 --- a/eth-contracts/contracts/service/ClaimFactory.sol +++ b/eth-contracts/contracts/service/ClaimFactory.sol @@ -123,11 +123,11 @@ contract ClaimFactory is RegistryContract { registry.getContract(serviceProviderFactoryKey) ).getAccountStakeBounds(_claimer); require( - (totalStakedAtFundBlockForClaimer >= spMin), - 'Minimum stake bounds violated at fund block'); + (totalStakedAtFundBlockForClaimer >= spMin), + "Minimum stake bounds violated at fund block"); require( - (totalStakedAtFundBlockForClaimer <= spMax), - 'Maximum stake bounds violated at fund block'); + (totalStakedAtFundBlockForClaimer <= spMax), + "Maximum stake bounds violated at fund block"); // Subtract total locked amount for SP from stake at fund block uint claimerTotalStake = totalStakedAtFundBlockForClaimer - _totalLockedForSP; diff --git a/eth-contracts/contracts/service/DelegateManager.sol b/eth-contracts/contracts/service/DelegateManager.sol index 930ee6b0a07..7a8a8678a05 100644 --- a/eth-contracts/contracts/service/DelegateManager.sol +++ b/eth-contracts/contracts/service/DelegateManager.sol @@ -314,10 +314,10 @@ contract DelegateManager is RegistryContract { registry.getContract(serviceProviderFactoryKey) ); - // Confirm service provider is valid + // Confirm service provider is valid require( - spFactory.isServiceProviderWithinBounds(msg.sender), - 'Service provider must be within bounds'); + spFactory.isServiceProviderWithinBounds(msg.sender), + "Service provider must be within bounds"); // Process claim for msg.sender claimFactory.processClaim(msg.sender, totalLockedForClaimer); diff --git a/eth-contracts/contracts/service/ServiceProviderFactory.sol b/eth-contracts/contracts/service/ServiceProviderFactory.sol index 0c560f74931..edcd49d2174 100644 --- a/eth-contracts/contracts/service/ServiceProviderFactory.sol +++ b/eth-contracts/contracts/service/ServiceProviderFactory.sol @@ -35,9 +35,9 @@ contract ServiceProviderFactory is RegistryContract { mapping(address => ServiceProviderDetails) spDetails; - // Minimum direct staked by service provider + // Minimum staked by service provider account deployer // Static regardless of total number of endpoints for a given account - uint minDirectDeployerStake; + uint minDeployerStake; // END Temporary data structures @@ -115,7 +115,7 @@ contract ServiceProviderFactory is RegistryContract { }); // Configure direct minimum stake for deployer - minDirectDeployerStake = 5 * 10**uint256(DECIMALS); + minDeployerStake = 5 * 10**uint256(DECIMALS); } function register( @@ -151,7 +151,12 @@ contract ServiceProviderFactory is RegistryContract { // Update deployer total spDetails[owner].deployerStake += _stakeAmount; + // Confirm both aggregate account balance and directly staked amount are valid uint currentlyStakedForOwner = this.validateAccountStakeBalance(owner); + validateAccountDeployerStake(owner); + + // Indicate this service provider is within bounds + spDetails[owner].validBounds = true; emit RegisteredServiceProvider( newServiceProviderID, @@ -161,10 +166,6 @@ contract ServiceProviderFactory is RegistryContract { currentlyStakedForOwner ); - // Confirm both aggregate account balance and directly staked amount are valid - this.validateAccountStakeBalance(owner); - validateServiceProviderDirectStake(owner); - return newServiceProviderID; } @@ -217,7 +218,9 @@ contract ServiceProviderFactory is RegistryContract { // Only if unstake operation has not occurred if (!unstaked) { this.validateAccountStakeBalance(owner); - validateServiceProviderDirectStake(owner); + validateAccountDeployerStake(owner); + // Indicate this service provider is within bounds + spDetails[owner].validBounds = true; } return deregisteredID; @@ -241,17 +244,20 @@ contract ServiceProviderFactory is RegistryContract { registry.getContract(stakingProxyOwnerKey) ).totalStakedFor(owner); - emit UpdatedStakeAmount( - owner, - newStakeAmount - ); - // Update deployer total spDetails[owner].deployerStake += _increaseStakeAmount; // Confirm both aggregate account balance and directly staked amount are valid this.validateAccountStakeBalance(owner); - validateServiceProviderDirectStake(owner); + validateAccountDeployerStake(owner); + + // Indicate this service provider is within bounds + spDetails[owner].validBounds = true; + + emit UpdatedStakeAmount( + owner, + newStakeAmount + ); return newStakeAmount; } @@ -284,17 +290,20 @@ contract ServiceProviderFactory is RegistryContract { registry.getContract(stakingProxyOwnerKey) ).totalStakedFor(owner); - emit UpdatedStakeAmount( - owner, - newStakeAmount - ); - // Update deployer total spDetails[owner].deployerStake -= _decreaseStakeAmount; // Confirm both aggregate account balance and directly staked amount are valid this.validateAccountStakeBalance(owner); - validateServiceProviderDirectStake(owner); + validateAccountDeployerStake(owner); + + // Indicate this service provider is within bounds + spDetails[owner].validBounds = true; + + emit UpdatedStakeAmount( + owner, + newStakeAmount + ); return newStakeAmount; } @@ -364,6 +373,9 @@ contract ServiceProviderFactory is RegistryContract { if (totalSPStake < minStake || totalSPStake > maxStake) { // Indicate this service provider is out of bounds spDetails[_serviceProvider].validBounds = false; + } else { + // Indicate this service provider is within bounds + spDetails[_serviceProvider].validBounds = true; } } @@ -442,10 +454,10 @@ contract ServiceProviderFactory is RegistryContract { ).getServiceProviderIdFromEndpoint(_endpoint); } - function getMinDirectDeployerStake() + function getMinDeployerStake() external view returns (uint min) { - return minDirectDeployerStake; + return minDeployerStake; } function getServiceProviderIdsFromAddress(address _ownerAddress, bytes32 _serviceType) @@ -529,7 +541,7 @@ contract ServiceProviderFactory is RegistryContract { /// @notice Validate that the service provider is between the min and max stakes for all their registered services // Permission to 'this' contract or delegate manager function validateAccountStakeBalance(address sp) - external returns (uint stakedForOwner) + external view returns (uint stakedForOwner) { Staking stakingContract = Staking( registry.getContract(stakingProxyOwnerKey) @@ -545,17 +557,15 @@ contract ServiceProviderFactory is RegistryContract { currentlyStakedForOwner <= maxStakeAmount, "Maximum stake amount exceeded"); - // Indicate this service provider is within bounds - spDetails[sp].validBounds = true; - return currentlyStakedForOwner; } - function validateServiceProviderDirectStake(address sp) - internal view returns (uint directStake) + /// @notice Validate that the service provider deployer stake satisfies protocol minimum + function validateAccountDeployerStake(address sp) + internal view returns (uint deployerStake) { require( - spDetails[sp].deployerStake >= minDirectDeployerStake, + spDetails[sp].deployerStake >= minDeployerStake, "Direct stake restriction violated for this service provider"); return spDetails[sp].deployerStake; } diff --git a/eth-contracts/package-lock.json b/eth-contracts/package-lock.json index 6161d10f8ac..f2207c84a37 100644 --- a/eth-contracts/package-lock.json +++ b/eth-contracts/package-lock.json @@ -9092,7 +9092,6 @@ "requires": { "debug": "^2.2.0", "es5-ext": "^0.10.50", - "gulp": "^4.0.2", "nan": "^2.14.0", "typedarray-to-buffer": "^3.1.5", "yaeti": "^0.0.6" diff --git a/eth-contracts/test/delegateManager.test.js b/eth-contracts/test/delegateManager.test.js index ae4ae159b1c..bfc0962ff44 100644 --- a/eth-contracts/test/delegateManager.test.js +++ b/eth-contracts/test/delegateManager.test.js @@ -892,7 +892,7 @@ contract('DelegateManager', async (accounts) => { // Decrease to min let spInfo = await getAccountStakeInfo(stakerAccount, false) - let minDirectStake = await serviceProviderFactory.getMinDirectDeployerStake() + let minDirectStake = await serviceProviderFactory.getMinDeployerStake() let diffToMin = (spInfo.spFactoryStake).sub(minDirectStake) await decreaseRegisteredProviderStake(diffToMin, stakerAccount) let infoAfterDecrease = await getAccountStakeInfo(stakerAccount, false)