From 15614dc67e155fd27035e169e43acd38e573796e Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 7 Aug 2020 16:39:46 +0200 Subject: [PATCH] Extracted common tests for bonding abstract contract --- solidity/contracts/AbstractBonding.sol | 11 +- solidity/contracts/KeepBonding.sol | 1 + .../contracts/test/AbstractBondingStub.sol | 26 + .../contracts/test/AuthorizationsStub.sol | 38 + .../contracts/test/StakeDelegetableStub.sol | 51 + solidity/test/AbstractBondingTest.js | 885 ++++++++++++++++++ solidity/test/KeepBondingTest.js | 652 +------------ 7 files changed, 1009 insertions(+), 655 deletions(-) create mode 100644 solidity/contracts/test/AbstractBondingStub.sol create mode 100644 solidity/contracts/test/AuthorizationsStub.sol create mode 100644 solidity/contracts/test/StakeDelegetableStub.sol create mode 100644 solidity/test/AbstractBondingTest.js diff --git a/solidity/contracts/AbstractBonding.sol b/solidity/contracts/AbstractBonding.sol index c6b41adb9..d92dabe3c 100644 --- a/solidity/contracts/AbstractBonding.sol +++ b/solidity/contracts/AbstractBonding.sol @@ -20,6 +20,7 @@ import "@keep-network/keep-core/contracts/StakeDelegatable.sol"; import "@keep-network/sortition-pools/contracts/api/IBonding.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; + /// @title Keep Bonding /// @notice Contract holding deposits from keeps' operators. contract AbstractBonding is IBonding { @@ -189,11 +190,11 @@ contract AbstractBonding is IBonding { /// @param holder Address of the holder of the bond. /// @param referenceID Reference ID of the bond. /// @return Amount of wei in the selected bond. - function bondAmount( - address operator, - address holder, - uint256 referenceID - ) public view returns (uint256) { + function bondAmount(address operator, address holder, uint256 referenceID) + public + view + returns (uint256) + { bytes32 bondID = keccak256( abi.encodePacked(operator, holder, referenceID) ); diff --git a/solidity/contracts/KeepBonding.sol b/solidity/contracts/KeepBonding.sol index 7d5e5be04..a0b2dddec 100644 --- a/solidity/contracts/KeepBonding.sol +++ b/solidity/contracts/KeepBonding.sol @@ -19,6 +19,7 @@ import "./AbstractBonding.sol"; import "@keep-network/keep-core/contracts/TokenGrant.sol"; import "@keep-network/keep-core/contracts/libraries/RolesLookup.sol"; + /// @title Keep Bonding /// @notice Contract holding deposits from keeps' operators. contract KeepBonding is AbstractBonding { diff --git a/solidity/contracts/test/AbstractBondingStub.sol b/solidity/contracts/test/AbstractBondingStub.sol new file mode 100644 index 000000000..7a6faed7b --- /dev/null +++ b/solidity/contracts/test/AbstractBondingStub.sol @@ -0,0 +1,26 @@ +pragma solidity 0.5.17; + +import "../../contracts/AbstractBonding.sol"; + +contract AbstractBondingStub is AbstractBonding { + constructor( + address registryAddress, + address authorizationsAddress, + address stakeDelegatableAddress + ) + public + AbstractBonding( + registryAddress, + authorizationsAddress, + stakeDelegatableAddress + ) + {} + + function withdraw(uint256 amount, address operator) public { + revert("abstract function"); + } + + function withdrawBondExposed(uint256 amount, address operator) public { + withdrawBond(amount, operator); + } +} diff --git a/solidity/contracts/test/AuthorizationsStub.sol b/solidity/contracts/test/AuthorizationsStub.sol new file mode 100644 index 000000000..f666250c6 --- /dev/null +++ b/solidity/contracts/test/AuthorizationsStub.sol @@ -0,0 +1,38 @@ +pragma solidity 0.5.17; + +import "@keep-network/keep-core/contracts/Authorizations.sol"; +import "@keep-network/keep-core/contracts/KeepRegistry.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; + +/// @title Authorizations Stub +/// @dev This contract is for testing purposes only. +contract AuthorizationsStub is Authorizations { + // Authorized operator contracts. + mapping(address => mapping(address => bool)) internal authorizations; + + address public delegatedAuthority; + + constructor(KeepRegistry _registry) public Authorizations(_registry) {} + + function authorizeOperatorContract( + address _operator, + address _operatorContract + ) public { + authorizations[_operatorContract][_operator] = true; + } + + function isAuthorizedForOperator( + address _operator, + address _operatorContract + ) public view returns (bool) { + return authorizations[_operatorContract][_operator]; + } + + function authorizerOf(address _operator) public view returns (address) { + revert("abstract function"); + } + + function claimDelegatedAuthority(address delegatedAuthoritySource) public { + delegatedAuthority = delegatedAuthoritySource; + } +} diff --git a/solidity/contracts/test/StakeDelegetableStub.sol b/solidity/contracts/test/StakeDelegetableStub.sol new file mode 100644 index 000000000..f868c3159 --- /dev/null +++ b/solidity/contracts/test/StakeDelegetableStub.sol @@ -0,0 +1,51 @@ +pragma solidity 0.5.17; + +import "@keep-network/keep-core/contracts/StakeDelegatable.sol"; + +/// @title Stake Delegatable Stub +/// @dev This contract is for testing purposes only. +contract StakeDelegatableStub is StakeDelegatable { + mapping(address => uint256) stakes; + + mapping(address => address) operatorToOwner; + mapping(address => address payable) operatorToBeneficiary; + mapping(address => address) operatorToAuthorizer; + + function setBalance(address _operator, uint256 _balance) public { + stakes[_operator] = _balance; + } + + function balanceOf(address _address) public view returns (uint256 balance) { + return stakes[_address]; + } + + function setOwner(address _operator, address _owner) public { + operatorToOwner[_operator] = _owner; + } + + function ownerOf(address _operator) public view returns (address) { + return operatorToOwner[_operator]; + } + + function setBeneficiary(address _operator, address payable _beneficiary) + public + { + operatorToBeneficiary[_operator] = _beneficiary; + } + + function beneficiaryOf(address _operator) + public + view + returns (address payable) + { + return operatorToBeneficiary[_operator]; + } + + function setAuthorizer(address _operator, address _authorizer) public { + operatorToAuthorizer[_operator] = _authorizer; + } + + function authorizerOf(address _operator) public view returns (address) { + return operatorToAuthorizer[_operator]; + } +} diff --git a/solidity/test/AbstractBondingTest.js b/solidity/test/AbstractBondingTest.js new file mode 100644 index 000000000..a2624b4ce --- /dev/null +++ b/solidity/test/AbstractBondingTest.js @@ -0,0 +1,885 @@ +const {accounts, contract, web3} = require("@openzeppelin/test-environment") +const {createSnapshot, restoreSnapshot} = require("./helpers/snapshot") + +const KeepRegistry = contract.fromArtifact("KeepRegistry") +const AuthorizationsStub = contract.fromArtifact("AuthorizationsStub") +const StakeDelegatableStub = contract.fromArtifact("StakeDelegatableStub") +const AbstractBonding = contract.fromArtifact("AbstractBondingStub") +const TestEtherReceiver = contract.fromArtifact("TestEtherReceiver") + +const { + constants, + expectEvent, + expectRevert, +} = require("@openzeppelin/test-helpers") + +const BN = web3.utils.BN + +const chai = require("chai") +chai.use(require("bn-chai")(BN)) +const expect = chai.expect +const assert = chai.assert + +describe("AbstractBonding", function () { + let registry + let stakeDelegatable + let abstractBonding + let etherReceiver + + let operator + let authorizer + let bondCreator + let sortitionPool + let beneficiary + + before(async () => { + operator = accounts[1] + authorizer = accounts[2] + beneficiary = accounts[3] + bondCreator = accounts[4] + sortitionPool = accounts[5] + + registry = await KeepRegistry.new() + authorizations = await AuthorizationsStub.new(registry.address) + + stakeDelegatable = await StakeDelegatableStub.new() + + abstractBonding = await AbstractBonding.new( + registry.address, + authorizations.address, + stakeDelegatable.address + ) + + etherReceiver = await TestEtherReceiver.new() + + await registry.approveOperatorContract(bondCreator) + + await stakeDelegatable.setAuthorizer(operator, authorizer) + await abstractBonding.authorizeSortitionPoolContract( + operator, + sortitionPool, + { + from: authorizer, + } + ) + + await authorizations.authorizeOperatorContract(operator, bondCreator) + }) + + beforeEach(async () => { + await createSnapshot() + }) + + afterEach(async () => { + await restoreSnapshot() + }) + + describe("deposit", async () => { + const value = new BN(100) + const expectedUnbonded = value + + it("registers unbonded value", async () => { + await stakeDelegatable.setBeneficiary(operator, beneficiary) + await abstractBonding.deposit(operator, {value: value}) + const unbonded = await abstractBonding.availableUnbondedValue( + operator, + bondCreator, + sortitionPool + ) + + expect(unbonded).to.eq.BN(expectedUnbonded, "invalid unbonded value") + }) + + it("sums deposits", async () => { + const value1 = value + const value2 = new BN(230) + + await stakeDelegatable.setBeneficiary(operator, beneficiary) + + await abstractBonding.deposit(operator, {value: value1}) + expect( + await abstractBonding.availableUnbondedValue( + operator, + bondCreator, + sortitionPool + ) + ).to.eq.BN(value1, "invalid unbonded value after first deposit") + + await abstractBonding.deposit(operator, {value: value2}) + expect( + await abstractBonding.availableUnbondedValue( + operator, + bondCreator, + sortitionPool + ) + ).to.eq.BN( + value1.add(value2), + "invalid unbonded value after second deposit" + ) + }) + + it("emits event", async () => { + await stakeDelegatable.setBeneficiary(operator, beneficiary) + const receipt = await abstractBonding.deposit(operator, {value: value}) + expectEvent(receipt, "UnbondedValueDeposited", { + operator: operator, + beneficiary: beneficiary, + amount: value, + }) + }) + + it("reverts if beneficiary is not defined", async () => { + await stakeDelegatable.setBeneficiary(operator, constants.ZERO_ADDRESS) + + await expectRevert( + abstractBonding.deposit(operator, {value: value}), + "Beneficiary not defined for the operator" + ) + }) + }) + + describe("availableUnbondedValue", async () => { + const value = new BN(100) + + beforeEach(async () => { + await stakeDelegatable.setBeneficiary(operator, beneficiary) + await abstractBonding.deposit(operator, {value: value}) + }) + + it("returns zero for operator with no deposit", async () => { + const unbondedOperator = "0x0000000000000000000000000000000000000001" + const expectedUnbonded = 0 + + const unbondedValue = await abstractBonding.availableUnbondedValue( + unbondedOperator, + bondCreator, + sortitionPool + ) + expect(unbondedValue).to.eq.BN(expectedUnbonded, "invalid unbonded value") + }) + + it("return zero when bond creator is not approved by operator", async () => { + const notApprovedBondCreator = + "0x0000000000000000000000000000000000000001" + const expectedUnbonded = 0 + + const unbondedValue = await abstractBonding.availableUnbondedValue( + operator, + notApprovedBondCreator, + sortitionPool + ) + expect(unbondedValue).to.eq.BN(expectedUnbonded, "invalid unbonded value") + }) + + it("returns zero when sortition pool is not authorized", async () => { + const notAuthorizedSortitionPool = + "0x0000000000000000000000000000000000000001" + const expectedUnbonded = 0 + + const unbondedValue = await abstractBonding.availableUnbondedValue( + operator, + bondCreator, + notAuthorizedSortitionPool + ) + expect(unbondedValue).to.eq.BN(expectedUnbonded, "invalid unbonded value") + }) + + it("returns value of operators deposit", async () => { + const expectedUnbonded = value + + const unbonded = await abstractBonding.availableUnbondedValue( + operator, + bondCreator, + sortitionPool + ) + + expect(unbonded).to.eq.BN(expectedUnbonded, "invalid unbonded value") + }) + }) + + describe("createBond", async () => { + const holder = accounts[3] + const value = new BN(100) + + beforeEach(async () => { + await stakeDelegatable.setBeneficiary(operator, beneficiary) + await abstractBonding.deposit(operator, {value: value}) + }) + + it("creates bond", async () => { + const reference = new BN(888) + + const expectedUnbonded = 0 + + await abstractBonding.createBond( + operator, + holder, + reference, + value, + sortitionPool, + {from: bondCreator} + ) + + const unbonded = await abstractBonding.availableUnbondedValue( + operator, + bondCreator, + sortitionPool + ) + expect(unbonded).to.eq.BN(expectedUnbonded, "invalid unbonded value") + + const lockedBonds = await abstractBonding.bondAmount( + operator, + holder, + reference + ) + expect(lockedBonds).to.eq.BN(value, "unexpected bond value") + }) + + it("emits event", async () => { + const reference = new BN(999) + + const receipt = await abstractBonding.createBond( + operator, + holder, + reference, + value, + sortitionPool, + {from: bondCreator} + ) + + expectEvent(receipt, "BondCreated", { + operator: operator, + holder: holder, + sortitionPool: sortitionPool, + referenceID: reference, + amount: value, + }) + }) + + it("creates two bonds with the same reference for different operators", async () => { + const operator2 = accounts[6] + const authorizer2 = accounts[7] + const bondValue = new BN(10) + const reference = new BN(777) + + const expectedUnbonded = value.sub(bondValue) + + await stakeDelegatable.setBeneficiary(operator2, etherReceiver.address) + await stakeDelegatable.setAuthorizer(operator2, authorizer2) + + await abstractBonding.deposit(operator2, {value: value}) + + await authorizations.authorizeOperatorContract(operator2, bondCreator) + await abstractBonding.authorizeSortitionPoolContract( + operator2, + sortitionPool, + {from: authorizer2} + ) + await abstractBonding.createBond( + operator, + holder, + reference, + bondValue, + sortitionPool, + {from: bondCreator} + ) + await abstractBonding.createBond( + operator2, + holder, + reference, + bondValue, + sortitionPool, + {from: bondCreator} + ) + + const unbonded1 = await abstractBonding.availableUnbondedValue( + operator, + bondCreator, + sortitionPool + ) + expect(unbonded1).to.eq.BN(expectedUnbonded, "invalid unbonded value 1") + + const unbonded2 = await abstractBonding.availableUnbondedValue( + operator2, + bondCreator, + sortitionPool + ) + expect(unbonded2).to.eq.BN(expectedUnbonded, "invalid unbonded value 2") + + const lockedBonds1 = await abstractBonding.bondAmount( + operator, + holder, + reference + ) + expect(lockedBonds1).to.eq.BN(bondValue, "unexpected bond value 1") + + const lockedBonds2 = await abstractBonding.bondAmount( + operator2, + holder, + reference + ) + expect(lockedBonds2).to.eq.BN(bondValue, "unexpected bond value 2") + }) + + it("fails to create two bonds with the same reference for the same operator", async () => { + const bondValue = new BN(10) + const reference = new BN(777) + + await abstractBonding.createBond( + operator, + holder, + reference, + bondValue, + sortitionPool, + {from: bondCreator} + ) + + await expectRevert( + abstractBonding.createBond( + operator, + holder, + reference, + bondValue, + sortitionPool, + {from: bondCreator} + ), + "Reference ID not unique for holder and operator" + ) + }) + + it("fails if insufficient unbonded value", async () => { + const bondValue = value.add(new BN(1)) + + await expectRevert( + abstractBonding.createBond( + operator, + holder, + 0, + bondValue, + sortitionPool, + { + from: bondCreator, + } + ), + "Insufficient unbonded value" + ) + }) + }) + + describe("reassignBond", async () => { + const holder = accounts[6] + const newHolder = accounts[3] + const bondValue = new BN(100) + const reference = new BN(777) + const newReference = new BN(888) + + beforeEach(async () => { + await stakeDelegatable.setBeneficiary(operator, beneficiary) + await abstractBonding.deposit(operator, {value: bondValue}) + await abstractBonding.createBond( + operator, + holder, + reference, + bondValue, + sortitionPool, + {from: bondCreator} + ) + }) + + it("reassigns bond to a new holder and a new reference", async () => { + await abstractBonding.reassignBond( + operator, + reference, + newHolder, + newReference, + {from: holder} + ) + + let lockedBonds = await abstractBonding.bondAmount( + operator, + holder, + reference + ) + expect(lockedBonds).to.eq.BN(0, "invalid locked bonds") + + lockedBonds = await abstractBonding.bondAmount( + operator, + holder, + newReference + ) + expect(lockedBonds).to.eq.BN(0, "invalid locked bonds") + + lockedBonds = await abstractBonding.bondAmount( + operator, + newHolder, + reference + ) + expect(lockedBonds).to.eq.BN(0, "invalid locked bonds") + + lockedBonds = await abstractBonding.bondAmount( + operator, + newHolder, + newReference + ) + expect(lockedBonds).to.eq.BN(bondValue, "invalid locked bonds") + }) + + it("reassigns bond to the same holder and a new reference", async () => { + await abstractBonding.reassignBond( + operator, + reference, + holder, + newReference, + {from: holder} + ) + + let lockedBonds = await abstractBonding.bondAmount( + operator, + holder, + reference + ) + expect(lockedBonds).to.eq.BN(0, "invalid locked bonds") + + lockedBonds = await abstractBonding.bondAmount( + operator, + holder, + newReference + ) + expect(lockedBonds).to.eq.BN(bondValue, "invalid locked bonds") + }) + + it("reassigns bond to a new holder and the same reference", async () => { + await abstractBonding.reassignBond( + operator, + reference, + newHolder, + reference, + {from: holder} + ) + + let lockedBonds = await abstractBonding.bondAmount( + operator, + holder, + reference + ) + expect(lockedBonds).to.eq.BN(0, "invalid locked bonds") + + lockedBonds = await abstractBonding.bondAmount( + operator, + newHolder, + reference + ) + expect(lockedBonds).to.eq.BN(bondValue, "invalid locked bonds") + }) + + it("emits event", async () => { + const receipt = await abstractBonding.reassignBond( + operator, + reference, + newHolder, + newReference, + {from: holder} + ) + + expectEvent(receipt, "BondReassigned", { + operator: operator, + referenceID: reference, + newHolder: newHolder, + newReferenceID: newReference, + }) + }) + + it("fails if sender is not the holder", async () => { + await expectRevert( + abstractBonding.reassignBond( + operator, + reference, + newHolder, + newReference, + { + from: accounts[0], + } + ), + "Bond not found" + ) + }) + + it("fails if reassigned to the same holder and the same reference", async () => { + await abstractBonding.deposit(operator, {value: bondValue}) + await abstractBonding.createBond( + operator, + holder, + newReference, + bondValue, + sortitionPool, + {from: bondCreator} + ) + + await expectRevert( + abstractBonding.reassignBond( + operator, + reference, + holder, + newReference, + { + from: holder, + } + ), + "Reference ID not unique for holder and operator" + ) + }) + }) + + describe("freeBond", async () => { + const holder = accounts[6] + const initialUnboundedValue = new BN(500) + const bondValue = new BN(100) + const reference = new BN(777) + + beforeEach(async () => { + await stakeDelegatable.setBeneficiary(operator, beneficiary) + await abstractBonding.deposit(operator, {value: initialUnboundedValue}) + await abstractBonding.createBond( + operator, + holder, + reference, + bondValue, + sortitionPool, + {from: bondCreator} + ) + }) + + it("releases bond amount to operator's available bonding value", async () => { + await abstractBonding.freeBond(operator, reference, {from: holder}) + + const lockedBonds = await abstractBonding.bondAmount( + operator, + holder, + reference + ) + expect(lockedBonds).to.eq.BN(0, "unexpected remaining locked bonds") + + const unbondedValue = await abstractBonding.availableUnbondedValue( + operator, + bondCreator, + sortitionPool + ) + expect(unbondedValue).to.eq.BN( + initialUnboundedValue, + "unexpected unbonded value" + ) + }) + + it("emits event", async () => { + const receipt = await abstractBonding.freeBond(operator, reference, { + from: holder, + }) + + expectEvent(receipt, "BondReleased", { + operator: operator, + referenceID: reference, + }) + }) + + it("fails if sender is not the holder", async () => { + await expectRevert( + abstractBonding.freeBond(operator, reference, {from: accounts[0]}), + "Bond not found" + ) + }) + }) + + describe("seizeBond", async () => { + const holder = accounts[6] + const destination = accounts[3] + const bondValue = new BN(1000) + const reference = new BN(777) + + beforeEach(async () => { + await stakeDelegatable.setBeneficiary(operator, beneficiary) + await abstractBonding.deposit(operator, {value: bondValue}) + await abstractBonding.createBond( + operator, + holder, + reference, + bondValue, + sortitionPool, + {from: bondCreator} + ) + }) + + it("transfers whole bond amount to destination account", async () => { + const amount = bondValue + const expectedBalance = web3.utils + .toBN(await web3.eth.getBalance(destination)) + .add(amount) + + await abstractBonding.seizeBond( + operator, + reference, + amount, + destination, + { + from: holder, + } + ) + + const actualBalance = await web3.eth.getBalance(destination) + expect(actualBalance).to.eq.BN( + expectedBalance, + "invalid destination account balance" + ) + + const lockedBonds = await abstractBonding.bondAmount( + operator, + holder, + reference + ) + expect(lockedBonds).to.eq.BN(0, "unexpected remaining bond value") + }) + + it("emits event", async () => { + const amount = new BN(80) + + const receipt = await abstractBonding.seizeBond( + operator, + reference, + amount, + destination, + { + from: holder, + } + ) + + expectEvent(receipt, "BondSeized", { + operator: operator, + referenceID: reference, + destination: destination, + amount: amount, + }) + }) + + it("transfers less than bond amount to destination account", async () => { + const remainingBond = new BN(1) + const amount = bondValue.sub(remainingBond) + const expectedBalance = web3.utils + .toBN(await web3.eth.getBalance(destination)) + .add(amount) + + await abstractBonding.seizeBond( + operator, + reference, + amount, + destination, + { + from: holder, + } + ) + + const actualBalance = await web3.eth.getBalance(destination) + expect(actualBalance).to.eq.BN( + expectedBalance, + "invalid destination account balance" + ) + + const lockedBonds = await abstractBonding.bondAmount( + operator, + holder, + reference + ) + expect(lockedBonds).to.eq.BN( + remainingBond, + "unexpected remaining bond value" + ) + }) + + it("reverts if seized amount equals zero", async () => { + const amount = new BN(0) + await expectRevert( + abstractBonding.seizeBond(operator, reference, amount, destination, { + from: holder, + }), + "Requested amount should be greater than zero" + ) + }) + + it("reverts if seized amount is greater than bond value", async () => { + const amount = bondValue.add(new BN(1)) + await expectRevert( + abstractBonding.seizeBond(operator, reference, amount, destination, { + from: holder, + }), + "Requested amount is greater than the bond" + ) + }) + + it("reverts if transfer fails", async () => { + await etherReceiver.setShouldFail(true) + const destination = etherReceiver.address + + await expectRevert( + abstractBonding.seizeBond(operator, reference, bondValue, destination, { + from: holder, + }), + "Transfer failed" + ) + + const destinationBalance = await web3.eth.getBalance(destination) + expect(destinationBalance).to.eq.BN( + 0, + "invalid destination account balance" + ) + + const lockedBonds = await abstractBonding.bondAmount( + operator, + holder, + reference + ) + expect(lockedBonds).to.eq.BN(bondValue, "unexpected bond value") + }) + }) + + describe("authorizeSortitionPoolContract", async () => { + it("reverts when operator is not an authorizer", async () => { + const authorizer1 = accounts[6] + + await expectRevert( + abstractBonding.authorizeSortitionPoolContract( + operator, + sortitionPool, + { + from: authorizer1, + } + ), + "Not authorized" + ) + }) + + it("should authorize sortition pool for the provided operator", async () => { + await abstractBonding.authorizeSortitionPoolContract( + operator, + sortitionPool, + {from: authorizer} + ) + + assert.isTrue( + await abstractBonding.hasSecondaryAuthorization( + operator, + sortitionPool + ), + "Sortition pool should be authorized for the provided operator" + ) + }) + }) + + describe("deauthorizeSortitionPoolContract", async () => { + it("reverts when operator is not an authorizer", async () => { + const authorizer1 = accounts[6] + + await expectRevert( + abstractBonding.deauthorizeSortitionPoolContract( + operator, + sortitionPool, + { + from: authorizer1, + } + ), + "Not authorized" + ) + }) + + it("should deauthorize sortition pool for the provided operator", async () => { + await abstractBonding.authorizeSortitionPoolContract( + operator, + sortitionPool, + {from: authorizer} + ) + await abstractBonding.deauthorizeSortitionPoolContract( + operator, + sortitionPool, + {from: authorizer} + ) + assert.isFalse( + await abstractBonding.hasSecondaryAuthorization( + operator, + sortitionPool + ), + "Sortition pool should be deauthorized for the provided operator" + ) + }) + + describe("withdrawBond", async () => { + const value = new BN(1000) + + beforeEach(async () => { + await stakeDelegatable.setBeneficiary(operator, beneficiary) + await abstractBonding.deposit(operator, {value: value}) + }) + + it("transfers unbonded value to beneficiary", async () => { + const expectedUnbonded = 0 + await stakeDelegatable.setBeneficiary(operator, beneficiary) + const expectedBeneficiaryBalance = web3.utils + .toBN(await web3.eth.getBalance(beneficiary)) + .add(value) + + await abstractBonding.withdrawBondExposed(value, operator, { + from: operator, + }) + + const unbonded = await abstractBonding.availableUnbondedValue( + operator, + bondCreator, + sortitionPool + ) + expect(unbonded).to.eq.BN(expectedUnbonded, "invalid unbonded value") + + const actualBeneficiaryBalance = await web3.eth.getBalance(beneficiary) + expect(actualBeneficiaryBalance).to.eq.BN( + expectedBeneficiaryBalance, + "invalid beneficiary balance" + ) + }) + + it("emits event", async () => { + const value = new BN(90) + + const receipt = await abstractBonding.withdrawBondExposed( + value, + operator, + { + from: operator, + } + ) + expectEvent(receipt, "UnbondedValueWithdrawn", { + operator: operator, + beneficiary: beneficiary, + amount: value, + }) + }) + + it("reverts if insufficient unbonded value", async () => { + const invalidValue = value.add(new BN(1)) + + await expectRevert( + abstractBonding.withdrawBondExposed(invalidValue, operator, { + from: operator, + }), + "Insufficient unbonded value" + ) + }) + + it("reverts if transfer fails", async () => { + await etherReceiver.setShouldFail(true) + await stakeDelegatable.setBeneficiary(operator, etherReceiver.address) + + await expectRevert( + abstractBonding.withdrawBondExposed(value, operator, { + from: operator, + }), + "Transfer failed" + ) + }) + }) + }) +}) diff --git a/solidity/test/KeepBondingTest.js b/solidity/test/KeepBondingTest.js index b8233311e..b221e8ed9 100644 --- a/solidity/test/KeepBondingTest.js +++ b/solidity/test/KeepBondingTest.js @@ -8,18 +8,13 @@ const ManagedGrant = contract.fromArtifact("ManagedGrantStub") const KeepBonding = contract.fromArtifact("KeepBonding") const TestEtherReceiver = contract.fromArtifact("TestEtherReceiver") -const { - constants, - expectEvent, - expectRevert, -} = require("@openzeppelin/test-helpers") +const {expectEvent, expectRevert} = require("@openzeppelin/test-helpers") const BN = web3.utils.BN const chai = require("chai") chai.use(require("bn-chai")(BN)) const expect = chai.expect -const assert = chai.assert describe("KeepBonding", function () { let registry @@ -52,6 +47,7 @@ describe("KeepBonding", function () { etherReceiver = await TestEtherReceiver.new() await registry.approveOperatorContract(bondCreator) + await keepBonding.authorizeSortitionPoolContract(operator, sortitionPool, { from: authorizer, }) @@ -67,42 +63,6 @@ describe("KeepBonding", function () { await restoreSnapshot() }) - describe("deposit", async () => { - const value = new BN(100) - const expectedUnbonded = value - - it("registers unbonded value", async () => { - await tokenStaking.setBeneficiary(operator, beneficiary) - await keepBonding.deposit(operator, {value: value}) - const unbonded = await keepBonding.availableUnbondedValue( - operator, - bondCreator, - sortitionPool - ) - - expect(unbonded).to.eq.BN(expectedUnbonded, "invalid unbonded value") - }) - - it("emits event", async () => { - await tokenStaking.setBeneficiary(operator, beneficiary) - const receipt = await keepBonding.deposit(operator, {value: value}) - expectEvent(receipt, "UnbondedValueDeposited", { - operator: operator, - beneficiary: beneficiary, - amount: value, - }) - }) - - it("reverts if beneficiary is not defined", async () => { - await tokenStaking.setBeneficiary(operator, constants.ZERO_ADDRESS) - - await expectRevert( - keepBonding.deposit(operator, {value: value}), - "Beneficiary not defined for the operator" - ) - }) - }) - describe("withdraw", async () => { const value = new BN(1000) @@ -351,612 +311,4 @@ describe("KeepBonding", function () { ) }) }) - - describe("availableUnbondedValue", async () => { - const value = new BN(100) - - beforeEach(async () => { - await tokenStaking.setBeneficiary(operator, beneficiary) - await keepBonding.deposit(operator, {value: value}) - }) - - it("returns zero for operator with no deposit", async () => { - const unbondedOperator = "0x0000000000000000000000000000000000000001" - const expectedUnbonded = 0 - - const unbondedValue = await keepBonding.availableUnbondedValue( - unbondedOperator, - bondCreator, - sortitionPool - ) - expect(unbondedValue).to.eq.BN(expectedUnbonded, "invalid unbonded value") - }) - - it("return zero when bond creator is not approved by operator", async () => { - const notApprovedBondCreator = - "0x0000000000000000000000000000000000000001" - const expectedUnbonded = 0 - - const unbondedValue = await keepBonding.availableUnbondedValue( - operator, - notApprovedBondCreator, - sortitionPool - ) - expect(unbondedValue).to.eq.BN(expectedUnbonded, "invalid unbonded value") - }) - - it("returns zero when sortition pool is not authorized", async () => { - const notAuthorizedSortitionPool = - "0x0000000000000000000000000000000000000001" - const expectedUnbonded = 0 - - const unbondedValue = await keepBonding.availableUnbondedValue( - operator, - bondCreator, - notAuthorizedSortitionPool - ) - expect(unbondedValue).to.eq.BN(expectedUnbonded, "invalid unbonded value") - }) - - it("returns value of operators deposit", async () => { - const expectedUnbonded = value - - const unbonded = await keepBonding.availableUnbondedValue( - operator, - bondCreator, - sortitionPool - ) - - expect(unbonded).to.eq.BN(expectedUnbonded, "invalid unbonded value") - }) - }) - - describe("createBond", async () => { - const holder = accounts[3] - const value = new BN(100) - - beforeEach(async () => { - await tokenStaking.setBeneficiary(operator, beneficiary) - await keepBonding.deposit(operator, {value: value}) - }) - - it("creates bond", async () => { - const reference = new BN(888) - - const expectedUnbonded = 0 - - await keepBonding.createBond( - operator, - holder, - reference, - value, - sortitionPool, - {from: bondCreator} - ) - - const unbonded = await keepBonding.availableUnbondedValue( - operator, - bondCreator, - sortitionPool - ) - expect(unbonded).to.eq.BN(expectedUnbonded, "invalid unbonded value") - - const lockedBonds = await keepBonding.bondAmount( - operator, - holder, - reference - ) - expect(lockedBonds).to.eq.BN(value, "unexpected bond value") - }) - - it("emits event", async () => { - const reference = new BN(999) - - const receipt = await keepBonding.createBond( - operator, - holder, - reference, - value, - sortitionPool, - {from: bondCreator} - ) - - expectEvent(receipt, "BondCreated", { - operator: operator, - holder: holder, - sortitionPool: sortitionPool, - referenceID: reference, - amount: value, - }) - }) - - it("creates two bonds with the same reference for different operators", async () => { - const operator2 = accounts[2] - const authorizer2 = accounts[2] - const bondValue = new BN(10) - const reference = new BN(777) - - const expectedUnbonded = value.sub(bondValue) - - await tokenStaking.setBeneficiary(operator2, etherReceiver.address) - await keepBonding.deposit(operator2, {value: value}) - - await tokenStaking.authorizeOperatorContract(operator2, bondCreator) - await keepBonding.authorizeSortitionPoolContract( - operator2, - sortitionPool, - {from: authorizer2} - ) - await keepBonding.createBond( - operator, - holder, - reference, - bondValue, - sortitionPool, - {from: bondCreator} - ) - await keepBonding.createBond( - operator2, - holder, - reference, - bondValue, - sortitionPool, - {from: bondCreator} - ) - - const unbonded1 = await keepBonding.availableUnbondedValue( - operator, - bondCreator, - sortitionPool - ) - expect(unbonded1).to.eq.BN(expectedUnbonded, "invalid unbonded value 1") - - const unbonded2 = await keepBonding.availableUnbondedValue( - operator2, - bondCreator, - sortitionPool - ) - expect(unbonded2).to.eq.BN(expectedUnbonded, "invalid unbonded value 2") - - const lockedBonds1 = await keepBonding.bondAmount( - operator, - holder, - reference - ) - expect(lockedBonds1).to.eq.BN(bondValue, "unexpected bond value 1") - - const lockedBonds2 = await keepBonding.bondAmount( - operator2, - holder, - reference - ) - expect(lockedBonds2).to.eq.BN(bondValue, "unexpected bond value 2") - }) - - it("fails to create two bonds with the same reference for the same operator", async () => { - const bondValue = new BN(10) - const reference = new BN(777) - - await keepBonding.createBond( - operator, - holder, - reference, - bondValue, - sortitionPool, - {from: bondCreator} - ) - - await expectRevert( - keepBonding.createBond( - operator, - holder, - reference, - bondValue, - sortitionPool, - {from: bondCreator} - ), - "Reference ID not unique for holder and operator" - ) - }) - - it("fails if insufficient unbonded value", async () => { - const bondValue = value.add(new BN(1)) - - await expectRevert( - keepBonding.createBond(operator, holder, 0, bondValue, sortitionPool, { - from: bondCreator, - }), - "Insufficient unbonded value" - ) - }) - }) - - describe("reassignBond", async () => { - const holder = accounts[2] - const newHolder = accounts[3] - const bondValue = new BN(100) - const reference = new BN(777) - const newReference = new BN(888) - - beforeEach(async () => { - await tokenStaking.setBeneficiary(operator, beneficiary) - await keepBonding.deposit(operator, {value: bondValue}) - await keepBonding.createBond( - operator, - holder, - reference, - bondValue, - sortitionPool, - {from: bondCreator} - ) - }) - - it("reassigns bond to a new holder and a new reference", async () => { - await keepBonding.reassignBond( - operator, - reference, - newHolder, - newReference, - {from: holder} - ) - - let lockedBonds = await keepBonding.bondAmount( - operator, - holder, - reference - ) - expect(lockedBonds).to.eq.BN(0, "invalid locked bonds") - - lockedBonds = await keepBonding.bondAmount(operator, holder, newReference) - expect(lockedBonds).to.eq.BN(0, "invalid locked bonds") - - lockedBonds = await keepBonding.bondAmount(operator, newHolder, reference) - expect(lockedBonds).to.eq.BN(0, "invalid locked bonds") - - lockedBonds = await keepBonding.bondAmount( - operator, - newHolder, - newReference - ) - expect(lockedBonds).to.eq.BN(bondValue, "invalid locked bonds") - }) - - it("reassigns bond to the same holder and a new reference", async () => { - await keepBonding.reassignBond( - operator, - reference, - holder, - newReference, - {from: holder} - ) - - let lockedBonds = await keepBonding.bondAmount( - operator, - holder, - reference - ) - expect(lockedBonds).to.eq.BN(0, "invalid locked bonds") - - lockedBonds = await keepBonding.bondAmount(operator, holder, newReference) - expect(lockedBonds).to.eq.BN(bondValue, "invalid locked bonds") - }) - - it("reassigns bond to a new holder and the same reference", async () => { - await keepBonding.reassignBond( - operator, - reference, - newHolder, - reference, - {from: holder} - ) - - let lockedBonds = await keepBonding.bondAmount( - operator, - holder, - reference - ) - expect(lockedBonds).to.eq.BN(0, "invalid locked bonds") - - lockedBonds = await keepBonding.bondAmount(operator, newHolder, reference) - expect(lockedBonds).to.eq.BN(bondValue, "invalid locked bonds") - }) - - it("emits event", async () => { - const receipt = await keepBonding.reassignBond( - operator, - reference, - newHolder, - newReference, - {from: holder} - ) - - expectEvent(receipt, "BondReassigned", { - operator: operator, - referenceID: reference, - newHolder: newHolder, - newReferenceID: newReference, - }) - }) - - it("fails if sender is not the holder", async () => { - await expectRevert( - keepBonding.reassignBond(operator, reference, newHolder, newReference, { - from: accounts[0], - }), - "Bond not found" - ) - }) - - it("fails if reassigned to the same holder and the same reference", async () => { - await keepBonding.deposit(operator, {value: bondValue}) - await keepBonding.createBond( - operator, - holder, - newReference, - bondValue, - sortitionPool, - {from: bondCreator} - ) - - await expectRevert( - keepBonding.reassignBond(operator, reference, holder, newReference, { - from: holder, - }), - "Reference ID not unique for holder and operator" - ) - }) - }) - - describe("freeBond", async () => { - const holder = accounts[2] - const initialUnboundedValue = new BN(500) - const bondValue = new BN(100) - const reference = new BN(777) - - beforeEach(async () => { - await tokenStaking.setBeneficiary(operator, beneficiary) - await keepBonding.deposit(operator, {value: initialUnboundedValue}) - await keepBonding.createBond( - operator, - holder, - reference, - bondValue, - sortitionPool, - {from: bondCreator} - ) - }) - - it("releases bond amount to operator's available bonding value", async () => { - await keepBonding.freeBond(operator, reference, {from: holder}) - - const lockedBonds = await keepBonding.bondAmount( - operator, - holder, - reference - ) - expect(lockedBonds).to.eq.BN(0, "unexpected remaining locked bonds") - - const unbondedValue = await keepBonding.availableUnbondedValue( - operator, - bondCreator, - sortitionPool - ) - expect(unbondedValue).to.eq.BN( - initialUnboundedValue, - "unexpected unbonded value" - ) - }) - - it("emits event", async () => { - const receipt = await keepBonding.freeBond(operator, reference, { - from: holder, - }) - - expectEvent(receipt, "BondReleased", { - operator: operator, - referenceID: reference, - }) - }) - - it("fails if sender is not the holder", async () => { - await expectRevert( - keepBonding.freeBond(operator, reference, {from: accounts[0]}), - "Bond not found" - ) - }) - }) - - describe("seizeBond", async () => { - const holder = accounts[2] - const destination = accounts[3] - const bondValue = new BN(1000) - const reference = new BN(777) - - beforeEach(async () => { - await tokenStaking.setBeneficiary(operator, beneficiary) - await keepBonding.deposit(operator, {value: bondValue}) - await keepBonding.createBond( - operator, - holder, - reference, - bondValue, - sortitionPool, - {from: bondCreator} - ) - }) - - it("transfers whole bond amount to destination account", async () => { - const amount = bondValue - const expectedBalance = web3.utils - .toBN(await web3.eth.getBalance(destination)) - .add(amount) - - await keepBonding.seizeBond(operator, reference, amount, destination, { - from: holder, - }) - - const actualBalance = await web3.eth.getBalance(destination) - expect(actualBalance).to.eq.BN( - expectedBalance, - "invalid destination account balance" - ) - - const lockedBonds = await keepBonding.bondAmount( - operator, - holder, - reference - ) - expect(lockedBonds).to.eq.BN(0, "unexpected remaining bond value") - }) - - it("emits event", async () => { - const amount = new BN(80) - - const receipt = await keepBonding.seizeBond( - operator, - reference, - amount, - destination, - { - from: holder, - } - ) - - expectEvent(receipt, "BondSeized", { - operator: operator, - referenceID: reference, - destination: destination, - amount: amount, - }) - }) - - it("transfers less than bond amount to destination account", async () => { - const remainingBond = new BN(1) - const amount = bondValue.sub(remainingBond) - const expectedBalance = web3.utils - .toBN(await web3.eth.getBalance(destination)) - .add(amount) - - await keepBonding.seizeBond(operator, reference, amount, destination, { - from: holder, - }) - - const actualBalance = await web3.eth.getBalance(destination) - expect(actualBalance).to.eq.BN( - expectedBalance, - "invalid destination account balance" - ) - - const lockedBonds = await keepBonding.bondAmount( - operator, - holder, - reference - ) - expect(lockedBonds).to.eq.BN( - remainingBond, - "unexpected remaining bond value" - ) - }) - - it("reverts if seized amount equals zero", async () => { - const amount = new BN(0) - await expectRevert( - keepBonding.seizeBond(operator, reference, amount, destination, { - from: holder, - }), - "Requested amount should be greater than zero" - ) - }) - - it("reverts if seized amount is greater than bond value", async () => { - const amount = bondValue.add(new BN(1)) - await expectRevert( - keepBonding.seizeBond(operator, reference, amount, destination, { - from: holder, - }), - "Requested amount is greater than the bond" - ) - }) - - it("reverts if transfer fails", async () => { - await etherReceiver.setShouldFail(true) - const destination = etherReceiver.address - - await expectRevert( - keepBonding.seizeBond(operator, reference, bondValue, destination, { - from: holder, - }), - "Transfer failed" - ) - - const destinationBalance = await web3.eth.getBalance(destination) - expect(destinationBalance).to.eq.BN( - 0, - "invalid destination account balance" - ) - - const lockedBonds = await keepBonding.bondAmount( - operator, - holder, - reference - ) - expect(lockedBonds).to.eq.BN(bondValue, "unexpected bond value") - }) - }) - - describe("authorizeSortitionPoolContract", async () => { - it("reverts when operator is not an authorizer", async () => { - const authorizer1 = accounts[2] - - await expectRevert( - keepBonding.authorizeSortitionPoolContract(operator, sortitionPool, { - from: authorizer1, - }), - "Not authorized" - ) - }) - - it("should authorize sortition pool for the provided operator", async () => { - await keepBonding.authorizeSortitionPoolContract( - operator, - sortitionPool, - {from: authorizer} - ) - - assert.isTrue( - await keepBonding.hasSecondaryAuthorization(operator, sortitionPool), - "Sortition pool should be authorized for the provided operator" - ) - }) - }) - - describe("deauthorizeSortitionPoolContract", async () => { - it("reverts when operator is not an authorizer", async () => { - const authorizer1 = accounts[2] - - await expectRevert( - keepBonding.deauthorizeSortitionPoolContract(operator, sortitionPool, { - from: authorizer1, - }), - "Not authorized" - ) - }) - - it("should deauthorize sortition pool for the provided operator", async () => { - await keepBonding.authorizeSortitionPoolContract( - operator, - sortitionPool, - {from: authorizer} - ) - await keepBonding.deauthorizeSortitionPoolContract( - operator, - sortitionPool, - {from: authorizer} - ) - assert.isFalse( - await keepBonding.hasSecondaryAuthorization(operator, sortitionPool), - "Sortition pool should be deauthorized for the provided operator" - ) - }) - }) })