diff --git a/contracts/solidity/contracts/StakeDelegatable.sol b/contracts/solidity/contracts/StakeDelegatable.sol new file mode 100644 index 0000000000..c217e7d68e --- /dev/null +++ b/contracts/solidity/contracts/StakeDelegatable.sol @@ -0,0 +1,127 @@ +pragma solidity ^0.4.24; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; + + +/** + * @title Stake Delegatable + * @dev A contract that allows a staker to delegate their staked balance + * to any address that is not already a staker. Delegator refers to a + * staker who has delegated their stake to another address, while delegate + * refers to an address that has had a stake delegated to it. + */ +contract StakeDelegatable { + + /** + * @dev Only not null addresses can be passed to the functions with this modifier. + */ + modifier notNull(address _address) { + require(_address != address(0), "Provided address can not be zero."); + _; + } + + /** + * @dev Only non staker addresses can be passed to the functions with this modifier. + */ + modifier notStaker(address _address) { + require(stakeBalances[_address] <= 0, "Provided address is not a staker."); + _; + } + + mapping(address => uint256) public stakeBalances; + mapping(address => address) public delegatorToDelegate; + mapping(address => address) public delegateToDelegator; + + /** + * @dev Gets the staked balance of the specified address. + * @param _address The address to query the balance of. + * @return The amount staked by the passed address. + */ + function stakedBalanceOf(address _address) + public + view + notNull(_address) + returns (uint256) + { + // If provided address is a delegate return its delegator balance. + address delegator = delegatorToDelegate[_address]; + if (delegator != address(0) && delegateToDelegator[delegator] == _address) { + return stakeBalances[delegator]; + } + + // If provided address is a delegator return zero balance since the + // balance is delegated to a delegate. + address delegate = delegateToDelegator[_address]; + if (delegate != address(0) && delegatorToDelegate[delegate] == _address) { + return 0; + } + + return stakeBalances[_address]; + } + + /** + * @dev Returns address of a delegate if it exists for the + * provided address or the provided address otherwise. + * @param _address The address to check. + * @return Delegate address or provided address. + */ + function getDelegatorOrDelegate(address _address) + public + view + notNull(_address) + returns (address) + { + address delegate = delegateToDelegator[_address]; + if (delegate != address(0) && delegatorToDelegate[delegate] == _address) { + return delegate; + } + return _address; + } + + /** + * @dev Approves address to delegate your stake balance. You can only + * have one delegate address. Delegate must also request to operate + * your stake by calling requestDelegateFor() method. + * @param _address Address to where you want to delegate your balance. + */ + function approveDelegateAt(address _address) + public + notNull(_address) + notStaker(_address) + { + delegateToDelegator[msg.sender] = _address; + } + + /** + * @dev Requests to delegate stake for a specified address. + * An address must approve you first to delegate by calling + * requestDelegateFor() method. + * @param _address Address for which you request to delegate. + */ + function requestDelegateFor(address _address) + public + notNull(_address) + { + delegatorToDelegate[msg.sender] = _address; + } + + /** + * @dev Removes delegate for your stake balance. + */ + function removeDelegate() public { + address delegate = delegateToDelegator[msg.sender]; + delete delegatorToDelegate[delegate]; + delete delegateToDelegator[msg.sender]; + } + + /** + * @dev Revert if a delegate tries to stake. + * @param _address The address to check. + */ + function revertIfDelegateStakes(address _address) internal { + address delegator = delegatorToDelegate[_address]; + if (delegator != address(0)) { + revert("Provided address can not stake since it has stake delegated to it."); + } + } +} diff --git a/contracts/solidity/contracts/StakingProxy.sol b/contracts/solidity/contracts/StakingProxy.sol index 0f68513ecb..65f8f7930b 100644 --- a/contracts/solidity/contracts/StakingProxy.sol +++ b/contracts/solidity/contracts/StakingProxy.sol @@ -4,7 +4,7 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./utils/AddressArrayUtils.sol"; interface authorizedStakingContract { - function stakeBalanceOf(address addr) external view returns (uint256); + function stakedBalanceOf(address addr) external view returns (uint256); } @@ -48,7 +48,7 @@ contract StakingProxy is Ownable { require(_staker != address(0), "Staker address can't be zero."); uint256 balance = 0; for (uint i = 0; i < authorizedContracts.length; i++) { - balance = balance + authorizedStakingContract(authorizedContracts[i]).stakeBalanceOf(_staker); + balance = balance + authorizedStakingContract(authorizedContracts[i]).stakedBalanceOf(_staker); } return balance; } diff --git a/contracts/solidity/contracts/TokenGrant.sol b/contracts/solidity/contracts/TokenGrant.sol index 0095dbb14c..25d6d6345f 100644 --- a/contracts/solidity/contracts/TokenGrant.sol +++ b/contracts/solidity/contracts/TokenGrant.sol @@ -4,6 +4,7 @@ import "openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; import "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "./StakingProxy.sol"; +import "./StakeDelegatable.sol"; /** @@ -14,7 +15,7 @@ import "./StakingProxy.sol"; * released gradually based on the vesting schedule cliff and vesting duration. * Optionally grant can be revoked by the token grant creator. */ -contract TokenGrant { +contract TokenGrant is StakeDelegatable { using SafeMath for uint256; using SafeERC20 for StandardToken; @@ -54,9 +55,6 @@ contract TokenGrant { // available to be released to the beneficiary mapping(address => uint256) public balances; - // Token grants stake balances. - mapping(address => uint256) public stakeBalances; - // Token grants stake withdrawals. mapping(uint256 => uint256) public stakeWithdrawalStart; @@ -82,15 +80,6 @@ contract TokenGrant { return balances[_owner]; } - /** - * @dev Gets the grants stake balance of the specified address. - * @param _owner The address to query the grants balance of. - * @return An uint256 representing the grants stake balance owned by the passed address. - */ - function stakeBalanceOf(address _owner) public view returns (uint256 balance) { - return stakeBalances[_owner]; - } - /** * @dev Gets grant by ID. Returns only basic grant data. * If you need vesting schedule for the grant you must call `getGrantVestingSchedule()` @@ -240,6 +229,8 @@ contract TokenGrant { uint256 available = grants[_id].amount.sub(grants[_id].released); require(available > 0, "Must have available granted amount to stake."); + revertIfDelegateStakes(msg.sender); + // Lock grant from releasing its balance. grants[_id].locked = true; diff --git a/contracts/solidity/contracts/TokenStaking.sol b/contracts/solidity/contracts/TokenStaking.sol index 70615a289d..876bf8aa2f 100644 --- a/contracts/solidity/contracts/TokenStaking.sol +++ b/contracts/solidity/contracts/TokenStaking.sol @@ -5,6 +5,7 @@ import "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "./StakingProxy.sol"; import "./utils/UintArrayUtils.sol"; +import "./StakeDelegatable.sol"; /** @@ -13,7 +14,7 @@ import "./utils/UintArrayUtils.sol"; * A holder of the specified token can stake its tokens to this contract * and unstake after withdrawal delay is over. */ -contract TokenStaking { +contract TokenStaking is StakeDelegatable { using SafeMath for uint256; using SafeERC20 for StandardToken; using UintArrayUtils for uint256[]; @@ -35,7 +36,6 @@ contract TokenStaking { uint256 public withdrawalDelay; uint256 public numWithdrawals; - mapping(address => uint256) public balances; mapping(address => uint256[]) public withdrawalIndices; mapping(uint256 => Withdrawal) public withdrawals; @@ -67,14 +67,19 @@ contract TokenStaking { require(StandardToken(_token) == token, "Token contract must be the same one linked to this contract."); require(_value <= token.balanceOf(_from), "Sender must have enough tokens."); + revertIfDelegateStakes(_from); + // Transfer tokens to this contract. token.transferFrom(_from, this, _value); // Maintain a record of the stake amount by the sender. - balances[_from] = balances[_from].add(_value); - emit Staked(_from, _value); + stakeBalances[_from] = stakeBalances[_from].add(_value); + + // Emit staked event. Check if staker works via operator first. + address delegatorOrDelegate = getDelegatorOrDelegate(_from); + emit Staked(delegatorOrDelegate, _value); if (address(stakingProxy) != address(0)) { - stakingProxy.emitStakedEvent(_from, _value); + stakingProxy.emitStakedEvent(delegatorOrDelegate, _value); } } @@ -86,16 +91,19 @@ contract TokenStaking { */ function initiateUnstake(uint256 _value) public returns (uint256 id) { - require(_value <= balances[msg.sender], "Staker must have enough tokens to unstake."); + require(_value <= stakeBalances[msg.sender], "Staker must have enough tokens to unstake."); - balances[msg.sender] = balances[msg.sender].sub(_value); + stakeBalances[msg.sender] = stakeBalances[msg.sender].sub(_value); id = numWithdrawals++; withdrawals[id] = Withdrawal(msg.sender, _value, now); withdrawalIndices[msg.sender].push(id); emit InitiatedUnstake(id); + + // Emit unstaked event. Check if staker delegated its balance first. + address delegatorOrDelegate = getDelegatorOrDelegate(msg.sender); if (address(stakingProxy) != address(0)) { - stakingProxy.emitUnstakedEvent(msg.sender, _value); + stakingProxy.emitUnstakedEvent(delegatorOrDelegate, _value); } return id; } @@ -123,15 +131,6 @@ contract TokenStaking { emit FinishedUnstake(_id); } - /** - * @dev Gets the stake balance of the specified address. - * @param _staker The address to query the balance of. - * @return An uint256 representing the amount owned by the passed address. - */ - function stakeBalanceOf(address _staker) public view returns (uint256 balance) { - return balances[_staker]; - } - /** * @dev Gets withdrawal request by ID. * @param _id ID of withdrawal request. diff --git a/contracts/solidity/test/TestStake.sol b/contracts/solidity/test/TestStake.sol index b93fe37dda..2aaa17b51c 100644 --- a/contracts/solidity/test/TestStake.sol +++ b/contracts/solidity/test/TestStake.sol @@ -28,7 +28,7 @@ contract TestStake { token.approveAndCall(address(stakingContract), 100, ""); Assert.equal(token.balanceOf(address(this)), balance - 100, "Stake amount should be taken out from token holder's main balance."); - Assert.equal(stakingContract.stakeBalanceOf(address(this)), 100, "Stake amount should be added to token holder's stake balance."); + Assert.equal(stakingContract.stakedBalanceOf(address(this)), 100, "Stake amount should be added to token holder's stake balance."); } // Token holder should be able to initiate unstake of it's tokens @@ -46,7 +46,7 @@ contract TestStake { Assert.equal(amount, 100, "Withdrawal request should maintain a record of the amount."); Assert.equal(start, now, "Withdrawal request should maintain a record of when it was initiated."); - Assert.equal(stakingContract.stakeBalanceOf(address(this)), 0, "Unstake amount should be taken out from token holder's stake balance."); + Assert.equal(stakingContract.stakedBalanceOf(address(this)), 0, "Unstake amount should be taken out from token holder's stake balance."); Assert.equal(token.balanceOf(address(this)), balance, "Unstake amount should not be added to token holder main balance."); } @@ -63,7 +63,7 @@ contract TestStake { // r will be false if it threw and true if it didn't. bool r = throwProxy.execute.gas(200000)(); Assert.isFalse(r, "Should throw when trying to unstake when withdrawal delay is not over."); - Assert.equal(stakingContract.stakeBalanceOf(address(this)), 0, "Stake balance should stay unchanged."); + Assert.equal(stakingContract.stakedBalanceOf(address(this)), 0, "Stake balance should stay unchanged."); } // Token holder should not be able to stake without providing correct stakingContract address. diff --git a/contracts/solidity/test/TestStakeDelegate.js b/contracts/solidity/test/TestStakeDelegate.js new file mode 100644 index 0000000000..68ff5fa08b --- /dev/null +++ b/contracts/solidity/test/TestStakeDelegate.js @@ -0,0 +1,95 @@ +import increaseTime, { duration, increaseTimeTo } from './helpers/increaseTime'; +import exceptThrow from './helpers/expectThrow'; +const KeepToken = artifacts.require('./KeepToken.sol'); +const StakingProxy = artifacts.require('./StakingProxy.sol'); +const TokenStaking = artifacts.require('./TokenStaking.sol'); + + +contract('TestStakeDelegate', function(accounts) { + + let token, stakingProxy, stakingContract, + account_one = accounts[0], + account_two = accounts[1], + account_three = accounts[2], + account_four = accounts[3]; + + beforeEach(async () => { + token = await KeepToken.new(); + stakingProxy = await StakingProxy.new(); + stakingContract = await TokenStaking.new(token.address, stakingProxy.address, duration.days(30)); + await stakingProxy.authorizeContract(stakingContract.address, {from: account_one}) + + // Stake tokens as account one + await token.approveAndCall(stakingContract.address, 200, "", {from: account_one}); + + // Send tokens to the accounts + await token.transfer(account_two, 200, {from: account_one}); + await token.transfer(account_three, 500, {from: account_one}); + + // Stake tokens as account two + await token.approveAndCall(stakingContract.address, 200, "", {from: account_two}); + }); + + it("should not be able to delegate stake to a delegate address that is a staker", async function() { + await stakingContract.requestDelegateFor(account_one, {from: account_two}); + await exceptThrow(stakingContract.approveDelegateAt(account_two)); + }); + + it("should be able to delegate stake to a delegate address to represent your stake balance", async function() { + await stakingContract.requestDelegateFor(account_one, {from: account_three}); + await stakingContract.approveDelegateAt(account_three, {from: account_one}); + assert.equal(await stakingProxy.balanceOf(account_three), 200, "Delegate account should represent delegator's stake balance."); + assert.equal(await stakingProxy.balanceOf(account_one), 0, "Delegator account stake balance should become zero."); + }); + + it("should not be able to delegate stake to a delegate address that is not approved", async function() { + await stakingContract.requestDelegateFor(account_two, {from: account_three}); + await stakingContract.approveDelegateAt(account_three, {from: account_one}); + assert.equal(await stakingProxy.balanceOf(account_three), 0, "Delegate account should be zero since there were no handshake with delegator."); + }); + + it("should be able to update delegate address if new delegate request exist", async function() { + await stakingContract.requestDelegateFor(account_one, {from: account_three}); + await stakingContract.approveDelegateAt(account_three, {from: account_one}); + assert.equal(await stakingProxy.balanceOf(account_three), 200, "Delegate account should represent delegator's stake balance."); + assert.equal(await stakingProxy.balanceOf(account_one), 0, "Delegator account stake balance should become zero."); + + await stakingContract.approveDelegateAt(account_four, {from: account_one}); + await stakingContract.requestDelegateFor(account_one, {from: account_four}); + assert.equal(await stakingProxy.balanceOf(account_three), 0, "Previous delegate account should stop representing delegator's stake balance."); + assert.equal(await stakingProxy.balanceOf(account_four), 200, "Updated delegate account should represent delegator's stake balance."); + }); + + it("should be able to remove delegated address that represents your stake balance", async function() { + await stakingContract.requestDelegateFor(account_one, {from: account_three}); + await stakingContract.approveDelegateAt(account_three, {from: account_one}); + assert.equal(await stakingProxy.balanceOf(account_three), 200, "Delegate account should represent delegator's stake balance."); + await stakingContract.removeDelegate(); + assert.equal(await stakingProxy.balanceOf(account_three), 0, "Delegate account should stop representing delegator's stake balance."); + assert.equal(await stakingProxy.balanceOf(account_one), 200, "Delegator account should get its balance back."); + }); + + it("should be able to change stake and get delegate to reflect updated balance", async function() { + await stakingContract.requestDelegateFor(account_one, {from: account_three}); + await stakingContract.approveDelegateAt(account_three, {from: account_one}); + assert.equal(await stakingProxy.balanceOf(account_three), 200, "Delegate account should represent delegator's stake balance."); + + // Stake more tokens + await token.approveAndCall(stakingContract.address, 100, "", {from: account_one}); + assert.equal(await stakingProxy.balanceOf(account_three), 300, "Delegate account should reflect delegator's updated stake balance."); + assert.equal(await stakingProxy.balanceOf(account_one), 0, "Delegator account stake balance should be zero."); + + // Unstake everything + await stakingContract.initiateUnstake(300); + assert.equal(await stakingProxy.balanceOf(account_three), 0, "Delegate account should reflect delegator's updated stake balance."); + }); + + it("should revert if delegate tries to stake", async function() { + await stakingContract.requestDelegateFor(account_one, {from: account_three}); + await stakingContract.approveDelegateAt(account_three, {from: account_one}); + assert.equal(await stakingProxy.balanceOf(account_three), 200, "Delegate account should represent delegator's stake balance."); + + // Stake tokens as account three + await exceptThrow(token.approveAndCall(stakingContract.address, 500, "", {from: account_three})); + }); +}); diff --git a/contracts/solidity/test/TestStakeNoDelay.sol b/contracts/solidity/test/TestStakeNoDelay.sol index 52e161974b..b15ca1f83f 100644 --- a/contracts/solidity/test/TestStakeNoDelay.sol +++ b/contracts/solidity/test/TestStakeNoDelay.sol @@ -21,7 +21,7 @@ contract TestStakeNoDelay { token.approveAndCall(address(stakingContract), 100, ""); Assert.equal(token.balanceOf(address(this)), balance - 100, "Stake amount should be taken out from token holder's main balance."); - Assert.equal(stakingContract.stakeBalanceOf(address(this)), 100, "Stake amount should be added to token holder's stake balance."); + Assert.equal(stakingContract.stakedBalanceOf(address(this)), 100, "Stake amount should be added to token holder's stake balance."); } @@ -40,7 +40,7 @@ contract TestStakeNoDelay { Assert.equal(amount, 100, "Withdrawal request should maintain a record of the amount."); Assert.equal(start, now, "Withdrawal request should maintain a record of when it was initiated."); - Assert.equal(stakingContract.stakeBalanceOf(address(this)), 0, "Unstake amount should be taken out from token holder's stake balance."); + Assert.equal(stakingContract.stakedBalanceOf(address(this)), 0, "Unstake amount should be taken out from token holder's stake balance."); Assert.equal(token.balanceOf(address(this)), balance, "Unstake amount should not be added to token holder main balance."); } @@ -50,7 +50,7 @@ contract TestStakeNoDelay { stakingContract.finishUnstake(withdrawalId); Assert.equal(token.balanceOf(address(this)), balance + 100, "Unstake amount should be added to token holder main balance."); - Assert.equal(stakingContract.stakeBalanceOf(address(this)), 0, "Stake balance should be empty."); + Assert.equal(stakingContract.stakedBalanceOf(address(this)), 0, "Stake balance should be empty."); // Inspect changes in withdrawal request address owner; diff --git a/contracts/solidity/test/TestTokenGrant.sol b/contracts/solidity/test/TestTokenGrant.sol index bc9187cb3d..ab1e9070c0 100644 --- a/contracts/solidity/test/TestTokenGrant.sol +++ b/contracts/solidity/test/TestTokenGrant.sol @@ -1,13 +1,12 @@ pragma solidity ^0.4.21; import "truffle/Assert.sol"; -import "truffle/DeployedAddresses.sol"; import "../contracts/KeepToken.sol"; import "../contracts/TokenGrant.sol"; -import "./helpers/ThrowProxy.sol"; -contract TestTokenGrant { - + +contract TestTokenGrant { + // Create KEEP token. KeepToken t = new KeepToken(); @@ -29,28 +28,4 @@ contract TestTokenGrant { Assert.equal(t.balanceOf(address(this)), balance - 100, "Amount should be taken out from grant creator main balance."); Assert.equal(c.balanceOf(beneficiary), 100, "Amount should be added to beneficiary's granted balance."); } - - function testCanGetGrantByID() public { - uint _amount; - uint _released; - bool _locked; - bool _revoked; - (_amount, _released, _locked, _revoked) = c.getGrant(id); - Assert.equal(_amount, 100, "Grant should maintain a record of the granted amount."); - Assert.equal(_released, 0, "Grant should have 0 amount released initially."); - Assert.equal(_locked, false, "Grant should initially be unlocked."); - Assert.equal(_revoked, false, "Grant should not be marked as revoked initially."); - } - - function testCanGetGrantVestingScheduleByGrantID() public { - address _owner; - uint _duration; - uint _start; - uint _cliff; - (_owner, _duration, _start, _cliff) = c.getGrantVestingSchedule(id); - Assert.equal(_owner, address(this), "Grant should maintain a record of the creator."); - Assert.equal(_duration, duration, "Grant should have vesting schedule duration."); - Assert.equal(_start, start, "Grant should have start time."); - Assert.equal(_cliff, start+cliff, "Grant should have vesting schedule cliff duration."); - } } diff --git a/contracts/solidity/test/TestTokenGrantLookup.sol b/contracts/solidity/test/TestTokenGrantLookup.sol new file mode 100644 index 0000000000..80620085b2 --- /dev/null +++ b/contracts/solidity/test/TestTokenGrantLookup.sol @@ -0,0 +1,38 @@ +pragma solidity ^0.4.21; + +import "truffle/Assert.sol"; +import "../contracts/KeepToken.sol"; +import "../contracts/TokenGrant.sol"; + + +contract TestTokenGrantLookup { + + // Create KEEP token. + KeepToken t = new KeepToken(); + + // Create token grant contract with 30 days withdrawal delay. + TokenGrant c = new TokenGrant(t, 0, 30 days); + + uint id; + address beneficiary = 0xf17f52151EbEF6C7334FAD080c5704D77216b732; + uint start = now; + uint duration = 10 days; + uint cliff = 0; + + function beforeAll() public { + t.approve(address(c), 100); + id = c.grant(100, beneficiary, duration, start, cliff, false); + } + + function testCanGetGrantByID() public { + uint _amount; + uint _released; + bool _locked; + bool _revoked; + (_amount, _released, _locked, _revoked) = c.getGrant(id); + Assert.equal(_amount, 100, "Grant should maintain a record of the granted amount."); + Assert.equal(_released, 0, "Grant should have 0 amount released initially."); + Assert.equal(_locked, false, "Grant should initially be unlocked."); + Assert.equal(_revoked, false, "Grant should not be marked as revoked initially."); + } +} diff --git a/contracts/solidity/test/TestTokenGrantLookupVesting.sol b/contracts/solidity/test/TestTokenGrantLookupVesting.sol new file mode 100644 index 0000000000..f1166a226c --- /dev/null +++ b/contracts/solidity/test/TestTokenGrantLookupVesting.sol @@ -0,0 +1,38 @@ +pragma solidity ^0.4.21; + +import "truffle/Assert.sol"; +import "../contracts/KeepToken.sol"; +import "../contracts/TokenGrant.sol"; + + +contract TestTokenGrantLookupVesting { + + // Create KEEP token. + KeepToken t = new KeepToken(); + + // Create token grant contract with 30 days withdrawal delay. + TokenGrant c = new TokenGrant(t, 0, 30 days); + + uint id; + address beneficiary = 0xf17f52151EbEF6C7334FAD080c5704D77216b732; + uint start = now; + uint duration = 10 days; + uint cliff = 0; + + function beforeAll() public { + t.approve(address(c), 100); + id = c.grant(100, beneficiary, duration, start, cliff, false); + } + + function testCanGetGrantVestingScheduleByGrantID() public { + address _owner; + uint _duration; + uint _start; + uint _cliff; + (_owner, _duration, _start, _cliff) = c.getGrantVestingSchedule(id); + Assert.equal(_owner, address(this), "Grant should maintain a record of the creator."); + Assert.equal(_duration, duration, "Grant should have vesting schedule duration."); + Assert.equal(_start, start, "Grant should have start time."); + Assert.equal(_cliff, start+cliff, "Grant should have vesting schedule cliff duration."); + } +} diff --git a/contracts/solidity/test/TestTokenGrantNonRevocable.sol b/contracts/solidity/test/TestTokenGrantNonRevocable.sol index 520c616e65..17fcb3eaca 100644 --- a/contracts/solidity/test/TestTokenGrantNonRevocable.sol +++ b/contracts/solidity/test/TestTokenGrantNonRevocable.sol @@ -6,8 +6,9 @@ import "../contracts/KeepToken.sol"; import "../contracts/TokenGrant.sol"; import "./helpers/ThrowProxy.sol"; -contract TestTokenGrantNonRevocable { - + +contract TestTokenGrantNonRevocable { + // Create KEEP token. KeepToken t = new KeepToken(); diff --git a/contracts/solidity/test/TestTokenGrantRelease.sol b/contracts/solidity/test/TestTokenGrantRelease.sol index 91c49068ba..a4fcf627ac 100644 --- a/contracts/solidity/test/TestTokenGrantRelease.sol +++ b/contracts/solidity/test/TestTokenGrantRelease.sol @@ -6,8 +6,9 @@ import "../contracts/KeepToken.sol"; import "../contracts/TokenGrant.sol"; import "./helpers/ThrowProxy.sol"; -contract TestTokenGrantRelease { - + +contract TestTokenGrantRelease { + // Create KEEP token. KeepToken t = new KeepToken(); @@ -20,33 +21,6 @@ contract TestTokenGrantRelease { uint duration = 10 days; uint cliff = 0; - // Token holder should be able to grant it's tokens to a beneficiary. - function testCanGrant() public { - uint balance = t.balanceOf(address(this)); - - t.approve(address(c), 100); - id = c.grant(100, beneficiary, duration, start, cliff, false); - Assert.equal(t.balanceOf(address(this)), balance - 100, "Amount should be taken out from grant creator main balance."); - Assert.equal(c.balanceOf(beneficiary), 100, "Amount should be added to beneficiary's granted balance."); - } - - function testCannotReleaseGrantedAmmount() public { - Assert.equal(c.unreleasedAmount(id), 0, "Unreleased token grant amount should be 0."); - - // http://truffleframework.com/tutorials/testing-for-throws-in-solidity-tests - ThrowProxy throwProxy = new ThrowProxy(address(c)); - - // Prime the proxy - TokenGrant(address(throwProxy)).release(id); - - // Execute the call that is supposed to throw. - // r will be false if it threw and true if it didn't. - bool r = throwProxy.execute.gas(200000)(); - Assert.isFalse(r, "Should throw when trying to release token grant."); - - Assert.equal(t.balanceOf(beneficiary), 0, "Released balance should not be added to beneficiary main balance."); - } - // Token grant with 0 duration, release available immediately. function testCanReleaseGrantedAmount() public { diff --git a/contracts/solidity/test/TestTokenGrantReleaseThrow.sol b/contracts/solidity/test/TestTokenGrantReleaseThrow.sol new file mode 100644 index 0000000000..6087645d97 --- /dev/null +++ b/contracts/solidity/test/TestTokenGrantReleaseThrow.sol @@ -0,0 +1,44 @@ +pragma solidity ^0.4.21; + +import "truffle/Assert.sol"; +import "truffle/DeployedAddresses.sol"; +import "../contracts/KeepToken.sol"; +import "../contracts/TokenGrant.sol"; +import "./helpers/ThrowProxy.sol"; + + +contract TestTokenGrantReleaseThrow { + + // Create KEEP token. + KeepToken t = new KeepToken(); + + // Create token grant contract with 30 days withdrawal delay. + TokenGrant c = new TokenGrant(t, 0, 30 days); + + uint id; + address beneficiary = 0xf17f52151EbEF6C7334FAD080c5704D77216b732; + uint start = now; + uint duration = 10 days; + uint cliff = 0; + + function testCannotReleaseGrantedAmmount() public { + + t.approve(address(c), 100); + id = c.grant(100, beneficiary, duration, start, cliff, false); + + Assert.equal(c.unreleasedAmount(id), 0, "Unreleased token grant amount should be 0."); + + // http://truffleframework.com/tutorials/testing-for-throws-in-solidity-tests + ThrowProxy throwProxy = new ThrowProxy(address(c)); + + // Prime the proxy + TokenGrant(address(throwProxy)).release(id); + + // Execute the call that is supposed to throw. + // r will be false if it threw and true if it didn't. + bool r = throwProxy.execute.gas(200000)(); + Assert.isFalse(r, "Should throw when trying to release token grant."); + + Assert.equal(t.balanceOf(beneficiary), 0, "Released balance should not be added to beneficiary main balance."); + } +} diff --git a/contracts/solidity/test/TestTokenGrantRevoke.sol b/contracts/solidity/test/TestTokenGrantRevoke.sol index c658afd87b..e864b906b5 100644 --- a/contracts/solidity/test/TestTokenGrantRevoke.sol +++ b/contracts/solidity/test/TestTokenGrantRevoke.sol @@ -5,8 +5,9 @@ import "truffle/DeployedAddresses.sol"; import "../contracts/KeepToken.sol"; import "../contracts/TokenGrant.sol"; -contract TestTokenGrantRevoke { - + +contract TestTokenGrantRevoke { + // Create KEEP token. KeepToken t = new KeepToken(); @@ -16,39 +17,21 @@ contract TestTokenGrantRevoke { uint id; address beneficiary = 0xf17f52151EbEF6C7334FAD080c5704D77216b732; - // Grant owner can revoke revocable token grant. - function testCanFullyRevokeGrant() public { - uint balance = t.balanceOf(address(this)); - - // Create revocable token grant. - t.approve(address(c), 100); - id = c.grant(100, beneficiary, 10 days, now, 0, true); - - Assert.equal(t.balanceOf(address(this)), balance - 100, "Amount should be taken out from grant creator main balance."); - Assert.equal(c.balanceOf(beneficiary), 100, "Amount should be added to beneficiary's granted balance."); - - c.revoke(id); - - Assert.equal(t.balanceOf(address(this)), balance, "Amount should be returned to token grant owner."); - Assert.equal(c.balanceOf(beneficiary), 0, "Amount should be removed from beneficiary's grant balance."); - } - - // Token grant creator can revoke the grant but no amount + // Token grant creator can revoke the grant but no amount // is refunded since duration of the vesting is over. function testCanZeroRevokeGrant() public { uint balance = t.balanceOf(address(this)); - + // Create revocable token grant with 0 duration. t.approve(address(c), 100); id = c.grant(100, beneficiary, 0, now, 0, true); - + Assert.equal(t.balanceOf(address(this)), balance - 100, "Amount should be removed from grant creator main balance."); Assert.equal(c.balanceOf(beneficiary), 100, "Amount should be added to beneficiary's granted balance."); - + c.revoke(id); Assert.equal(t.balanceOf(address(this)), balance - 100, "No amount to be returned to grant creator since vesting duration is over."); Assert.equal(c.balanceOf(beneficiary), 100, "Amount should stay at beneficiary's grant balance."); } - } diff --git a/contracts/solidity/test/TestTokenGrantRevokeFull.sol b/contracts/solidity/test/TestTokenGrantRevokeFull.sol new file mode 100644 index 0000000000..0876c0572c --- /dev/null +++ b/contracts/solidity/test/TestTokenGrantRevokeFull.sol @@ -0,0 +1,36 @@ +pragma solidity ^0.4.21; + +import "truffle/Assert.sol"; +import "truffle/DeployedAddresses.sol"; +import "../contracts/KeepToken.sol"; +import "../contracts/TokenGrant.sol"; + + +contract TestTokenGrantRevokeFull { + + // Create KEEP token. + KeepToken t = new KeepToken(); + + // Create token grant contract with 30 days withdrawal delay. + TokenGrant c = new TokenGrant(t, 0, 30 days); + + uint id; + address beneficiary = 0xf17f52151EbEF6C7334FAD080c5704D77216b732; + + // Grant owner can revoke revocable token grant. + function testCanFullyRevokeGrant() public { + uint balance = t.balanceOf(address(this)); + + // Create revocable token grant. + t.approve(address(c), 100); + id = c.grant(100, beneficiary, 10 days, now, 0, true); + + Assert.equal(t.balanceOf(address(this)), balance - 100, "Amount should be taken out from grant creator main balance."); + Assert.equal(c.balanceOf(beneficiary), 100, "Amount should be added to beneficiary's granted balance."); + + c.revoke(id); + + Assert.equal(t.balanceOf(address(this)), balance, "Amount should be returned to token grant owner."); + Assert.equal(c.balanceOf(beneficiary), 0, "Amount should be removed from beneficiary's grant balance."); + } +} diff --git a/contracts/solidity/test/TestTokenGrantStake.sol b/contracts/solidity/test/TestTokenGrantStake.sol index 3165758e2a..d89950f5f1 100644 --- a/contracts/solidity/test/TestTokenGrantStake.sol +++ b/contracts/solidity/test/TestTokenGrantStake.sol @@ -6,8 +6,9 @@ import "../contracts/KeepToken.sol"; import "../contracts/TokenGrant.sol"; import "./helpers/ThrowProxy.sol"; -contract TestTokenGrantStake { - + +contract TestTokenGrantStake { + // Create KEEP token KeepToken t = new KeepToken(); @@ -43,20 +44,4 @@ contract TestTokenGrantStake { Assert.equal(c.stakeWithdrawalStart(id), now, "Stake withdrawal start should be set."); Assert.equal(c.stakeBalances(beneficiary), 0, "Stake balance should change immediately after unstake initiation."); } - - // Token grant beneficiary can not finish unstake of the grant until delay is over - function testCannotFinishUnstake() public { - - // http://truffleframework.com/tutorials/testing-for-throws-in-solidity-tests - ThrowProxy throwProxy = new ThrowProxy(address(c)); - - // Prime the proxy. - TokenGrant(address(throwProxy)).finishUnstake(id); - - // Execute the call that is supposed to throw. - // r will be false if it threw and true if it didn't. - bool r = throwProxy.execute.gas(200000)(); - Assert.isFalse(r, "Should throw when trying to unstake when delay is not over."); - Assert.equal(c.stakeBalances(beneficiary), 0, "Stake balance should stay unchanged."); - } } diff --git a/contracts/solidity/test/TestTokenGrantStakeThrow.sol b/contracts/solidity/test/TestTokenGrantStakeThrow.sol new file mode 100644 index 0000000000..f2294f28a0 --- /dev/null +++ b/contracts/solidity/test/TestTokenGrantStakeThrow.sol @@ -0,0 +1,48 @@ +pragma solidity ^0.4.21; + +import "truffle/Assert.sol"; +import "truffle/DeployedAddresses.sol"; +import "../contracts/KeepToken.sol"; +import "../contracts/TokenGrant.sol"; +import "./helpers/ThrowProxy.sol"; + + +contract TestTokenGrantStakeThrow { + + // Create KEEP token + KeepToken t = new KeepToken(); + + // Create token grant contract with 30 days withdrawal delay. + TokenGrant c = new TokenGrant(t, 0, 30 days); + + uint id; + address beneficiary = address(this); // For test simplicity set beneficiary the same as sender. + uint start = now; + uint duration = 10 days; + uint cliff = 0; + + // Token grant beneficiary can not finish unstake of the grant until delay is over + function testCannotFinishUnstake() public { + + // Approve transfer of tokens to the token grant contract. + t.approve(address(c), 100); + // Create new token grant. + id = c.grant(100, beneficiary, duration, start, cliff, false); + // Stake token grant. + c.stake(id); + + c.initiateUnstake(id); + + // http://truffleframework.com/tutorials/testing-for-throws-in-solidity-tests + ThrowProxy throwProxy = new ThrowProxy(address(c)); + + // Prime the proxy. + TokenGrant(address(throwProxy)).finishUnstake(id); + + // Execute the call that is supposed to throw. + // r will be false if it threw and true if it didn't. + bool r = throwProxy.execute.gas(200000)(); + Assert.isFalse(r, "Should throw when trying to unstake when delay is not over."); + Assert.equal(c.stakeBalances(beneficiary), 0, "Stake balance should stay unchanged."); + } +} diff --git a/contracts/solidity/test/TestTokenGrantStakeNoDelay.sol b/contracts/solidity/test/TestTokenGrantUnstakeNoDelay.sol similarity index 60% rename from contracts/solidity/test/TestTokenGrantStakeNoDelay.sol rename to contracts/solidity/test/TestTokenGrantUnstakeNoDelay.sol index d95f98b443..1b8ede9fce 100644 --- a/contracts/solidity/test/TestTokenGrantStakeNoDelay.sol +++ b/contracts/solidity/test/TestTokenGrantUnstakeNoDelay.sol @@ -6,8 +6,9 @@ import "../contracts/KeepToken.sol"; import "../contracts/TokenGrant.sol"; import "./helpers/ThrowProxy.sol"; -contract TestTokenGrantStakeNoDelay { - + +contract TestTokenGrantUnstakeNoDelay { + // Create KEEP token KeepToken t = new KeepToken(); @@ -20,9 +21,8 @@ contract TestTokenGrantStakeNoDelay { uint duration = 10 days; uint cliff = 0; - - // Token grant beneficiary should be able to stake unreleased granted balance. - function testCanStakeTokenGrant() public { + // Token grant beneficiary can finish unstake of token grant when delay is over + function testCanFinishUnstakeTokenGrant() public { // Approve transfer of tokens to the token grant contract. t.approve(address(c), 100); @@ -31,22 +31,7 @@ contract TestTokenGrantStakeNoDelay { // Stake token grant. c.stake(id); - Assert.equal(c.stakeBalances(beneficiary), 100, "Token grant balance should be added to beneficiary grant stake balance."); - - bool _locked; - (, , _locked, , , , , , ,) = c.grants(id); - Assert.equal(_locked, true, "Token grant should become locked."); - } - - // Token grant beneficiary should be able to initiate unstake of the token grant - function testCanInitiateUnstakeTokenGrant() public { c.initiateUnstake(id); - Assert.equal(c.stakeWithdrawalStart(id), now, "Stake withdrawal start should be set."); - Assert.equal(c.stakeBalances(beneficiary), 0, "Stake balance should change immediately after unstake initiation."); - } - - // Token grant beneficiary can finish unstake of token grant when delay is over - function testCanFinishUnstakeTokenGrant() public { c.finishUnstake(id); Assert.equal(c.stakeBalances(beneficiary), 0, "Stake balance should stay unchanged."); bool _locked; diff --git a/contracts/solidity/test/keeptoken.js b/contracts/solidity/test/keeptoken.js index 65eb773008..b5ef29c581 100644 --- a/contracts/solidity/test/keeptoken.js +++ b/contracts/solidity/test/keeptoken.js @@ -48,7 +48,7 @@ contract('KeepToken', function(accounts) { // Ending balances let account_one_ending_balance = await token.balanceOf.call(account_one); - let account_one_stake_balance = await stakingContract.stakeBalanceOf.call(account_one); + let account_one_stake_balance = await stakingContract.stakedBalanceOf.call(account_one); assert.equal(account_one_ending_balance.toNumber(), account_one_starting_balance.toNumber() - stakingAmount, "Staking amount should be transfered from sender balance"); assert.equal(account_one_stake_balance.toNumber(), stakingAmount, "Staking amount should be added to the sender staking balance"); @@ -81,7 +81,7 @@ contract('KeepToken', function(accounts) { // check balances account_one_ending_balance = await token.balanceOf.call(account_one); - account_one_stake_balance = await stakingContract.stakeBalanceOf.call(account_one); + account_one_stake_balance = await stakingContract.stakedBalanceOf.call(account_one); assert.equal(account_one_ending_balance.toNumber(), account_one_starting_balance.toNumber(), "Staking amount should be transfered to sender balance"); assert.equal(account_one_stake_balance.toNumber(), 0, "Staking amount should be removed from sender staking balance"); @@ -170,7 +170,7 @@ contract('KeepToken', function(accounts) { // stake granted tokens can be only called by grant beneficiary await grantContract.stake(id, {from: account_two}); - let account_two_grant_stake_balance = await grantContract.stakeBalanceOf.call(account_two); + let account_two_grant_stake_balance = await grantContract.stakedBalanceOf.call(account_two); assert.equal(account_two_grant_stake_balance.toNumber(), amount, "Should stake grant amount"); // should throw if initiate unstake called by anyone except grant beneficiary @@ -196,7 +196,7 @@ contract('KeepToken', function(accounts) { // jump in time over withdrawal delay await increaseTimeTo(latestTime()+duration.days(30)); await grantContract.finishUnstake(stakeWithdrawalId); - account_two_grant_stake_balance = await grantContract.stakeBalanceOf.call(account_two); + account_two_grant_stake_balance = await grantContract.stakedBalanceOf.call(account_two); assert.equal(account_two_grant_stake_balance.toNumber(), 0, "Stake grant amount should be 0"); // should be able to release 'releasable' granted amount as it's not locked for staking anymore