diff --git a/contracts/BondingCurve/dividend/RewardsDistributor.sol b/contracts/BondingCurve/dividend/RewardsDistributor.sol new file mode 100644 index 0000000..ece0455 --- /dev/null +++ b/contracts/BondingCurve/dividend/RewardsDistributor.sol @@ -0,0 +1,190 @@ + +pragma solidity ^0.5.6; + +import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol"; + + +/** + * @title RewardsDistributor - Distribute pro rata rewards (dividends) + * @author Bogdan Batog (https://batog.info) + * @dev Distribute pro rata rewards (dividends) to token holders in O(1) time. + * Based on [1] http://batog.info/papers/scalable-reward-distribution.pdf + * And on [2] https://solmaz.io/2019/02/24/scalable-reward-changing/ + */ +contract RewardsDistributor { + using SafeMath for uint256; + + /// @notice ELIGIBLE_UNIT is the smallest eligible unit for reward. Minimum + /// possible distribution is 1 (wei for Ether) PER ELIGIBLE_UNIT. + /// + /// Only multiple of ELIGIBLE_UNIT will be subject to reward + /// distribution. Any fractional part of deposit, smaller than + /// ELIGIBLE_UNIT, won't receive any reward, but it will be tracked. + /// + /// Recommended value 10**(decimals / 2), that is 10**9 for most ERC20. + uint256 public constant ELIGIBLE_UNIT = 10**9; + + /// @notice Stake per address. + mapping(address => uint256) internal _stake; + + /// @notice Stake remainder per address, smaller than ELIGIBLE_UNIT. + mapping(address => uint256) internal _stakeRemainder; + + /// @notice Total staked tokens. In ELIGIBLE_UNIT units. + uint256 internal _stakeTotal; + + /// @notice Total accumulated reward since the beginning of time, in units + /// per ELIGIBLE_UNIT. + uint256 internal _rewardTotal; + + /// @notice Remainder from the last _distribute() call, this amount was not + /// enough to award at least 1 wei to every staked ELIGIBLE_UNIT. At the + /// time of last _distribute() call _rewardRemainder < _stakeTotal. + /// Note that later, _stakeTotal can decrease, but _rewardRemainder will + /// stay unchanged until the next call to _distribute(). + uint256 internal _rewardRemainder; + + /// @notice Proportional rewards awarded *before* this stake was created. + /// See [2] for more details. + mapping(address => int256) _rewardOffset; + + + event DepositMade(address indexed from, uint256 value); + event DistributionMade(address indexed from, uint256 value); + event RewardWithdrawalMade(address indexed to, uint256 value); + event StakeWithdrawalMade(address indexed to, uint256 value); + + + /// Initialize the contract. + constructor() public { + _stakeTotal = 0; + _rewardTotal = 0; + _rewardRemainder = 0; + } + + + /// @notice Deposit funds into contract. + function _deposit(address staker, uint256 tokens) internal returns (bool success) { + + uint256 _tokensToAdd = tokens.add(_stakeRemainder[staker]); + + uint256 _eligibleUnitsToAdd = _tokensToAdd.div(ELIGIBLE_UNIT); + + // update the new remainder for this address + _stakeRemainder[staker] = _tokensToAdd.mod(ELIGIBLE_UNIT); + + // set the current stake for this address + _stake[staker] = _stake[staker].add(_eligibleUnitsToAdd); + + // update total eligible stake units + _stakeTotal = _stakeTotal.add(_eligibleUnitsToAdd); + + // update reward offset + _rewardOffset[staker] += (int256)(_rewardTotal * _eligibleUnitsToAdd); + + emit DepositMade(staker, tokens); + return true; + } + + + /// @notice Distribute tokens pro rata to all stakers. + function _distribute(address from, uint tokens) internal returns (bool success) { + require(tokens > 0); + require(_stakeTotal > 0); + + // add past distribution remainder + uint256 _amountToDistribute = tokens.add(_rewardRemainder); + + // determine rewards per eligible stake + uint256 _ratio = _amountToDistribute.div(_stakeTotal); + + // carry on remainder + _rewardRemainder = _amountToDistribute.mod(_stakeTotal); + + // increase total rewards per stake unit + _rewardTotal = _rewardTotal.add(_ratio); + + emit DistributionMade(from, tokens); + return true; + } + + + /// @notice Withdraw accumulated reward for the staker address. + function _withdrawReward(address staker) internal returns (uint256 tokens) { + + uint256 _reward = getReward(staker); + + // refresh reward offset (so a new call to getReward returns 0) + _rewardOffset[staker] = (int256) (_rewardTotal.mul(_stake[staker])); + + emit RewardWithdrawalMade(staker, _reward); + return _reward; + } + + + /// @notice Withdraw stake for the staker address + function _withdrawStake(address staker, uint256 tokens) internal returns (bool) { + + uint256 _currentStake = getStake(staker); + + require(tokens <= _currentStake); + + // update stake and remainder for this address + uint256 _newStake = _currentStake.sub(tokens); + + _stakeRemainder[staker] = _newStake.mod(ELIGIBLE_UNIT); + + uint256 _eligibleUnitsDelta = _stake[staker].sub( + _newStake.div(ELIGIBLE_UNIT) + ); + + _stake[staker] = _stake[staker].sub(_eligibleUnitsDelta); + + // update total stake + _stakeTotal = _stakeTotal.sub(_eligibleUnitsDelta); + + // update reward offset + _rewardOffset[staker] -= (int256) (_rewardTotal.mul(_eligibleUnitsDelta)); + + emit StakeWithdrawalMade(staker, tokens); + return true; + } + + /// + /// READ ONLY + /// + + + /// @notice Read total stake. + function getStakeTotal() public returns (uint256) { + return _stakeTotal.mul(ELIGIBLE_UNIT); + } + + + /// @notice Read current stake for address. + function getStake(address staker) public view returns (uint256 tokens) { + tokens = ( + _stake[staker].mul(ELIGIBLE_UNIT) + ).add( + _stakeRemainder[staker] + ); + + return tokens; + } + + + /// @notice Read current accumulated reward for address. + function getReward(address staker) public view returns (uint256 tokens) { + int256 _tokens = ( + (int256)( + _stake[staker].mul(_rewardTotal) + ) - _rewardOffset[staker] + ); + + tokens = (uint256) (_tokens); + + return tokens; + } + +} + diff --git a/contracts/BondingCurve/dividend/RewardsDistributorWrapper.sol b/contracts/BondingCurve/dividend/RewardsDistributorWrapper.sol new file mode 100644 index 0000000..60b1b41 --- /dev/null +++ b/contracts/BondingCurve/dividend/RewardsDistributorWrapper.sol @@ -0,0 +1,39 @@ +pragma solidity ^0.5.6; + +import "./RewardsDistributor.sol"; + + +/// @title RewardsDistributorWrapper +/// @author Bogdan Batog (https://batog.info) +/// @dev ONLY FOR TESTING. DO NOT DEPLOY THIS!!!!! +contract RewardsDistributorWrapper is RewardsDistributor { + + /// @notice Deposit funds into contract. + function deposit(address staker, uint tokens) public returns (bool success) { + return _deposit(staker, tokens); + } + + /// @notice Distribute tokens pro rata to all stakers. + function distribute(uint tokens) public returns (bool success) { + return _distribute(address(0), tokens); + } + + /// @notice Withdraw accumulated reward for the staker address. + function withdrawReward(address staker) public returns (uint tokens) { + return _withdrawReward(staker); + } + + /// @notice Withdraw all stake for the staker address. + function withdrawAllStake(address staker) public returns (uint tokens) { + tokens = getStake(staker); + _withdrawStake(staker, tokens); + + return tokens; + } + + /// @notice Withdraw stake for the staker address. + function withdrawStake(address staker, uint tokens) public returns (bool success) { + return _withdrawStake(staker, tokens); + } +} + diff --git a/index.js b/index.js index c47f620..107bcb7 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,7 @@ const DividendPool = Contracts.getFromLocal('DividendPool'); const BondingCurve = Contracts.getFromLocal('BondingCurve'); const BondingCurveFactory = Contracts.getFromLocal('BondingCurveFactory'); const BondedToken = Contracts.getFromLocal('BondedToken'); +const RewardsDistributorWrapper = Contracts.getFromLocal('RewardsDistributorWrapper'); const CONTRACT_ABIS = { BondingCurve, @@ -20,7 +21,8 @@ const CONTRACT_ABIS = { StaticCurveLogic, BondedToken, DividendPool, - BancorCurveService + BancorCurveService, + RewardsDistributorWrapper }; const CONTRACT_NAMES = { @@ -30,7 +32,8 @@ const CONTRACT_NAMES = { StaticCurveLogic: 'StaticCurveLogic', BondedToken: 'BondedToken', DividendPool: 'DividendPool', - BancorCurveService: 'BancorCurveService' + BancorCurveService: 'BancorCurveService', + RewardsDistributorWrapper: 'RewardsDistributorWrapper' }; const PACKAGE_NAMES = { @@ -55,6 +58,7 @@ async function setupApp(txParams) { await appProject.setImplementation(BondedToken, CONTRACT_NAMES.BondedToken); await appProject.setImplementation(DividendPool, CONTRACT_NAMES.DividendPool); await appProject.setImplementation(BancorCurveService, CONTRACT_NAMES.BancorCurveService); + await appProject.setImplementation(RewardsDistributor, CONTRACT_NAMES.RewardsDistributor); return appProject; } @@ -166,6 +170,15 @@ async function deployStandaloneERC20(myProject, initArgs) { // }); } +async function deployRewardsDistributorWrapper(myProject, initArgs) { + ZWeb3.initialize(web3.currentProvider); + + const instance = await myProject.createProxy(RewardsDistributorWrapper, { + initArgs + }); + return instance; +} + async function getImplementation(project, contractName) { const directory = await project.getCurrentDirectory(); const implementation = await directory.getImplementation(contractName); @@ -203,6 +216,7 @@ module.exports = { deployBondingCurveFactory, deployBondedToken, deployStandaloneERC20, + deployRewardsDistributorWrapper, CONTRACT_NAMES, CONTRACT_ABIS, getImplementation, diff --git a/test/unit/rewardDistributor.spec.ts b/test/unit/rewardDistributor.spec.ts new file mode 100644 index 0000000..5a01b88 --- /dev/null +++ b/test/unit/rewardDistributor.spec.ts @@ -0,0 +1,396 @@ +// Import all required modules from openzeppelin-test-helpers +const {BN, constants, expectRevert} = require('openzeppelin-test-helpers'); + +// Import preferred chai flavor: both expect and should are supported +const expect = require('chai').expect; +const should = require('chai').should(); +const expectEvent = require('../expectEvent'); + +require('../setup'); + +const {deployProject, deployRewardsDistributorWrapper} = require('../../index.js'); + +var TEN18 = new BN(String(10 ** 18)); + +var PPB = new BN(String(10 ** 9)); + +contract('RewardsDistributorWrapper', accounts => { + let project; + let rd; + let tx; + let ELIGIBLE_UNIT; + + let acct_a = accounts[1]; + let acct_b = accounts[2]; + let acct_c = accounts[3]; + let acct_d = accounts[4]; + + beforeEach(async function() { + project = await deployProject(); + rd = await deployRewardsDistributorWrapper(project); + ELIGIBLE_UNIT = rd.ELIGIBLE_UNIT; + }); + + it('deploys and initializes', async function() { + expect( + await rd.methods.getStakeTotal().call({from: acct_a}) + ).to.be.equal('0'); + }); + + it("withdraws ZERO reward", async function() { + tx = await rd.methods.withdrawReward(acct_a).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'RewardWithdrawalMade', { + to: acct_a, + value: new BN('0') + }); + }); + + it("reads ZERO stake", async function() { + expect( + await rd.methods.getStake(acct_a).call({from: acct_a}) + ).to.be.equal('0'); + }); + + it("reverts if trying to withdraw amount > stake", async function() { + await expectRevert.unspecified( + rd.methods.withdrawStake(acct_a, '1').send({from: acct_a}) + ); + }); + + it('updates total stake after deposit > ELIGIBLE_UNIT', async function() { + var amount = new BN('100').mul(TEN18); + + tx = await rd.methods + .deposit(acct_a, amount.toString()) + .send({from: acct_a}); + + expectEvent.inLogs(tx.events, 'DepositMade', { + from: acct_a, + value: amount + }); + + expect( + await rd.methods.getStakeTotal().call({from: acct_a}) + ).to.be.equal(amount.toString()); + }); + + it('doesn\'t update total stake after deposit < ELIGIBLE_UNIT', async function() { + var amount = new BN('100'); + + tx = await rd.methods + .deposit(acct_a, amount.toString()) + .send({from: acct_a}); + + expect( + await rd.methods.getStakeTotal().call({from: acct_a}) + ).to.be.equal('0'); + }); + + it('deposits A, gets stake, withdraws all stake, gets stake again', async function() { + var amount = new BN('100').mul(TEN18); + + tx = await rd.methods + .deposit(acct_a, amount.toString()) + .send({from: acct_a}); + + expect( + await rd.methods.getStake(acct_a).call({from: acct_a}) + ).to.be.equal(amount.toString()); + + await rd.methods.withdrawAllStake(acct_a).send({from: acct_a}); + + expect( + await rd.methods.getStake(acct_a).call({from: acct_a}) + ).to.be.equal('0'); + }); + + it("does no distribution if no stake >= ELIGIBLE_UNIT", async function() { + await rd.methods.deposit(acct_a, '1234').send({from: acct_a}); + + await expectRevert.unspecified( + rd.methods.distribute('1000').send({from: acct_a}), + 'no deposit greater than 1 ELIGIBLE_UNIT' + ); + }); + + it("does no distribution if stake becomes ineligible after withdrawl", async function() { + var amountDeposit = new BN('100').mul(TEN18); + var amountWithdraw = new BN('100').mul(TEN18).sub(new BN('10000')); + + await rd.methods.deposit(acct_a, amountDeposit.toString()).send({from: acct_a}); + + tx = await rd.methods.withdrawStake(acct_a, amountWithdraw.toString()).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'StakeWithdrawalMade', { + to: acct_b, + value: amountWithdraw + }); + + await expectRevert.unspecified( + rd.methods.distribute('1000').send({from: acct_a}), + 'no deposit greater than 1 ELIGIBLE_UNIT' + ); + + // stake of A is 10000 but because it is < ELIGIBLE_UNIT total stake + // should be zero + expect( + await rd.methods.getStake(acct_a).call({from: acct_a}) + ).to.be.equal('10000'); + + expect( + await rd.methods.getStakeTotal().call({from: acct_a}) + ).to.be.equal('0'); + }); + + it('allocates all reward to a single staker and allow its withdrawl', async function() { + var amountDeposit = new BN('100').mul(TEN18); + var amountDistribute = new BN('200').mul(TEN18); + + await rd.methods.deposit(acct_a, amountDeposit.toString()).send({from: acct_a}); + + tx = await rd.methods.distribute(amountDistribute.toString()).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'DistributionMade', { + from: 0, + value: amountDistribute + }); + + expect ( + await rd.methods.getReward(acct_a).call({from: acct_a}) + ).to.be.equal(amountDistribute.toString()) + + tx = await rd.methods.withdrawReward(acct_a).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'RewardWithdrawalMade', { + to: acct_a, + value: amountDistribute + }); + + expect ( + await rd.methods.getReward(acct_a).call({from: acct_a}) + ).to.be.equal('0') + }); + + it("allocates no reward to stake <= ELIGIBLE_UNIT", async function() { + await rd.methods.deposit(acct_a, String(10 ** 9)).send({from: acct_a}); + await rd.methods.deposit(acct_b, String(100)).send({from: acct_a}); + + var distribute1 = new BN('100').mul(TEN18); + await rd.methods.distribute(distribute1.toString()).send({from: acct_a}); + + // all reward goes to A + expect ( + await rd.methods.getReward(acct_a).call({from: acct_a}) + ).to.be.equal(distribute1.toString()) + + expect ( + await rd.methods.getReward(acct_b).call({from: acct_a}) + ).to.be.equal('0') + }); + + it("allocates 1st reward proportionally to 2 stakers and 2nd reward to remaining staker after the other withdrew", async function() { + var depositA = new BN(String(10 * 10 ** 9)); // 10 ELIGIBLE_UNITS + var depositB = new BN(String(30 * 10 ** 9)); + var distribute1 = new BN('400'); // notice this is in wei + var distribute2 = new BN('900'); + + await rd.methods.deposit(acct_a, depositA.toString()).send({from: acct_a}); + await rd.methods.deposit(acct_b, depositB.toString()).send({from: acct_a}); + await rd.methods.distribute(distribute1.toString()).send({from: acct_a}); + + // second distribution after A has withdrawn entirely + await rd.methods.withdrawAllStake(acct_a).send({from: acct_a}); + await rd.methods.distribute(distribute2.toString()).send({from: acct_a}); + + expect ( + await rd.methods.getReward(acct_b).call({from: acct_a}) + ).to.be.equal('1200') + + expect ( + await rd.methods.getReward(acct_a).call({from: acct_a}) + ).to.be.equal('100') + }); + + it("withdraws reward after proportional reward distribution", async function() { + var depositA = new BN('100').mul(TEN18); + var depositB = new BN('300').mul(TEN18); + var distribute1 = new BN('40').mul(TEN18); + + await rd.methods.deposit(acct_a, depositA.toString()).send({from: acct_a}); + await rd.methods.deposit(acct_b, depositB.toString()).send({from: acct_a}); + await rd.methods.distribute(distribute1.toString()).send({from: acct_a}); + + tx = await rd.methods.withdrawReward(acct_b).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'RewardWithdrawalMade', { + to: acct_b, + value: new BN('30').mul(TEN18) + }); + }); + + it("withdraws reward after two consecutive reward distributions", async function() { + var depositA = new BN('100').mul(TEN18); + var depositB = new BN('300').mul(TEN18); + var distribute1 = new BN('400').mul(TEN18); + + await rd.methods.deposit(acct_a, depositA.toString()).send({from: acct_a}); + await rd.methods.deposit(acct_b, depositB.toString()).send({from: acct_a}); + await rd.methods.distribute(distribute1.toString()).send({from: acct_a}); + + var distribute2 = new BN('4000').mul(TEN18); + await rd.methods.distribute(distribute2.toString()).send({from: acct_a}); + + tx = await rd.methods.withdrawReward(acct_a).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'RewardWithdrawalMade', { + to: acct_a, + value: new BN('1100').mul(TEN18) + }); + + tx = await rd.methods.withdrawReward(acct_a).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'RewardWithdrawalMade', { + to: acct_a, + value: new BN('0') + }); + + tx = await rd.methods.withdrawReward(acct_b).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'RewardWithdrawalMade', { + to: acct_b, + value: new BN('3300').mul(TEN18) + }); + }); + + it("distributes after partial stake withdrawal and reads reward", async function() { + var depositA = new BN('100').mul(TEN18); + var depositB = new BN('300').mul(TEN18); + var distribute1 = new BN('100').mul(TEN18); + var withdrawB = new BN('200').mul(TEN18); + var distribute2 = new BN('100').mul(TEN18); + + await rd.methods.deposit(acct_a, depositA.toString()).send({from: acct_a}); + await rd.methods.deposit(acct_b, depositB.toString()).send({from: acct_a}); + await rd.methods.distribute(distribute1.toString()).send({from: acct_a}); + + tx = await rd.methods.withdrawStake(acct_b, withdrawB.toString()).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'StakeWithdrawalMade', { + to: acct_b, + value: withdrawB + }); + + await rd.methods.distribute(distribute2.toString()).send({from: acct_a}); + + expect( + await rd.methods.getStakeTotal().call({from: acct_a}) + ).to.be.equal(String(200 * 10 ** 18)); + + expect ( + await rd.methods.getReward(acct_a).call({from: acct_a}) + ).to.be.equal(String(75 * 10 ** 18)); + + expect ( + await rd.methods.getReward(acct_b).call({from: acct_a}) + ).to.be.equal(String(125 * 10 ** 18)); + }); + + it("withdraws reward after stake has been withdrawn", async function() { + var depositA = new BN('100').mul(TEN18); + var distribute1 = new BN('10').mul(TEN18); + + await rd.methods.deposit(acct_a, depositA.toString()).send({from: acct_a}); + await rd.methods.distribute(distribute1.toString()).send({from: acct_a}); + + tx = await rd.methods.withdrawStake(acct_a, depositA.toString()).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'StakeWithdrawalMade', { + to: acct_b, + value: depositA + }); + + tx = await rd.methods.withdrawReward(acct_a).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'RewardWithdrawalMade', { + to: acct_a, + value: distribute1 + }); + + expect( + await rd.methods.getStakeTotal().call({from: acct_a}) + ).to.be.equal('0'); + }); + + it("handles magnitude: A deposits 9999, B deposits 1, distribute, withdraw stake, distribute, withdraw reward", async function() { + var depositA = new BN('9999').mul(TEN18); + var depositB = new BN('1').mul(TEN18); + var distribute1 = new BN('1').mul(TEN18); + var distribute2 = new BN('2').mul(TEN18); + + await rd.methods.deposit(acct_a, depositA.toString()).send({from: acct_a}); + await rd.methods.deposit(acct_b, depositB.toString()).send({from: acct_a}); + + await rd.methods.distribute(distribute1.toString()).send({from: acct_a}); + await rd.methods.withdrawAllStake(acct_a).send({from: acct_a}); + await rd.methods.distribute(distribute2.toString()).send({from: acct_a}); + + expect ( + await rd.methods.getReward(acct_b).call({from: acct_a}) + ).to.be.equal('2000100000000000000'); + }); + + it("handles magnitude: deposit 10**6 10**9 10**12 10**15, distribute, withdraw reward", async function() { + await rd.methods.deposit(acct_a, String(10 ** 6)).send({from: acct_a}); + await rd.methods.deposit(acct_b, String(10 ** 9)).send({from: acct_a}); + await rd.methods.deposit(acct_c, String(10 ** 12)).send({from: acct_a}); + // 10**6 is NOT substracted so B + C + D stakes sum up to 10 ** 15 + await rd.methods.deposit(acct_d, String(10 ** 15 - 10 ** 12 - 10 ** 9)).send({from: acct_a}); + + await rd.methods.distribute(String(10 ** 9)).send({from: acct_a}); + + // A gets no reward because its stake is below ELIGIBLE_UNIT + expect ( + await rd.methods.getReward(acct_a).call({from: acct_a}) + ).to.be.equal('0'); + + expect ( + await rd.methods.getReward(acct_b).call({from: acct_a}) + ).to.be.equal('1000'); + + expect ( + await rd.methods.getReward(acct_c).call({from: acct_a}) + ).to.be.equal('1000000'); + + expect ( + await rd.methods.getReward(acct_d).call({from: acct_a}) + ).to.be.equal(String(10 ** 9 - 10 ** 6 - 10 ** 3)); + }); + + it("carries remainder to second distribution and withdraws reward", async function() { + await rd.methods.deposit(acct_a, String(10 ** 9)).send({from: acct_a}); + await rd.methods.deposit(acct_b, String(9 * 10 ** 9)).send({from: acct_a}); + + // 19 wei can not be divided to 10 ELIGIBLE_UNITS; So only 10 wei + // will be distributed and 9 will be stored as remainder and + // added to the next distribution + await rd.methods.distribute(String(19)).send({from: acct_a}); + + tx = await rd.methods.withdrawReward(acct_a).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'RewardWithdrawalMade', { + to: acct_a, + value: 1 + }); + + tx = await rd.methods.withdrawReward(acct_b).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'RewardWithdrawalMade', { + to: acct_b, + value: 9 + }); + + // 9 wei remainder + 1 new wei can now be divided to 10 EILIGIBLE_UNITS + await rd.methods.distribute(String(1)).send({from: acct_a}); + + tx = await rd.methods.withdrawReward(acct_a).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'RewardWithdrawalMade', { + to: acct_a, + value: 1 + }); + + tx = await rd.methods.withdrawReward(acct_b).send({from: acct_a}); + expectEvent.inLogs(tx.events, 'RewardWithdrawalMade', { + to: acct_b, + value: 9 + }); + }); + +});