diff --git a/.github/workflows/celo-monorepo.yml b/.github/workflows/celo-monorepo.yml index 9c44fc397e3..d06497efc0b 100644 --- a/.github/workflows/celo-monorepo.yml +++ b/.github/workflows/celo-monorepo.yml @@ -250,9 +250,6 @@ jobs: - name: Protocol Governance Validators command: | yarn --cwd packages/protocol test governance/validators/ - - name: Protocol Governance Voting - command: | - yarn --cwd packages/protocol test governance/voting/ - name: Protocol scripts test command: | yarn --cwd packages/protocol test:scripts diff --git a/packages/protocol/test-sol/governance/voting/ReleaseGold.t.sol b/packages/protocol/test-sol/governance/voting/ReleaseGold.t.sol new file mode 100644 index 00000000000..6a054b12b9b --- /dev/null +++ b/packages/protocol/test-sol/governance/voting/ReleaseGold.t.sol @@ -0,0 +1,2191 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.13; +pragma experimental ABIEncoderV2; + +import "celo-foundry/Test.sol"; +import "../../../contracts/identity/Escrow.sol"; +import "../../../contracts/identity/FederatedAttestations.sol"; +import "../../../contracts/identity/test/MockAttestations.sol"; +import "../../../contracts/identity/test/MockERC20Token.sol"; +import "../../../contracts/common/FixidityLib.sol"; + +import "../../../contracts/common/Registry.sol"; +import "../../../contracts/common/Accounts.sol"; +import "../../../contracts/common/Freezer.sol"; +import "../../../contracts/common/GoldToken.sol"; +import "../../../contracts/governance/LockedGold.sol"; +import "../../../contracts/governance/ReleaseGold.sol"; +import "../../../contracts/stability/test/MockStableToken.sol"; +import "../../../contracts/governance/test/MockElection.sol"; +import "../../../contracts/governance/test/MockGovernance.sol"; +import "../../../contracts/governance/test/MockValidators.sol"; +import "@test-sol/utils/ECDSAHelper.sol"; + +contract ReleaseGoldMockTunnel is ForgeTest { + ReleaseGold private releaseGoldTunnel; + address payable releaseGoldContractAddress; + + struct InitParams { + uint256 releaseStartTime; + uint256 releaseCliffTime; + uint256 numReleasePeriods; + uint256 releasePeriod; + uint256 amountReleasedPerPeriod; + bool revocable; + address payable _beneficiary; + } + + struct InitParams2 { + address _releaseOwner; + address payable _refundAddress; + bool subjectToLiquidityProvision; + uint256 initialDistributionRatio; + bool _canValidate; + bool _canVote; + address registryAddress; + } + + constructor(address _releaseGoldContractAddress) public { + releaseGoldContractAddress = address(uint160(_releaseGoldContractAddress)); + releaseGoldTunnel = ReleaseGold(releaseGoldContractAddress); + } + + function MockInitialize(address sender, InitParams calldata params, InitParams2 calldata params2) + external + returns (bool, bytes memory) + { + bytes4 selector = bytes4( + keccak256( + "initialize(uint256,uint256,uint256,uint256,uint256,bool,address,address,address,bool,uint256,bool,bool,address)" + ) + ); + + bytes memory dataFirstHalf; + { + // Encode the first half of the parameters + dataFirstHalf = abi.encode( + params.releaseStartTime, + params.releaseCliffTime, + params.numReleasePeriods, + params.releasePeriod, + params.amountReleasedPerPeriod, + params.revocable, + params._beneficiary + ); + } + + bytes memory dataSecondHalf; + { + // Encode the second half of the parameters + dataSecondHalf = abi.encode( + params2._releaseOwner, + params2._refundAddress, + params2.subjectToLiquidityProvision, + params2.initialDistributionRatio, + params2._canValidate, + params2._canVote, + params2.registryAddress + ); + } + + // Concatenate the selector, first half, and second half + bytes memory data = abi.encodePacked(selector, dataFirstHalf, dataSecondHalf); + + vm.prank(sender); + (bool success, ) = address(releaseGoldTunnel).call(data); + require(success, "unsuccessful tunnel call"); + } +} + +contract ReleaseGoldTest is Test, ECDSAHelper { + using FixidityLib for FixidityLib.Fraction; + + Registry registry; + Accounts accounts; + Freezer freezer; + GoldToken goldToken; + MockStableToken stableToken; + MockElection election; + MockGovernance governance; + MockValidators validators; + LockedGold lockedGold; + ReleaseGold releaseGold; + ReleaseGold releaseGold2; + + event ReleaseGoldInstanceCreated(address indexed beneficiary, address indexed atAddress); + event ReleaseScheduleRevoked(uint256 revokeTimestamp, uint256 releasedBalanceAtRevoke); + event ReleaseGoldInstanceDestroyed(address indexed beneficiary, address indexed atAddress); + event DistributionLimitSet(address indexed beneficiary, uint256 maxDistribution); + event LiquidityProvisionSet(address indexed beneficiary); + event CanExpireSet(bool canExpire); + event BeneficiarySet(address indexed beneficiary); + + address owner = address(this); + address beneficiary; + uint256 beneficiaryPrivateKey; + address walletAddress = beneficiary; + address releaseOwner = actor("releaseOwner"); + address refundAddress = actor("refundAddress"); + address newBeneficiary = actor("newBeneficiary"); + address randomAddress = actor("randomAddress"); + + uint256 constant TOTAL_AMOUNT = 1 ether * 10; + + uint256 constant MINUTE = 60; + uint256 constant HOUR = 60 * 60; + uint256 constant DAY = 24 * HOUR; + uint256 constant MONTH = 30 * DAY; + uint256 constant UNLOCKING_PERIOD = 3 * DAY; + + ReleaseGoldMockTunnel.InitParams initParams; + ReleaseGoldMockTunnel.InitParams2 initParams2; + + function newReleaseGold(bool prefund, bool startReleasing) internal returns (ReleaseGold) { + releaseGold = new ReleaseGold(true); + + if (prefund) { + goldToken.transfer( + address(releaseGold), + initParams.amountReleasedPerPeriod * initParams.numReleasePeriods + ); + } + + // releaseGold.initialize(config); + ReleaseGoldMockTunnel tunnel = new ReleaseGoldMockTunnel(address(releaseGold)); + tunnel.MockInitialize(owner, initParams, initParams2); + + if (startReleasing) { + vm.warp(block.timestamp + initParams.releaseCliffTime + initParams.releasePeriod + 1); + } + } + + function getParsedSignatureOfAddress(address _address, uint256 privateKey) + public + pure + returns (uint8, bytes32, bytes32) + { + bytes32 addressHash = keccak256(abi.encodePacked(_address)); + bytes32 prefixedHash = ECDSA.toEthSignedMessageHash(addressHash); + return vm.sign(privateKey, prefixedHash); + } + + function setUp() public { + (beneficiary, beneficiaryPrivateKey) = actorWithPK("beneficiary"); + walletAddress = beneficiary; + + address registryAddress = 0x000000000000000000000000000000000000ce10; + deployCodeTo("Registry.sol", abi.encode(false), registryAddress); + registry = Registry(registryAddress); + + accounts = new Accounts(true); + freezer = new Freezer(true); + goldToken = new GoldToken(true); + lockedGold = new LockedGold(true); + election = new MockElection(); + governance = new MockGovernance(); + validators = new MockValidators(); + stableToken = new MockStableToken(); + + registry.setAddressFor("Accounts", address(accounts)); + registry.setAddressFor("Election", address(election)); + registry.setAddressFor("Freezer", address(freezer)); + registry.setAddressFor("GoldToken", address(goldToken)); + registry.setAddressFor("Governance", address(governance)); + registry.setAddressFor("LockedGold", address(lockedGold)); + registry.setAddressFor("Validators", address(validators)); + registry.setAddressFor("StableToken", address(stableToken)); + + lockedGold.initialize(registryAddress, UNLOCKING_PERIOD); + goldToken.initialize(registryAddress); + accounts.initialize(registryAddress); + vm.prank(beneficiary); + accounts.createAccount(); + + initParams = ReleaseGoldMockTunnel.InitParams({ + releaseStartTime: block.timestamp + 5 * MINUTE, + releaseCliffTime: HOUR, + numReleasePeriods: 4, + releasePeriod: 3 * MONTH, + amountReleasedPerPeriod: TOTAL_AMOUNT / 4, + revocable: true, + _beneficiary: address(uint160(beneficiary)) + }); + + initParams2 = ReleaseGoldMockTunnel.InitParams2({ + _releaseOwner: releaseOwner, + _refundAddress: address(uint160(refundAddress)), + subjectToLiquidityProvision: false, + initialDistributionRatio: 1000, + _canValidate: false, + _canVote: true, + registryAddress: registryAddress + }); + + vm.deal(randomAddress, 1000 ether); + } +} + +contract ReleaseGoldInitialize is ReleaseGoldTest { + function setUp() public { + super.setUp(); + } + + function test_ShouldIndicateIsFundedIfDeploymentIsPrefunded() public { + newReleaseGold(true, false); + assertTrue(releaseGold.isFunded()); + } + + function test_ShouldNotIndicateFundedAndNotRevertIfDeploymentIsNotPrefunded() public { + newReleaseGold(false, false); + assertFalse(releaseGold.isFunded()); + } +} + +contract Payable is ReleaseGoldTest { + function setUp() public { + super.setUp(); + } + + function test_ShouldAcceptGoldTransferByDefaultFromAnyone() public { + newReleaseGold(true, false); + uint256 originalBalance = goldToken.balanceOf(address(releaseGold)); + vm.prank(randomAddress); + goldToken.transfer(address(releaseGold), 2 ether); + assertEq(goldToken.balanceOf(address(releaseGold)), 2 ether + originalBalance); + } + + function test_ShouldNotUpdateIsFundedIfSchedulePrincipleNotFulfilled() public { + newReleaseGold(false, false); + uint256 insufficientPrinciple = initParams.amountReleasedPerPeriod * + initParams.numReleasePeriods - + 1; + goldToken.transfer(address(releaseGold), insufficientPrinciple); + assertFalse(releaseGold.isFunded()); + } + + function test_ShouldUpdateIsFundedIfSchedulePrincipleIsFulfilledAfterDeploy() public { + newReleaseGold(false, false); + uint256 sufficientPrinciple = initParams.amountReleasedPerPeriod * initParams.numReleasePeriods; + goldToken.transfer(address(releaseGold), sufficientPrinciple); + assertTrue(releaseGold.isFunded()); + } + + function test_ShouldUpdateIsFundedIfSchedulePrincipleNotFulfilledButHasBegunReleasing() public { + newReleaseGold(false, true); + uint256 insufficientPrinciple = initParams.amountReleasedPerPeriod * + initParams.numReleasePeriods - + 1; + goldToken.transfer(address(releaseGold), insufficientPrinciple); + assertTrue(releaseGold.isFunded()); + } +} + +contract Transfer is ReleaseGoldTest { + address receiver = actor("receiver"); + uint256 transferAmount = 10; + + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + stableToken.mint(address(releaseGold), transferAmount); + } + + function test_ShouldTransferStableTokenFromTheReleaseGoldInstance() public { + vm.prank(beneficiary); + releaseGold.transfer(receiver, transferAmount); + assertEq(stableToken.balanceOf(address(releaseGold)), 0); + assertEq(stableToken.balanceOf(receiver), transferAmount); + } + +} + +contract GenericTransfer is ReleaseGoldTest { + address receiver = actor("receiver"); + uint256 transferAmount = 10; + + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + stableToken.mint(address(releaseGold), transferAmount); + } + + function test_ShouldTransferStableTokenFromTheReleaseGoldInstance() public { + uint256 startBalanceFrom = stableToken.balanceOf(address(releaseGold)); + uint256 startBalanceTo = stableToken.balanceOf(receiver); + vm.prank(beneficiary); + releaseGold.genericTransfer(address(stableToken), receiver, transferAmount); + assertEq(stableToken.balanceOf(address(releaseGold)), startBalanceFrom - transferAmount); + assertEq(stableToken.balanceOf(receiver), startBalanceTo + transferAmount); + } + + function test_ShouldEmitSafeTransferLogsOnErc20Revert() public { + uint256 startBalanceFrom = stableToken.balanceOf(address(releaseGold)); + vm.expectRevert("SafeERC20: ERC20 operation did not succeed"); + vm.prank(beneficiary); + releaseGold.genericTransfer(address(stableToken), receiver, startBalanceFrom + 1); + } + + function test_ShouldRevertWhenAttemptingTransferOfGoldTokenFromTheReleaseGoldInstance() public { + vm.expectRevert("Transfer must not target celo balance"); + vm.prank(beneficiary); + releaseGold.genericTransfer(address(goldToken), receiver, transferAmount); + } + +} + +contract Creation is ReleaseGoldTest { + uint256 public maxUint256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + + function setUp() public { + super.setUp(); + } + + function test_ShouldHaveAssociatedFundsWithAScheduleUponCreation() public { + newReleaseGold(true, false); + assertEq( + goldToken.balanceOf(address(releaseGold)), + initParams.numReleasePeriods * initParams.amountReleasedPerPeriod + ); + } + + function test_ShouldSetABeneficiaryToReleaseGoldInstance() public { + newReleaseGold(true, false); + assertEq(releaseGold.beneficiary(), initParams._beneficiary); + } + + function test_ShouldSetAReleaseOwnerToReleaseGoldInstance() public { + newReleaseGold(true, false); + assertEq(releaseGold.releaseOwner(), initParams2._releaseOwner); + } + + function test_ShouldSetReleaseGoldNumberOfPeriods() public { + newReleaseGold(true, false); + (, , uint256 releaseGoldNumPeriods, , ) = releaseGold.releaseSchedule(); + assertEq(releaseGoldNumPeriods, initParams.numReleasePeriods); + } + + function test_ShouldSetReleaseGoldAmountPerPeriod() public { + newReleaseGold(true, false); + (, , , , uint256 releaseGoldAmountPerPeriod) = releaseGold.releaseSchedule(); + assertEq(releaseGoldAmountPerPeriod, initParams.amountReleasedPerPeriod); + } + + function test_ShouldSetReleaseGoldPeriod() public { + newReleaseGold(true, false); + (, , , uint256 releaseGoldPeriod, ) = releaseGold.releaseSchedule(); + assertEq(releaseGoldPeriod, initParams.releasePeriod); + } + + function test_ShouldSetReleaseGoldStartTime() public { + newReleaseGold(true, false); + (uint256 releaseGoldStartTime, , , , ) = releaseGold.releaseSchedule(); + assertEq(releaseGoldStartTime, initParams.releaseStartTime); + } + + function test_ShouldSetReleaseGoldCliffTime() public { + newReleaseGold(true, false); + (, uint256 releaseGoldCliffTime, , , ) = releaseGold.releaseSchedule(); + uint256 expectedCliffTime = initParams.releaseStartTime + initParams.releaseCliffTime; + assertEq(releaseGoldCliffTime, expectedCliffTime); + } + + function test_ShouldSetRevocableFlagToReleaseGoldInstance() public { + newReleaseGold(true, false); + (bool revocable, , , ) = releaseGold.revocationInfo(); + assertEq(revocable, initParams.revocable); + } + + function test_ShouldSetReleaseOwnerToReleaseGoldInstance() public { + newReleaseGold(true, false); + assertEq(releaseGold.releaseOwner(), initParams2._releaseOwner); + } + + function test_ShouldSetLiquidityProvisionMetToTrue() public { + newReleaseGold(true, false); + assertEq(releaseGold.liquidityProvisionMet(), true); + } + + function test_ShouldHaveZeroTotalWithdrawnOnInit() public { + newReleaseGold(true, false); + assertEq(releaseGold.totalWithdrawn(), 0); + } + + function test_ShouldBeUnrevokedOnInitAndHaveRevokeTimeEqualZero() public { + newReleaseGold(true, false); + (, , , uint256 revokeTime) = releaseGold.revocationInfo(); + assertEq(revokeTime, 0); + assertEq(releaseGold.isRevoked(), false); + } + + function test_ShouldHaveReleaseGoldBalanceAtRevokeOnInitEqualToZero() public { + newReleaseGold(true, false); + (, , uint256 releasedBalanceAtRevoke, ) = releaseGold.revocationInfo(); + assertEq(releasedBalanceAtRevoke, 0); + } + + function test_ShouldRevertWhenReleaseGoldBeneficiaryIsTheNullAddress() public { + releaseGold = new ReleaseGold(true); + initParams._beneficiary = address(0); + ReleaseGoldMockTunnel tunnel = new ReleaseGoldMockTunnel(address(releaseGold)); + vm.expectRevert("unsuccessful tunnel call"); + tunnel.MockInitialize(owner, initParams, initParams2); + } + + function test_ShouldRevertWhenReleaseGoldPeriodsAreZero() public { + releaseGold2 = new ReleaseGold(true); + initParams.numReleasePeriods = 0; + ReleaseGoldMockTunnel tunnel = new ReleaseGoldMockTunnel(address(releaseGold2)); + vm.expectRevert("unsuccessful tunnel call"); + tunnel.MockInitialize(owner, initParams, initParams2); + } + + function test_ShouldRevertWhenReleasedAmountPerPeriodIsZero() public { + releaseGold2 = new ReleaseGold(true); + initParams.amountReleasedPerPeriod = 0; + ReleaseGoldMockTunnel tunnel = new ReleaseGoldMockTunnel(address(releaseGold2)); + vm.expectRevert("unsuccessful tunnel call"); + tunnel.MockInitialize(owner, initParams, initParams2); + } + + function test_ShouldOverflowForVeryLargeCombinationsOdReleasePeriodsAndAmountPerTime() public { + releaseGold = new ReleaseGold(true); + initParams.numReleasePeriods = maxUint256; + initParams.amountReleasedPerPeriod = maxUint256; + initParams2.initialDistributionRatio = 999; + ReleaseGoldMockTunnel tunnel = new ReleaseGoldMockTunnel(address(releaseGold)); + vm.expectRevert("unsuccessful tunnel call"); + tunnel.MockInitialize(owner, initParams, initParams2); + } + +} + +contract SetBeneficiary is ReleaseGoldTest { + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + } + + function test_ShouldSetBeneficiary() public { + releaseGold.setBeneficiary(address(uint160((newBeneficiary)))); + assertEq(releaseGold.beneficiary(), newBeneficiary); + } + + function test_ShouldRevertWhenSettingNewBeneficiaryFromTheReleaseOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(releaseOwner); + releaseGold.setBeneficiary(address(uint160((newBeneficiary)))); + } + + function test_ShouldEmitBeneficiarySetEvent() public { + vm.expectEmit(true, true, true, true); + emit BeneficiarySet(newBeneficiary); + releaseGold.setBeneficiary(address(uint160((newBeneficiary)))); + } +} + +contract CreateAccount is ReleaseGoldTest { + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + } + + function test_ShouldCreateTheAccountByBeneficiary_WhenUnrevoked() public { + assertEq(accounts.isAccount(address(releaseGold)), false); + vm.prank(beneficiary); + releaseGold.createAccount(); + assertEq(accounts.isAccount(address(releaseGold)), true); + } + + function test_ShouldRevertWhenNonBEneficiaryAttemtsAccountCreation_WhenUnrevoked() public { + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + vm.prank(randomAddress); + releaseGold.createAccount(); + } + + function test_ShouldRevertIfAnyoneAttemptsAccountCreation_WhenRevoked() public { + vm.prank(releaseOwner); + releaseGold.revoke(); + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + vm.prank(beneficiary); + releaseGold.createAccount(); + } +} + +contract SetAccount is ReleaseGoldTest { + uint8 v; + bytes32 r; + bytes32 s; + + string accountName = "name"; + bytes dataEncryptionKey = hex"02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111"; + + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + (v, r, s) = getParsedSignatureOfAddress(address(releaseGold), beneficiaryPrivateKey); + } + + function test_ShouldSetTheAccountByBeneficiary_WhenUnrevoked() public { + assertEq(accounts.isAccount(address(releaseGold)), false); + vm.prank(beneficiary); + releaseGold.setAccount(accountName, dataEncryptionKey, walletAddress, v, r, s); + assertEq(accounts.isAccount(address(releaseGold)), true); + } + + function test_ShouldRevertIfNonBeneficiaryAttemptsToSetTheAccount_WhenUnrevoked() public { + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + vm.prank(randomAddress); + releaseGold.setAccount(accountName, dataEncryptionKey, walletAddress, v, r, s); + } + + function test_ShouldSetTheNameDataEncryptionKeyAndWalletAddressOfTheAccountByBeneficiary_WhenUnrevoked() + public + { + vm.prank(beneficiary); + releaseGold.setAccount(accountName, dataEncryptionKey, walletAddress, v, r, s); + assertEq(accounts.getName(address(releaseGold)), accountName); + assertEq(accounts.getDataEncryptionKey(address(releaseGold)), dataEncryptionKey); + assertEq(accounts.getWalletAddress(address(releaseGold)), walletAddress); + } + + function test_ShouldRevertIfAnyOneAttemptsToSetTheAccount_WhenRevoked() public { + vm.prank(releaseOwner); + releaseGold.revoke(); + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + vm.prank(beneficiary); + releaseGold.setAccount(accountName, dataEncryptionKey, walletAddress, v, r, s); + } +} + +contract SetAccountName is ReleaseGoldTest { + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + } + + function test_ShouldRevert_WhenTheAccountHasNotBeenCreated() public { + vm.prank(beneficiary); + vm.expectRevert("Register with createAccount to set account name"); + releaseGold.setAccountName("name"); + } + + function test_ShouldAllowBeneficiaryToSetTheName_WhenAccountWasCreatedAndUnrevoked() public { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.prank(beneficiary); + releaseGold.setAccountName("name"); + assertEq(accounts.getName(address(releaseGold)), "name"); + } + + function test_ShouldRevertIfNonBeneficiaryTriesToSetName_WhenAccountWasCreatedAndUnrevoked() + public + { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + vm.prank(randomAddress); + releaseGold.setAccountName("name"); + } + + function test_ShouldNotAllowBeneficiaryToSetTheName_WhenAccountWasCreatedAndRevoked() public { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.prank(releaseOwner); + releaseGold.revoke(); + vm.prank(beneficiary); + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + releaseGold.setAccountName("name"); + } +} + +contract SetAccountWalletAddress is ReleaseGoldTest { + uint8 v; + bytes32 r; + bytes32 s; + + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + (v, r, s) = getParsedSignatureOfAddress(address(releaseGold), beneficiaryPrivateKey); + } + + function test_ShouldRevertWhenReleaseGoldAccountWasNotCreated() public { + vm.prank(beneficiary); + vm.expectRevert("Unknown account"); + releaseGold.setAccountWalletAddress(walletAddress, v, r, s); + } + + function test_ShouldAllowBeneficiaryToSetTheWalletAddress_WhenAccountWasCreatedAndUnrevoked() + public + { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.prank(beneficiary); + releaseGold.setAccountWalletAddress(walletAddress, v, r, s); + assertEq(accounts.getWalletAddress(address(releaseGold)), walletAddress); + } + + function test_ShouldRevertIfNonBeneficiaryTriesToSetWalletAddress_WhenAccountWasCreatedAndUnrevoked() + public + { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + vm.prank(randomAddress); + releaseGold.setAccountWalletAddress(walletAddress, v, r, s); + } + + function test_ShouldAllowBeneficiaryToSetNullAddress_WhenAccountWasCreatedAndUnrevoked() public { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.prank(beneficiary); + releaseGold.setAccountWalletAddress(address(0), 0, 0x0, 0x0); + assertEq(accounts.getWalletAddress(address(releaseGold)), address(0)); + } + + function test_ShouldRevertIfAnyoneAttemptsToSetTheWalletAddress_WhenACcountWasCreatedAndRevoked() + public + { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.prank(releaseOwner); + releaseGold.revoke(); + vm.prank(beneficiary); + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + releaseGold.setAccountWalletAddress(walletAddress, v, r, s); + } +} + +contract SetAccountMetadataURL is ReleaseGoldTest { + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + } + + function test_ShouldRevert_WhenTheAccountHasNotBeenCreated() public { + vm.prank(beneficiary); + vm.expectRevert("Unknown account"); + releaseGold.setAccountMetadataURL("url"); + } + + function test_ShouldAllowBeneficiaryToSetTheMetadataURL_WhenAccountWasCreatedAndUnrevoked() + public + { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.prank(beneficiary); + releaseGold.setAccountMetadataURL("url2"); + assertEq(accounts.getMetadataURL(address(releaseGold)), "url2"); + } + + function test_ShouldRevertIfNonBeneficiaryTriesToSetMetadataURL_WhenAccountWasCreatedAndUnrevoked() + public + { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + vm.prank(randomAddress); + releaseGold.setAccountMetadataURL("url"); + } + + function test_ShouldNotAllowBeneficiaryToSetTheMetadataURL_WhenAccountWasCreatedAndRevoked() + public + { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.prank(releaseOwner); + releaseGold.revoke(); + vm.prank(beneficiary); + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + releaseGold.setAccountMetadataURL("url"); + } +} + +contract SetAccountDataEncryptionKey is ReleaseGoldTest { + bytes dataEncryptionKey = hex"02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111"; + bytes longDataEncryptionKey = hex"04f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0161111111102f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111"; + + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + vm.prank(beneficiary); + releaseGold.createAccount(); + } + + function test_ShouldAllowBeneficiaryToSetTheDataEncryptionKey_WhenAccountWasCreatedAndUnrevoked() + public + { + vm.prank(beneficiary); + releaseGold.setAccountDataEncryptionKey(dataEncryptionKey); + assertEq(accounts.getDataEncryptionKey(address(releaseGold)), dataEncryptionKey); + } + + function test_ShouldRevertIfNonBeneficiaryTriesToSetDataEncryptionKey_WhenAccountWasCreatedAndUnrevoked() + public + { + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + vm.prank(randomAddress); + releaseGold.setAccountDataEncryptionKey(dataEncryptionKey); + } + + function test_ShouldAllowSettingAKeyWithLEadingZeros() public { + bytes memory keyWithLeadingZeros = hex"00000000000000000000000000000000000000000000000f2f48ee19680706191111"; + vm.prank(beneficiary); + releaseGold.setAccountDataEncryptionKey(keyWithLeadingZeros); + assertEq(accounts.getDataEncryptionKey(address(releaseGold)), keyWithLeadingZeros); + } + + function test_ShouldRevertWhenTheKeyIsInvalid() public { + bytes memory invalidKey = hex"321329312493"; + vm.expectRevert("data encryption key length <= 32"); + vm.prank(beneficiary); + releaseGold.setAccountDataEncryptionKey(invalidKey); + } + + function test_ShouldAllowKeyLongerThan32() public { + vm.prank(beneficiary); + releaseGold.setAccountDataEncryptionKey(longDataEncryptionKey); + assertEq(accounts.getDataEncryptionKey(address(releaseGold)), longDataEncryptionKey); + } + +} + +contract SetMaxDistribution is ReleaseGoldTest { + function setUp() public { + super.setUp(); + initParams2.initialDistributionRatio = 0; + newReleaseGold(true, false); + } + + function test_ShouldSetMaxDistributionTo5Celo_WhenMaxDistributionIsSetTo50Percent() public { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(500); + assertEq(releaseGold.maxDistribution(), TOTAL_AMOUNT / 2); + } + + function test_ShouldSetMaxDistributionTo5Celo_WhenMaxDistributionIsSetTo100Percent() public { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + assertTrue(releaseGold.maxDistribution() >= TOTAL_AMOUNT); + } + + function test_ShouldRevertWhenTryingToLowerMaxDistribution_WhenMaxDistributionIsSetTo100Percent() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + vm.prank(releaseOwner); + vm.expectRevert("Cannot set max distribution lower if already set to 1000"); + releaseGold.setMaxDistribution(500); + } +} + +contract AuthorizationTests is ReleaseGoldTest { + uint256 initialReleaseGoldAmount; + + uint8 v; + bytes32 r; + bytes32 s; + + address authorized; + uint256 authorizedPK; + + function setUp() public { + super.setUp(); + initParams.revocable = false; + initParams2._refundAddress = address(0); + initParams2._canValidate = true; + newReleaseGold(true, false); + vm.prank(beneficiary); + releaseGold.createAccount(); + + (authorized, authorizedPK) = actorWithPK("authorized"); + + (v, r, s) = getParsedSignatureOfAddress(address(releaseGold), authorizedPK); + } + + function test_ShouldSetTheAuthorizedVoteSigner() public { + vm.prank(beneficiary); + releaseGold.authorizeVoteSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.authorizedBy(authorized), address(releaseGold)); + assertEq(accounts.getVoteSigner(address(releaseGold)), authorized); + assertEq(accounts.voteSignerToAccount(authorized), address(releaseGold)); + } + + function test_ShouldSetTheAuthorizedValidatorSigner() public { + vm.prank(beneficiary); + releaseGold.authorizeValidatorSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.authorizedBy(authorized), address(releaseGold)); + assertEq(accounts.getValidatorSigner(address(releaseGold)), authorized); + assertEq(accounts.validatorSignerToAccount(authorized), address(releaseGold)); + } + + function test_ShouldSetTheAuthorizedAttestationSigner() public { + vm.prank(beneficiary); + releaseGold.authorizeAttestationSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.authorizedBy(authorized), address(releaseGold)); + assertEq(accounts.getAttestationSigner(address(releaseGold)), authorized); + assertEq(accounts.attestationSignerToAccount(authorized), address(releaseGold)); + } + + function test_ShouldTransfer1CELOToVoteSigner() public { + uint256 authorizedBalanceBefore = goldToken.balanceOf(authorized); + vm.prank(beneficiary); + releaseGold.authorizeVoteSigner(address(uint160(authorized)), v, r, s); + uint256 authorizedBalanceAfter = goldToken.balanceOf(authorized); + assertEq(authorizedBalanceAfter - authorizedBalanceBefore, 1 ether); + } + + function test_ShouldTransfer1CELOToValidatorSigner() public { + uint256 authorizedBalanceBefore = goldToken.balanceOf(authorized); + vm.prank(beneficiary); + releaseGold.authorizeValidatorSigner(address(uint160(authorized)), v, r, s); + uint256 authorizedBalanceAfter = goldToken.balanceOf(authorized); + assertEq(authorizedBalanceAfter - authorizedBalanceBefore, 1 ether); + } + + function test_ShouldNotTransfer1CELOToAttestationSigner() public { + uint256 authorizedBalanceBefore = goldToken.balanceOf(authorized); + vm.prank(beneficiary); + releaseGold.authorizeAttestationSigner(address(uint160(authorized)), v, r, s); + uint256 authorizedBalanceAfter = goldToken.balanceOf(authorized); + assertEq(authorizedBalanceAfter - authorizedBalanceBefore, 0); + } + + function test_ShouldRevertIfVoteSignerIsAnAccount() public { + vm.prank(authorized); + accounts.createAccount(); + vm.prank(beneficiary); + vm.expectRevert("Cannot re-authorize address or locked gold account for another account"); + releaseGold.authorizeVoteSigner(address(uint160(authorized)), v, r, s); + } + + function test_ShouldRevertIfValidatorSignerIsAnAccount() public { + vm.prank(authorized); + accounts.createAccount(); + vm.prank(beneficiary); + vm.expectRevert("Cannot re-authorize address or locked gold account for another account"); + releaseGold.authorizeValidatorSigner(address(uint160(authorized)), v, r, s); + } + + function test_ShouldRevertIfAttestationSignerIsAnAccount() public { + vm.prank(authorized); + accounts.createAccount(); + vm.prank(beneficiary); + vm.expectRevert("Cannot re-authorize address or locked gold account for another account"); + releaseGold.authorizeAttestationSigner(address(uint160(authorized)), v, r, s); + } + + function test_ShouldRevertIfTheVoteSignerIsAlreadyAuthorized() public { + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount"); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(otherAccount); + accounts.createAccount(); + vm.expectRevert("Cannot re-authorize address or locked gold account for another account"); + vm.prank(beneficiary); + releaseGold.authorizeVoteSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + } + + function test_ShouldRevertIfTheValidatorSignerIsAlreadyAuthorized() public { + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount"); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(otherAccount); + accounts.createAccount(); + vm.expectRevert("Cannot re-authorize address or locked gold account for another account"); + vm.prank(beneficiary); + releaseGold.authorizeValidatorSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + } + + function test_ShouldRevertIfTheAttestationSignerIsAlreadyAuthorized() public { + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount"); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(otherAccount); + accounts.createAccount(); + vm.expectRevert("Cannot re-authorize address or locked gold account for another account"); + vm.prank(beneficiary); + releaseGold.authorizeAttestationSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + } + + function test_ShouldRevertIfTheSignatureIsIncorrect() public { + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount"); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(beneficiary); + vm.expectRevert("Invalid signature"); + releaseGold.authorizeVoteSigner(address(uint160(authorized)), otherV, otherR, otherS); + } + + function test_ShouldSetTheNewAuthorizedVoteSigner_WhenPreviousAuthorizationHasBeenMade() public { + vm.prank(beneficiary); + releaseGold.authorizeVoteSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.getVoteSigner(address(releaseGold)), authorized); + + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount2"); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(beneficiary); + releaseGold.authorizeVoteSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + + assertEq(accounts.authorizedBy(otherAccount), address(releaseGold)); + assertEq(accounts.getVoteSigner(address(releaseGold)), otherAccount); + assertEq(accounts.voteSignerToAccount(otherAccount), address(releaseGold)); + } + + function test_ShouldSetTheNewAuthorizedValidatorSigner_WhenPreviousAuthorizationHasBeenMade() + public + { + vm.prank(beneficiary); + releaseGold.authorizeValidatorSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.getValidatorSigner(address(releaseGold)), authorized); + + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount2"); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(beneficiary); + releaseGold.authorizeValidatorSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + + assertEq(accounts.authorizedBy(otherAccount), address(releaseGold)); + assertEq(accounts.getValidatorSigner(address(releaseGold)), otherAccount); + assertEq(accounts.validatorSignerToAccount(otherAccount), address(releaseGold)); + } + + function test_ShouldSetTheNewAuthorizedAttestationSigner_WhenPreviousAuthorizationHasBeenMade() + public + { + vm.prank(beneficiary); + releaseGold.authorizeAttestationSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.getAttestationSigner(address(releaseGold)), authorized); + + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount2"); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(beneficiary); + releaseGold.authorizeAttestationSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + + assertEq(accounts.authorizedBy(otherAccount), address(releaseGold)); + assertEq(accounts.getAttestationSigner(address(releaseGold)), otherAccount); + assertEq(accounts.attestationSignerToAccount(otherAccount), address(releaseGold)); + } + + function test_ShouldNotTransfer1CEloWhenNewAuthorizedVoteSigner_WhenPreviousAuthorizationHasBeenMade() + public + { + vm.prank(beneficiary); + releaseGold.authorizeVoteSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.getVoteSigner(address(releaseGold)), authorized); + + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount2"); + uint256 otherAccountBalanceBefore = goldToken.balanceOf(otherAccount); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(beneficiary); + releaseGold.authorizeVoteSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + + uint256 otherAccountBalanceAfter = goldToken.balanceOf(otherAccount); + assertEq(otherAccountBalanceAfter - otherAccountBalanceBefore, 0); + } + + function test_ShouldNotTransfer1CEloWhenNewAuthorizedValidatorSigner_WhenPreviousAuthorizationHasBeenMade() + public + { + vm.prank(beneficiary); + releaseGold.authorizeValidatorSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.getValidatorSigner(address(releaseGold)), authorized); + + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount2"); + uint256 otherAccountBalanceBefore = goldToken.balanceOf(otherAccount); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(beneficiary); + releaseGold.authorizeValidatorSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + + uint256 otherAccountBalanceAfter = goldToken.balanceOf(otherAccount); + assertEq(otherAccountBalanceAfter - otherAccountBalanceBefore, 0); + } + + function test_ShouldNotTransfer1CEloWhenNewAuthorizedAttestationSigner_WhenPreviousAuthorizationHasBeenMade() + public + { + vm.prank(beneficiary); + releaseGold.authorizeAttestationSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.getAttestationSigner(address(releaseGold)), authorized); + + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount2"); + uint256 otherAccountBalanceBefore = goldToken.balanceOf(otherAccount); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(beneficiary); + releaseGold.authorizeAttestationSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + + uint256 otherAccountBalanceAfter = goldToken.balanceOf(otherAccount); + assertEq(otherAccountBalanceAfter - otherAccountBalanceBefore, 0); + } + + function test_ShouldPreserveOriginalAuthorizationWhenNewAuthorizedVoteSigner_WhenPreviousAuthorizationHasBeenMade() + public + { + vm.prank(beneficiary); + releaseGold.authorizeVoteSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.getVoteSigner(address(releaseGold)), authorized); + + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount2"); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(beneficiary); + releaseGold.authorizeVoteSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + + assertEq(accounts.authorizedBy(authorized), address(releaseGold)); + } + + function test_ShouldPreserveOriginalAuthorizationWhenNewAuthorizedValidatorSigner_WhenPreviousAuthorizationHasBeenMade() + public + { + vm.prank(beneficiary); + releaseGold.authorizeValidatorSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.getValidatorSigner(address(releaseGold)), authorized); + + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount2"); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(beneficiary); + releaseGold.authorizeValidatorSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + + assertEq(accounts.authorizedBy(authorized), address(releaseGold)); + } + + function test_ShouldPreserveOriginalAuthorizationWhenNewAuthorizedAttestationSigner_WhenPreviousAuthorizationHasBeenMade() + public + { + vm.prank(beneficiary); + releaseGold.authorizeAttestationSigner(address(uint160(authorized)), v, r, s); + assertEq(accounts.getAttestationSigner(address(releaseGold)), authorized); + + (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount2"); + (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( + address(releaseGold), + otherAccountPK + ); + vm.prank(beneficiary); + releaseGold.authorizeAttestationSigner(address(uint160(otherAccount)), otherV, otherR, otherS); + + assertEq(accounts.authorizedBy(authorized), address(releaseGold)); + } +} + +contract AuthorizeWithPublicKeys is ReleaseGoldTest { + uint8 v; + bytes32 r; + bytes32 s; + + address authorized; + uint256 authorizedPK; + + bytes ecdsaPublicKey; + + function _randomBytes32() internal returns (bytes32) { + return keccak256(abi.encodePacked(block.timestamp, block.difficulty, msg.sender)); + } + + function _truncateBytes(bytes memory data, uint256 size) internal pure returns (bytes memory) { + require(size <= data.length, "Size too large"); + bytes memory result = new bytes(size); + for (uint256 i = 0; i < size; i++) { + result[i] = data[i]; + } + return result; + } + + function setUp() public { + super.setUp(); + + initParams.revocable = false; + initParams2._canValidate = true; + initParams2._refundAddress = address(0); + newReleaseGold(true, false); + + vm.prank(beneficiary); + releaseGold.createAccount(); + + (authorized, authorizedPK) = actorWithPK("authorized"); + (v, r, s) = getParsedSignatureOfAddress(address(releaseGold), authorizedPK); + ecdsaPublicKey = addressToPublicKey(keccak256(abi.encodePacked("dummy_msg_data")), v, r, s); + } + + function test_ShouldSetTheAuthorizedKeys_WhenUsingECDSAPublickKey() public { + vm.prank(beneficiary); + releaseGold.authorizeValidatorSignerWithPublicKey( + address(uint160(authorized)), + v, + r, + s, + ecdsaPublicKey + ); + + assertEq(accounts.authorizedBy(authorized), address(releaseGold)); + assertEq(accounts.getValidatorSigner(address(releaseGold)), authorized); + assertEq(accounts.validatorSignerToAccount(authorized), address(releaseGold)); + } + + function test_ShouldSetTheAuthorizedKeys_WhenUsingBLSKeys() public { + bytes32 newBlsPublicKeyPart1 = _randomBytes32(); + bytes32 newBlsPublicKeyPart2 = _randomBytes32(); + bytes32 newBlsPublicKeyPart3 = _randomBytes32(); + bytes memory newBlsPublicKey = abi.encodePacked( + newBlsPublicKeyPart1, + newBlsPublicKeyPart2, + newBlsPublicKeyPart3 + ); + newBlsPublicKey = _truncateBytes(newBlsPublicKey, 96); + + bytes32 newBlsPoPPart1 = _randomBytes32(); + bytes32 newBlsPoPPart2 = _randomBytes32(); + bytes memory newBlsPoP = abi.encodePacked(newBlsPoPPart1, newBlsPoPPart2); + newBlsPoP = _truncateBytes(newBlsPoP, 48); + + vm.prank(beneficiary); + releaseGold.authorizeValidatorSignerWithKeys( + address(uint160(authorized)), + v, + r, + s, + ecdsaPublicKey, + newBlsPublicKey, + newBlsPoP + ); + + assertEq(accounts.authorizedBy(authorized), address(releaseGold)); + assertEq(accounts.getValidatorSigner(address(releaseGold)), authorized); + assertEq(accounts.validatorSignerToAccount(authorized), address(releaseGold)); + } +} + +contract Revoke is ReleaseGoldTest { + function setUp() public { + super.setUp(); + } + + function test_ShouldAllowReleaseOwnerToRevokeTheReleaseGOld() public { + newReleaseGold(true, false); + vm.expectEmit(true, true, true, true); + emit ReleaseScheduleRevoked(block.timestamp, releaseGold.getCurrentReleasedTotalAmount()); + vm.prank(releaseOwner); + releaseGold.revoke(); + assertEq(releaseGold.isRevoked(), true); + (, , , uint256 revokeTime) = releaseGold.revocationInfo(); + assertEq(revokeTime, block.timestamp); + } + + function test_ShouldRevertWhenNonReleaseOwnerAttemptsToRevokeTheReleaseGold() public { + newReleaseGold(true, false); + vm.expectRevert("Sender must be the registered releaseOwner address"); + vm.prank(randomAddress); + releaseGold.revoke(); + } + + function test_ShouldRevertWhenReleaseGoldIsAlreadyRevoked() public { + newReleaseGold(true, false); + vm.prank(releaseOwner); + releaseGold.revoke(); + vm.expectRevert("Release schedule instance must not already be revoked"); + vm.prank(releaseOwner); + releaseGold.revoke(); + } + + function test_ShouldRevertIfReleaseGoldIsNonRevocable() public { + initParams.revocable = false; + initParams2._refundAddress = address(0); + newReleaseGold(true, false); + vm.expectRevert("Release schedule instance must be revocable"); + vm.prank(releaseOwner); + releaseGold.revoke(); + } +} + +contract Expire is ReleaseGoldTest { + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + } + + function test_ShouldRevert_WhenCalledBeforeExpirationTimeHasPassed() public { + vm.expectRevert("`EXPIRATION_TIME` must have passed after the end of releasing"); + vm.prank(releaseOwner); + releaseGold.expire(); + } + + function test_ShouldRevertBeforeEXPIRATION_TIMEAfterReleaseScheduleEnd_WhenTheContractHasFinishedReleasing() + public + { + (, , uint256 numReleasePeriods, uint256 releasePeriod, ) = releaseGold.releaseSchedule(); + vm.warp(block.timestamp + numReleasePeriods * releasePeriod + 5 * MINUTE); + vm.expectRevert("`EXPIRATION_TIME` must have passed after the end of releasing"); + vm.prank(releaseOwner); + releaseGold.expire(); + } + + function test_ShouldRevert_WhenNotCalledByReleaseOwner_WhenEXPIRATION_TIMEHasPassedAfterReleaseScheduleCompletion() + public + { + (uint256 releaseStartTime, , uint256 numReleasePeriods, uint256 releasePeriod, ) = releaseGold + .releaseSchedule(); + vm.warp( + releaseStartTime + numReleasePeriods * releasePeriod + releaseGold.EXPIRATION_TIME() + 1 + ); + vm.expectRevert("Sender must be the registered releaseOwner address"); + vm.prank(randomAddress); + releaseGold.expire(); + } + + function test_ShouldSucceed_WhenCalledByReleaseOwner_WhenEXPIRATION_TIMEHasPassedAfterReleaseScheduleCompletion() + public + { + (uint256 releaseStartTime, , uint256 numReleasePeriods, uint256 releasePeriod, ) = releaseGold + .releaseSchedule(); + vm.warp( + releaseStartTime + numReleasePeriods * releasePeriod + releaseGold.EXPIRATION_TIME() + 1 + ); + vm.prank(releaseOwner); + releaseGold.expire(); + + assertEq(releaseGold.isRevoked(), true); + (, , uint256 releasedBalanceAtRevoke, ) = releaseGold.revocationInfo(); + assertEq(releasedBalanceAtRevoke, 0); + } + + function test_ShouldAllowToRefundOfAllRemainingGold_WhenAnInstanceIsExpired_WhenBeneficiaryHasNotWithdrawnAnyBalanceYet_WhenEXPIRATION_TIMEHasPassedAfterReleaseScheduleCompletion() + public + { + (uint256 releaseStartTime, , uint256 numReleasePeriods, uint256 releasePeriod, ) = releaseGold + .releaseSchedule(); + vm.warp( + releaseStartTime + numReleasePeriods * releasePeriod + releaseGold.EXPIRATION_TIME() + 1 + ); + vm.prank(releaseOwner); + releaseGold.expire(); + + uint256 balanceBefore = goldToken.balanceOf(initParams2._refundAddress); + vm.prank(releaseOwner); + releaseGold.refundAndFinalize(); + uint256 balanceAfter = goldToken.balanceOf(initParams2._refundAddress); + assertEq(balanceAfter - balanceBefore, TOTAL_AMOUNT); + } + + function test_ShouldRevokeTheContract_WhenAnInstanceIsExpired_WhenBeneficiaryHasNotWithdrawnAnyBalanceYet_WhenEXPIRATION_TIMEHasPassedAfterReleaseScheduleCompletionAndBeneficiaryHasWithdrawnSomeBalance() + public + { + (uint256 releaseStartTime, , uint256 numReleasePeriods, uint256 releasePeriod, ) = releaseGold + .releaseSchedule(); + vm.warp( + releaseStartTime + numReleasePeriods * releasePeriod + releaseGold.EXPIRATION_TIME() + 1 + ); + + vm.prank(releaseOwner); + releaseGold.expire(); + assertEq(releaseGold.isRevoked(), true); + } + + function test_SetTheReleasedBalanceAtRevocationToTotalWithdrawn_WhenAnInstanceIsExpired_WhenBeneficiaryHasNotWithdrawnAnyBalanceYet_WhenEXPIRATION_TIMEHasPassedAfterReleaseScheduleCompletionAndBeneficiaryHasWithdrawnSomeBalance() + public + { + (uint256 releaseStartTime, , uint256 numReleasePeriods, uint256 releasePeriod, ) = releaseGold + .releaseSchedule(); + vm.warp( + releaseStartTime + numReleasePeriods * releasePeriod + releaseGold.EXPIRATION_TIME() + 1 + ); + + vm.prank(releaseOwner); + releaseGold.expire(); + + (, , uint256 releasedBalanceAtRevoke, ) = releaseGold.revocationInfo(); + assertEq(releasedBalanceAtRevoke, 0); + } + + function test_ShouldAllowToRefundOfAllRemainingGold_WhenAnInstanceIsExpired_WhenBeneficiaryHasWithdrawnSomeBalance_WhenEXPIRATION_TIMEHasPassedAfterReleaseScheduleCompletionAndBeneficiaryHasWithdrawnSomeBalance() + public + { + (uint256 releaseStartTime, , uint256 numReleasePeriods, uint256 releasePeriod, ) = releaseGold + .releaseSchedule(); + vm.warp( + releaseStartTime + numReleasePeriods * releasePeriod + releaseGold.EXPIRATION_TIME() + 1 + ); + vm.prank(beneficiary); + releaseGold.withdraw(TOTAL_AMOUNT / 2); + vm.prank(releaseOwner); + releaseGold.expire(); + + uint256 balanceBefore = goldToken.balanceOf(initParams2._refundAddress); + vm.prank(releaseOwner); + releaseGold.refundAndFinalize(); + uint256 balanceAfter = goldToken.balanceOf(initParams2._refundAddress); + assertEq(balanceAfter - balanceBefore, TOTAL_AMOUNT / 2); + } + + function test_ShouldRevertWhenContractIsNotExpirable() public { + vm.prank(beneficiary); + releaseGold.setCanExpire(false); + + (uint256 releaseStartTime, , uint256 numReleasePeriods, uint256 releasePeriod, ) = releaseGold + .releaseSchedule(); + vm.warp( + releaseStartTime + numReleasePeriods * releasePeriod + releaseGold.EXPIRATION_TIME() + 1 + ); + + vm.expectRevert("Contract must be expirable"); + vm.prank(releaseOwner); + releaseGold.expire(); + } +} + +contract RefundAndFinalize is ReleaseGoldTest { + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + + // wait some time for some gold to release + vm.warp(block.timestamp + 7 * MONTH); + } + + function test_ShouldBeCallableByReleaseOwnerAndWhenRevoked() public { + vm.prank(releaseOwner); + releaseGold.revoke(); + vm.prank(releaseOwner); + releaseGold.refundAndFinalize(); + } + + function test_ShouldRevertWhenRevokeCalledByNonReleaseOwner() public { + vm.prank(releaseOwner); + releaseGold.revoke(); + vm.expectRevert("Sender must be the releaseOwner and state must be revoked"); + vm.prank(randomAddress); + releaseGold.refundAndFinalize(); + } + + function test_ShouldRevertWhenNonRevokedButCalledByReleaseOwner() public { + vm.expectRevert("Sender must be the releaseOwner and state must be revoked"); + vm.prank(releaseOwner); + releaseGold.refundAndFinalize(); + } + + function test_ShouldTransferGoldProportionsToBothBeneficiaryAndRefundAddressWhenNoGoldLocked_WhenRevoked() + public + { + vm.prank(releaseOwner); + releaseGold.revoke(); + + uint256 refundAddressBalanceBefore = goldToken.balanceOf(initParams2._refundAddress); + uint256 beneficiaryBalanceBefore = goldToken.balanceOf(initParams._beneficiary); + (, , uint256 releasedBalanceAtRevoke, ) = releaseGold.revocationInfo(); + uint256 beneficiaryRefundAmount = releasedBalanceAtRevoke - releaseGold.totalWithdrawn(); + uint256 refundAddressRefundAmount = goldToken.balanceOf(address(releaseGold)) - + beneficiaryRefundAmount; + vm.prank(releaseOwner); + releaseGold.refundAndFinalize(); + uint256 releaseGoldContractBalanceAfterFinalize = goldToken.balanceOf(address(goldToken)); + + uint256 refundAddressBalanceAfter = goldToken.balanceOf(initParams2._refundAddress); + uint256 beneficiaryBalanceAfter = goldToken.balanceOf(initParams._beneficiary); + + assertEq(beneficiaryBalanceAfter - beneficiaryBalanceBefore, beneficiaryRefundAmount); + assertEq(refundAddressBalanceAfter - refundAddressBalanceBefore, refundAddressRefundAmount); + + assertEq(releaseGoldContractBalanceAfterFinalize, 0); + } +} + +contract ExpireSelfDestructTest is ReleaseGoldTest { + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + + vm.prank(releaseOwner); + releaseGold.revoke(); + vm.prank(releaseOwner); + // selfdestruct can be tested only when called in setUp since destruction itself happens only after call is finished + releaseGold.refundAndFinalize(); + } + + function test_ShouldDestructReleaseGoldInstanceAfterFinalizingAndPreventCallingFurtherActions_WhenRevoked() + public + { + vm.expectRevert(); + releaseGold.getRemainingUnlockedBalance(); + } +} + +contract LockGold is ReleaseGoldTest { + uint256 lockAmount; + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + lockAmount = initParams.amountReleasedPerPeriod * initParams.numReleasePeriods; + } + + function test_ShouldAllowBeneficiaryToLockUpAnyUnlockedAmount() public { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.prank(beneficiary); + releaseGold.lockGold(lockAmount); + assertEq(lockedGold.getNonvotingLockedGold(), lockAmount); + assertEq(lockedGold.getTotalLockedGold(), lockAmount); + } + + function test_ShouldRevertIfReleaseGoldInstanceIsNotAccount() public { + vm.expectRevert("Must first register address with Account.createAccount"); + vm.prank(beneficiary); + releaseGold.lockGold(lockAmount); + } + + function test_ShouldRevertIfBeneficiaryTriesToLockUpMoreThanThereIsRemainingInTheContract() + public + { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.prank(beneficiary); + vm.expectRevert(); + releaseGold.lockGold(lockAmount + 1); + } + + function test_ShouldRevertIfNonBeneficiaryTriesToLockUpAnyUnlockedAmount() public { + vm.prank(beneficiary); + releaseGold.createAccount(); + vm.expectRevert("Sender must be the beneficiary and state must not be revoked"); + vm.prank(randomAddress); + releaseGold.lockGold(lockAmount); + } +} + +contract UnlockGold is ReleaseGoldTest { + uint256 lockAmount; + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + vm.prank(beneficiary); + releaseGold.createAccount(); + lockAmount = initParams.amountReleasedPerPeriod * initParams.numReleasePeriods; + } + + function test_ShouldAllowBeneficiaryToUnlockHisLockedGoldAndAddAPendingWithdrawal() public { + vm.prank(beneficiary); + releaseGold.lockGold(lockAmount); + vm.prank(beneficiary); + releaseGold.unlockGold(lockAmount); + (uint256[] memory values, uint256[] memory timestamps) = lockedGold.getPendingWithdrawals( + address(releaseGold) + ); + assertEq(values.length, 1); + assertEq(timestamps.length, 1); + + assertEq(values[0], lockAmount); + assertEq(timestamps[0], block.timestamp + UNLOCKING_PERIOD); + + assertEq(lockedGold.getAccountTotalLockedGold(address(releaseGold)), 0); + assertEq(releaseGold.getRemainingLockedBalance(), lockAmount); + assertEq(lockedGold.getAccountNonvotingLockedGold(address(releaseGold)), 0); + assertEq(lockedGold.getNonvotingLockedGold(), 0); + assertEq(lockedGold.getTotalLockedGold(), 0); + } + + function test_ShouldRevertIfNonBeneficiaryTriesToUnlockTheLockedAmount() public { + vm.prank(beneficiary); + releaseGold.lockGold(lockAmount); + vm.expectRevert("Must be called by releaseOwner when revoked or beneficiary before revocation"); + vm.prank(randomAddress); + releaseGold.unlockGold(lockAmount); + } + + function test_ShouldRevertIfBeneficiaryInVotingTriesToUnlockTheLockedAmount() public { + governance.setTotalVotes(address(releaseGold), lockAmount); + vm.prank(beneficiary); + releaseGold.lockGold(lockAmount); + vm.expectRevert("Not enough unlockable celo. Celo is locked in voting."); + vm.prank(beneficiary); + releaseGold.unlockGold(lockAmount); + } + + function test_ShouldRevertIfBeneficiaryWithBalanceRequirementsTriesToUnlockTheLockedAmount() + public + { + governance.setVoting(address(releaseGold)); + vm.prank(beneficiary); + releaseGold.lockGold(lockAmount); + validators.setAccountLockedGoldRequirement(address(releaseGold), 10); + vm.expectRevert( + "Either account doesn't have enough locked Celo or locked Celo is being used for voting." + ); + vm.prank(beneficiary); + releaseGold.unlockGold(lockAmount); + } +} + +contract WithdrawLockedGold is ReleaseGoldTest { + uint256 value = 1000; + uint256 index = 0; + + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + } + + function test_ShouldRemoveThePendingWithdrawal_WhenItIsAfterTheAvailabilityTime_WhenPendingWithdrawalExits() + public + { + vm.startPrank(beneficiary); + releaseGold.createAccount(); + releaseGold.lockGold(value); + releaseGold.unlockGold(value); + + vm.warp(block.timestamp + UNLOCKING_PERIOD + 1); + releaseGold.withdrawLockedGold(index); + + (uint256[] memory values, uint256[] memory timestamps) = lockedGold.getPendingWithdrawals( + address(releaseGold) + ); + + assertEq(values.length, 0); + assertEq(timestamps.length, 0); + assertEq(releaseGold.getRemainingLockedBalance(), 0); + + vm.stopPrank(); + } + + function test_ShouldRevert_WhenItIsBeforeTheAvailabilityTime_WhenPendingWithdrawalExits() public { + vm.startPrank(beneficiary); + releaseGold.createAccount(); + releaseGold.lockGold(value); + releaseGold.unlockGold(value); + + vm.warp(block.timestamp + UNLOCKING_PERIOD - 1); + vm.expectRevert("Pending withdrawal not available"); + releaseGold.withdrawLockedGold(index); + + vm.stopPrank(); + } + + function test_ShouldRevert_WhenCalledByNonBeneficiary_WhenItIsAfterTheAvailabilityTime_WhenPendingWithdrawalExits() + public + { + vm.startPrank(beneficiary); + releaseGold.createAccount(); + releaseGold.lockGold(value); + releaseGold.unlockGold(value); + vm.stopPrank(); + + vm.warp(block.timestamp + UNLOCKING_PERIOD + 1); + vm.expectRevert("Must be called by releaseOwner when revoked or beneficiary before revocation"); + vm.prank(randomAddress); + releaseGold.withdrawLockedGold(index); + } + + function test_ShouldRevert_WhenPendingWithdrawalDoesNotExist() public { + vm.prank(beneficiary); + releaseGold.createAccount(); + + vm.warp(block.timestamp + UNLOCKING_PERIOD + 1); + vm.expectRevert("Bad pending withdrawal index"); + vm.prank(beneficiary); + releaseGold.withdrawLockedGold(index); + } +} + +contract RelockGold is ReleaseGoldTest { + uint256 pendingWithdrawalValue = 1000; + uint256 index = 0; + + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + vm.startPrank(beneficiary); + releaseGold.createAccount(); + releaseGold.lockGold(pendingWithdrawalValue); + releaseGold.unlockGold(pendingWithdrawalValue); + vm.stopPrank(); + } + + function test_ShouldIncreaseUpdateCorrectly_WhenRelockingValueEqualToValueOfThePendingWithdrawal_WhenPendingWithdrawalExits() + public + { + vm.prank(beneficiary); + releaseGold.relockGold(index, pendingWithdrawalValue); + + assertEq( + lockedGold.getAccountNonvotingLockedGold(address(releaseGold)), + pendingWithdrawalValue + ); + assertEq(lockedGold.getAccountTotalLockedGold(address(releaseGold)), pendingWithdrawalValue); + assertEq(lockedGold.getNonvotingLockedGold(), pendingWithdrawalValue); + assertEq(lockedGold.getTotalLockedGold(), pendingWithdrawalValue); + + (uint256[] memory values, uint256[] memory timestamps) = lockedGold.getPendingWithdrawals( + address(releaseGold) + ); + assertEq(values.length, 0); + assertEq(timestamps.length, 0); + } + + function test_ShouldIncreaseAccountNonVotingLockedGoldCorrectly_WhenRelockingValueLessToValueOfThePendingWithdrawalAndWhenPendingWithdrawalExits() + public + { + vm.prank(beneficiary); + releaseGold.relockGold(index, pendingWithdrawalValue - 1); + + assertEq( + lockedGold.getAccountNonvotingLockedGold(address(releaseGold)), + pendingWithdrawalValue - 1 + ); + } + + function test_ShouldIncreaseTotalAccountLockedGoldCorrectly_WhenRelockingValueLessToValueOfThePendingWithdrawalAndWhenPendingWithdrawalExits() + public + { + vm.prank(beneficiary); + releaseGold.relockGold(index, pendingWithdrawalValue - 1); + + assertEq( + lockedGold.getAccountTotalLockedGold(address(releaseGold)), + pendingWithdrawalValue - 1 + ); + } + + function test_ShouldIncreaseTotalNonVotingLockedGoldCorrectly_WhenRelockingValueLessToValueOfThePendingWithdrawalAndWhenPendingWithdrawalExits() + public + { + vm.prank(beneficiary); + releaseGold.relockGold(index, pendingWithdrawalValue - 1); + + assertEq(lockedGold.getNonvotingLockedGold(), pendingWithdrawalValue - 1); + } + + function test_ShouldIncreaseTotalLockedGoldCorrectly_WhenRelockingValueLessToValueOfThePendingWithdrawalAndWhenPendingWithdrawalExits() + public + { + vm.prank(beneficiary); + releaseGold.relockGold(index, pendingWithdrawalValue - 1); + + assertEq(lockedGold.getTotalLockedGold(), pendingWithdrawalValue - 1); + } + + function test_ShouldUpdatePendingWithdrawalsCorrectly_WhenRelockingValueLessToValueOfThePendingWithdrawalAndWhenPendingWithdrawalExits() + public + { + vm.prank(beneficiary); + releaseGold.relockGold(index, pendingWithdrawalValue - 1); + + (uint256[] memory values, uint256[] memory timestamps) = lockedGold.getPendingWithdrawals( + address(releaseGold) + ); + assertEq(values.length, 1); + assertEq(timestamps.length, 1); + assertEq(values[0], 1); + } + + function test_ShouldRevert_WhenRelockingValueLessToValueOfThePendingWithdrawal_WhenPendingWithdrawalExits() + public + { + vm.expectRevert("Requested value larger than pending value"); + vm.prank(beneficiary); + releaseGold.relockGold(index, pendingWithdrawalValue + 1); + } + + function test_ShouldRevertWhenPendingWithdrawalDoesNotExit() public { + vm.expectRevert("Bad pending withdrawal index"); + vm.prank(beneficiary); + releaseGold.relockGold(1, pendingWithdrawalValue); + } +} + +contract Withdraw is ReleaseGoldTest { + uint256 initialReleaseGoldAmount; + + function setUp() public { + super.setUp(); + + initParams2.initialDistributionRatio = 0; + initialReleaseGoldAmount = initParams.amountReleasedPerPeriod * initParams.numReleasePeriods; + newReleaseGold(true, false); + } + + function test_ShouldRevertBeforeTheReleaseCliffHasPassed() public { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + vm.warp(block.timestamp + 30 * MINUTE); + vm.expectRevert("Requested amount is greater than available released funds"); + vm.prank(beneficiary); + releaseGold.withdraw(initialReleaseGoldAmount / 20); + } + + function test_ShouldRevertWhenWithdrawableAmountIsZero() public { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + vm.warp(block.timestamp + 3 * MONTH + 1 * DAY); + vm.expectRevert("Requested withdrawal amount must be greater than zero"); + vm.prank(beneficiary); + releaseGold.withdraw(0); + } + + function test_ShouldRevertSinceBeneficiaryShouldNotBeABleToWithdrawAnythingWIthingTheFirstQuarter_WhenNotRevoked_WhenMaxDistributionIs100Percent() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + uint256 beneficiaryBalanceBefore = goldToken.balanceOf(initParams._beneficiary); + vm.warp(block.timestamp + 3 * 29 * DAY); + uint256 expectedWithdrawalAmount = releaseGold.getCurrentReleasedTotalAmount(); + + vm.expectRevert("Requested withdrawal amount must be greater than zero"); + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount); + + uint256 beneficiaryBalanceAfter = goldToken.balanceOf(initParams._beneficiary); + assertEq(beneficiaryBalanceAfter - beneficiaryBalanceBefore, 0); + } + + function test_ShouldAllowBeneficiaryToWithdraw25PercentAFterTheFirstQuarter_WhenNotRevoked_WhenMaxDistributionIs100Percent() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + uint256 beneficiaryBalanceBefore = goldToken.balanceOf(initParams._beneficiary); + vm.warp(block.timestamp + 3 * MONTH + 1 * DAY); + uint256 expectedWithdrawalAmount = initialReleaseGoldAmount / 4; + + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount); + + assertEq(expectedWithdrawalAmount, releaseGold.totalWithdrawn(), "Incorrect withdrawalAmount"); + + uint256 beneficiaryBalanceAfter = goldToken.balanceOf(initParams._beneficiary); + assertEq(beneficiaryBalanceAfter - beneficiaryBalanceBefore, expectedWithdrawalAmount); + } + + function test_ShouldAllowBeneficiaryToWithdraw50PercentAFterTheSecondQuarter_WhenNotRevoked_WhenMaxDistributionIs100Percent() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + uint256 beneficiaryBalanceBefore = goldToken.balanceOf(initParams._beneficiary); + vm.warp(block.timestamp + 6 * MONTH + 1 * DAY); + uint256 expectedWithdrawalAmount = initialReleaseGoldAmount / 2; + + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount); + + assertEq(expectedWithdrawalAmount, releaseGold.totalWithdrawn(), "Incorrect withdrawalAmount"); + + uint256 beneficiaryBalanceAfter = goldToken.balanceOf(initParams._beneficiary); + assertEq(beneficiaryBalanceAfter - beneficiaryBalanceBefore, expectedWithdrawalAmount); + } + + function test_ShouldAllowBeneficiaryToWithdraw75PercentAFterTheThirdQuarter_WhenNotRevoked_WhenMaxDistributionIs100Percent() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + uint256 beneficiaryBalanceBefore = goldToken.balanceOf(initParams._beneficiary); + vm.warp(block.timestamp + 9 * MONTH + 1 * DAY); + uint256 expectedWithdrawalAmount = (initialReleaseGoldAmount / 4) * 3; + + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount); + + assertEq(expectedWithdrawalAmount, releaseGold.totalWithdrawn(), "Incorrect withdrawalAmount"); + + uint256 beneficiaryBalanceAfter = goldToken.balanceOf(initParams._beneficiary); + assertEq(beneficiaryBalanceAfter - beneficiaryBalanceBefore, expectedWithdrawalAmount); + } + + function test_ShouldAllowBeneficiaryToWithdraw100PercentAFterTheFourthQuarter_WhenNotRevoked_WhenMaxDistributionIs100Percent() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + uint256 beneficiaryBalanceBefore = goldToken.balanceOf(initParams._beneficiary); + vm.warp(block.timestamp + 12 * MONTH + 1 * DAY); + uint256 expectedWithdrawalAmount = initialReleaseGoldAmount; + + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount); + + assertEq(expectedWithdrawalAmount, releaseGold.totalWithdrawn(), "Incorrect withdrawalAmount"); + + uint256 beneficiaryBalanceAfter = goldToken.balanceOf(initParams._beneficiary); + assertEq(beneficiaryBalanceAfter - beneficiaryBalanceBefore, expectedWithdrawalAmount); + } + + function test_ShouldAllowDistributionOfInitialBalanceAndRewards_WhenTheGrantHasFullyReleased_WhenRewardsAreSimulated__WhenNotRevoked_WhenMaxDistributionIs100Percent() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + vm.prank(randomAddress); + goldToken.transfer(address(releaseGold), 1 ether / 2); + vm.warp(block.timestamp + 12 * MONTH + 1 * DAY); + + uint256 expectedWIthdrawalAmount = TOTAL_AMOUNT + 1 ether / 2; + vm.prank(beneficiary); + releaseGold.withdraw(expectedWIthdrawalAmount); + } + + function test_ShouldAllowDistributionOfHalfInitialBalanceAndHalfRewards_WhenTheGrantIsHalfwayReleased_WhenRewardsAreSimulated_WhenNotRevoked_WhenMaxDistributionIs100Percent() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + vm.prank(randomAddress); + goldToken.transfer(address(releaseGold), 1 ether / 2); + vm.warp(block.timestamp + 6 * MONTH + 1 * DAY); + + uint256 expectedWIthdrawalAmount = (TOTAL_AMOUNT + 1 ether / 2) / 2; + vm.prank(beneficiary); + releaseGold.withdraw(expectedWIthdrawalAmount); + } + + function test_ShouldRevertWhenRequestingMoreThanHalf_WhenTheGrantIsHalfwayReleased_WhenRewardsAreSimulated_WhenNotRevoked_WhenMaxDistributionIs100Percent() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + vm.prank(randomAddress); + goldToken.transfer(address(releaseGold), 1 ether / 2); + vm.warp(block.timestamp + 6 * MONTH + 1 * DAY); + + uint256 expectedWIthdrawalAmount = (TOTAL_AMOUNT + 1 ether / 2) / 2 + 1; + vm.prank(beneficiary); + vm.expectRevert("Requested amount is greater than available released funds"); + releaseGold.withdraw(expectedWIthdrawalAmount); + } + + function test_ShouldRevertWhenWithdrawingMoreThan50Percent_WhenTheGrantHasFullyReleased_WhenRewardsAreSimulated__WhenNotRevoked_WhenMaxDistributionIs50Percent() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(500); + + vm.prank(randomAddress); + // Simulate rewards of 0.5 Gold + // Have to send after setting max distribution + goldToken.transfer(address(releaseGold), 1 ether / 2); + vm.warp(block.timestamp + 12 * MONTH + 1 * DAY); + + uint256 expectedWIthdrawalAmount = TOTAL_AMOUNT / 2; + vm.prank(beneficiary); + releaseGold.withdraw(expectedWIthdrawalAmount); + + uint256 unexpectedWIthdrawalAmount = 1; + vm.prank(beneficiary); + vm.expectRevert("Requested amount exceeds current alloted maximum distribution"); + releaseGold.withdraw(unexpectedWIthdrawalAmount); + } + + function test_ShouldAllowTheBEneficiaryToWithdrawUpToTheReleasedBalanceAtRevoke_WhenMaxDistributionIs100Percent_WhenRevoked() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + uint256 beneficiaryBalanceBefore = goldToken.balanceOf(initParams._beneficiary); + vm.warp(block.timestamp + 6 * MONTH + 1 * DAY); + vm.prank(releaseOwner); + releaseGold.revoke(); + (, , uint256 expectedWithdrawalAmount, ) = releaseGold.revocationInfo(); + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount); + uint256 totalWithdrawn = releaseGold.totalWithdrawn(); + uint256 beneficiaryBalanceAfter = goldToken.balanceOf(initParams._beneficiary); + + assertEq(totalWithdrawn, expectedWithdrawalAmount); + assertEq(beneficiaryBalanceAfter - beneficiaryBalanceBefore, expectedWithdrawalAmount); + } + + function test_ShouldRevertIfBeneficiaryAttemptsToWitdrawMOreThanReleasedBAlanceAtRevoke_WhenMaxDistributionIs100Percent_WhenRevoked() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + vm.warp(block.timestamp + 6 * MONTH + 1 * DAY); + vm.prank(releaseOwner); + releaseGold.revoke(); + + (, , uint256 expectedWithdrawalAmount, ) = releaseGold.revocationInfo(); + vm.expectRevert("Requested amount is greater than available released funds"); + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount + 1); + } + + function test_ShouldAllowWithdrawalOf50Percent_WhenMaxDistributionIs50Percent_WhenMaxDistributionIsSetLower() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(500); + + uint256 beneficiaryBalanceBefore = goldToken.balanceOf(initParams._beneficiary); + vm.warp(block.timestamp + 12 * MONTH + 1 * DAY); + uint256 expectedWithdrawalAmount = initialReleaseGoldAmount / 2; + + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount); + + assertEq(expectedWithdrawalAmount, releaseGold.totalWithdrawn(), "Incorrect withdrawalAmount"); + + uint256 beneficiaryBalanceAfter = goldToken.balanceOf(initParams._beneficiary); + assertEq(beneficiaryBalanceAfter - beneficiaryBalanceBefore, expectedWithdrawalAmount); + } + + function test_ShouldRevertWhenWithdrawingMoreThan50Percent_WhenMaxDistributionIs50Percent_WhenMaxDistributionIsSetLower() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(500); + + vm.warp(block.timestamp + 12 * MONTH + 1 * DAY); + uint256 expectedWithdrawalAmount = initialReleaseGoldAmount / 2 + 1; + + vm.expectRevert("Requested amount exceeds current alloted maximum distribution"); + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount); + } + + function test_ShouldAllowWithdrawalOf100Percent_WhenMaxDistributionIs100Percent_WhenMaxDistributionIsSetLower() + public + { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + uint256 beneficiaryBalanceBefore = goldToken.balanceOf(initParams._beneficiary); + vm.warp(block.timestamp + 12 * MONTH + 1 * DAY); + uint256 expectedWithdrawalAmount = initialReleaseGoldAmount; + + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount); + + assertEq(expectedWithdrawalAmount, releaseGold.totalWithdrawn(), "Incorrect withdrawalAmount"); + + uint256 beneficiaryBalanceAfter = goldToken.balanceOf(initParams._beneficiary); + assertEq(beneficiaryBalanceAfter - beneficiaryBalanceBefore, expectedWithdrawalAmount); + } + + function test_ShouldRevertOnWithdrawalOfAnyAmount_WhenLiquidityProvisionIsObservedAndSetFalse() + public + { + initParams2.subjectToLiquidityProvision = true; + newReleaseGold(true, false); + vm.warp(block.timestamp + 12 * MONTH + 1 * DAY); + + vm.expectRevert("Requested withdrawal before liquidity provision is met"); + vm.prank(beneficiary); + releaseGold.withdraw(initialReleaseGoldAmount); + + vm.expectRevert("Requested withdrawal before liquidity provision is met"); + vm.prank(beneficiary); + releaseGold.withdraw(initialReleaseGoldAmount / 2); + } +} + +contract WithdrawSelfDestruct_WhenNotRevoked is ReleaseGoldTest { + uint256 initialReleaseGoldAmount; + + function setUp() public { + super.setUp(); + + initParams2.initialDistributionRatio = 0; + initialReleaseGoldAmount = initParams.amountReleasedPerPeriod * initParams.numReleasePeriods; + newReleaseGold(true, false); + + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + vm.warp(block.timestamp + 12 * MONTH + 1 * DAY); + uint256 expectedWithdrawalAmount = initialReleaseGoldAmount; + + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount); + } + + function test_ShouldSelfDestructIfBeneficiaryWithdrawsTheEntireAmount() public { + vm.expectRevert(); + releaseGold.totalWithdrawn(); + } +} + +contract WithdrawSelfDestruct_WhenRevoked is ReleaseGoldTest { + uint256 initialReleaseGoldAmount; + + function setUp() public { + super.setUp(); + + initParams2.initialDistributionRatio = 0; + initialReleaseGoldAmount = initParams.amountReleasedPerPeriod * initParams.numReleasePeriods; + newReleaseGold(true, false); + + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + vm.warp(block.timestamp + 12 * MONTH + 1 * DAY); + vm.prank(releaseOwner); + releaseGold.revoke(); + + (, , uint256 expectedWithdrawalAmount, ) = releaseGold.revocationInfo(); + vm.prank(beneficiary); + releaseGold.withdraw(expectedWithdrawalAmount); + } + + function test_ShouldSelfDestructIfBeneficiaryWithdrawsTheEntireAmount() public { + vm.expectRevert(); + releaseGold.totalWithdrawn(); + } +} + +contract GetCurrentReleasedTotalAmount is ReleaseGoldTest { + uint256 initialReleaseGoldAmount; + + function setUp() public { + super.setUp(); + newReleaseGold(true, false); + initialReleaseGoldAmount = initParams.amountReleasedPerPeriod * initParams.numReleasePeriods; + } + + function test_ShouldReturnZeroIfBeforeCliffStartTime() public { + vm.warp(block.timestamp + 1); + assertEq(releaseGold.getCurrentReleasedTotalAmount(), 0); + } + + function test_ShouldReturn25PercentOfReleasedAmountOfGoldRightAfterTheBeginningOfTheFirstQuarter() + public + { + vm.warp(block.timestamp + 3 * MONTH + 1 * DAY); + assertEq(releaseGold.getCurrentReleasedTotalAmount(), initialReleaseGoldAmount / 4); + } + + function test_ShouldReturn50PercentOfReleasedAmountOfGoldRightAfterTheBeginningOfTheSecondQuarter() + public + { + vm.warp(block.timestamp + 6 * MONTH + 1 * DAY); + assertEq(releaseGold.getCurrentReleasedTotalAmount(), initialReleaseGoldAmount / 2); + } + + function test_ShouldReturn75PercentOfReleasedAmountOfGoldRightAfterTheBeginningOfTheThirdQuarter() + public + { + vm.warp(block.timestamp + 9 * MONTH + 1 * DAY); + assertEq(releaseGold.getCurrentReleasedTotalAmount(), (initialReleaseGoldAmount / 4) * 3); + } + + function test_ShouldReturn100PercentOfReleasedAmountOfGoldRightAfterTheBeginningOfTheFourthQuarter() + public + { + vm.warp(block.timestamp + 12 * MONTH + 1 * DAY); + assertEq(releaseGold.getCurrentReleasedTotalAmount(), initialReleaseGoldAmount); + } +} + +contract GetWithdrawableAmount is ReleaseGoldTest { + uint256 initialReleaseGoldAmount; + + function setUp() public { + super.setUp(); + initParams2._canValidate = true; + initParams.revocable = false; + initParams2._refundAddress = address(0); + initParams2.initialDistributionRatio = 500; + + newReleaseGold(true, false); + initialReleaseGoldAmount = initParams.amountReleasedPerPeriod * initParams.numReleasePeriods; + } + + function test_ShouldReturnFullAmountAvailableForThisReleasePeriod() public { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + vm.warp(block.timestamp + 6 * MONTH + 1 * DAY); + assertEq(releaseGold.getWithdrawableAmount(), initialReleaseGoldAmount / 2); + } + + function test_ShouldReturnOnlyAmountNotYetWithdrawn() public { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + vm.warp(block.timestamp + 6 * MONTH + 1 * DAY); + vm.prank(beneficiary); + releaseGold.withdraw(initialReleaseGoldAmount / 4); + assertEq(releaseGold.getWithdrawableAmount(), initialReleaseGoldAmount / 4); + } + + function test_ShouldReturnOnlyUpToItsOwnBalance() public { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(1000); + + vm.prank(beneficiary); + releaseGold.createAccount(); + + vm.warp(block.timestamp + 6 * MONTH + 1 * DAY); + + uint256 signerFund = 1 ether; + uint256 expectedWithdrawalAmount = (initialReleaseGoldAmount - signerFund) / 2; + + (address authorized, uint256 authorizedPK) = actorWithPK("authorized"); + (uint8 v, bytes32 r, bytes32 s) = getParsedSignatureOfAddress( + address(releaseGold), + authorizedPK + ); + bytes memory ecdsaPublicKey = addressToPublicKey( + keccak256(abi.encodePacked("dummy_msg_data")), + v, + r, + s + ); + + vm.prank(beneficiary); + releaseGold.authorizeValidatorSignerWithPublicKey( + address(uint160(authorized)), + v, + r, + s, + ecdsaPublicKey + ); + + assertEq(releaseGold.getWithdrawableAmount(), expectedWithdrawalAmount); + } + + function test_ShouldReturnOnlyUpToMaxDistribution() public { + vm.prank(releaseOwner); + releaseGold.setMaxDistribution(250); + + vm.warp(block.timestamp + 6 * MONTH + 1 * DAY); + assertEq(releaseGold.getWithdrawableAmount(), initialReleaseGoldAmount / 4); + } +} diff --git a/packages/protocol/test/governance/voting/release_gold.ts b/packages/protocol/test/governance/voting/release_gold.ts deleted file mode 100644 index 3c49c1555c0..00000000000 --- a/packages/protocol/test/governance/voting/release_gold.ts +++ /dev/null @@ -1,2284 +0,0 @@ -import { NULL_ADDRESS } from '@celo/base/lib/address' -import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' -import { - assertEqualBN, - assertGteBN, - assertLogMatches, - assertSameAddress, - // eslint-disable-next-line: ordered-imports - assertTransactionRevertWithReason, - assertTransactionRevertWithoutReason, - expectBigNumberInRange, - timeTravel, -} from '@celo/protocol/lib/test-utils' -// eslint-disable-next-line: ordered-imports -import { Signature, addressToPublicKey } from '@celo/utils/lib/signatureUtils' -import { BigNumber } from 'bignumber.js' -import _ from 'lodash' -import { - AccountsContract, - AccountsInstance, - FreezerContract, - FreezerInstance, - GoldTokenContract, - GoldTokenInstance, - LockedGoldContract, - LockedGoldInstance, - MockElectionContract, - MockElectionInstance, - MockGovernanceContract, - MockGovernanceInstance, - MockStableTokenContract, - MockStableTokenInstance, - MockValidatorsContract, - MockValidatorsInstance, - RegistryContract, - RegistryInstance, - ReleaseGoldContract, - ReleaseGoldInstance, -} from 'types' -import Web3 from 'web3' - -const ONE_GOLDTOKEN = new BigNumber('1000000000000000000') - -const authorizationTests: any = {} -const authorizationTestDescriptions = { - voting: { - me: 'vote signing key', - subject: 'voteSigner', - }, - validating: { - me: 'validating signing key', - subject: 'validatorSigner', - }, - attestation: { - me: 'attestation signing key', - subject: 'attestationSigner', - }, -} - -const isTest = true - -interface ReleaseGoldConfig { - releaseStartTime: number - releaseCliffTime: number - numReleasePeriods: number - releasePeriod: number - amountReleasedPerPeriod: BigNumber - revocable: boolean - beneficiary: string - releaseOwner: string - refundAddress: string - subjectToLiquidityProvision: boolean - initialDistributionRatio: number - canValidate: boolean - canVote: boolean -} - -interface ReleaseSchedule { - // Timestamp (in UNIX time) that releasing begins. - releaseStartTime: number - // Timestamp (in UNIX time) of the releasing cliff. - releaseCliff: number - // Number of release periods. - numReleasePeriods: BigNumber - // Duration (in seconds) of one period. - releasePeriod: number - // Amount that is to be released per period. - amountReleasedPerPeriod: number -} -interface RevocationInfo { - // Indicates if the contract is revocable. - revocable: boolean - // Indicates if the contract can expire `EXPIRATION_TIME` after releasing finishes. - canExpire: boolean - // Released gold instance balance at time of revocation. - releasedBalanceAtRevoke: BigNumber - // The time at which the release schedule was revoked. - revokeTime: number -} - -const Accounts: AccountsContract = artifacts.require('Accounts') -const Freezer: FreezerContract = artifacts.require('Freezer') -const GoldToken: GoldTokenContract = artifacts.require('GoldToken') -const LockedGold: LockedGoldContract = artifacts.require('LockedGold') -const MockStableToken: MockStableTokenContract = artifacts.require('MockStableToken') -const MockElection: MockElectionContract = artifacts.require('MockElection') -const MockGovernance: MockGovernanceContract = artifacts.require('MockGovernance') -const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') -const Registry: RegistryContract = artifacts.require('Registry') -const ReleaseGold: ReleaseGoldContract = artifacts.require('ReleaseGold') - -// @ts-ignore -// TODO(mcortesi): Use BN -LockedGold.numberFormat = 'BigNumber' -// @ts-ignore -ReleaseGold.numberFormat = 'BigNumber' -// @ts-ignore -MockStableToken.numberFormat = 'BigNumber' -// @ts-ignore -GoldToken.numberFormat = 'BigNumber' - -const MINUTE = 60 -const HOUR = 60 * 60 -const DAY = 24 * HOUR -const MONTH = 30 * DAY -const UNLOCKING_PERIOD = 3 * DAY - -contract('ReleaseGold', (accounts: string[]) => { - const owner = accounts[0] - const beneficiary = accounts[1] - const walletAddress = beneficiary - - const releaseOwner = accounts[2] - const refundAddress = accounts[3] - const newBeneficiary = accounts[4] - let accountsInstance: AccountsInstance - let freezerInstance: FreezerInstance - let goldTokenInstance: GoldTokenInstance - let lockedGoldInstance: LockedGoldInstance - let mockElection: MockElectionInstance - let mockGovernance: MockGovernanceInstance - let mockValidators: MockValidatorsInstance - let mockStableToken: MockStableTokenInstance - let registry: RegistryInstance - let releaseGoldInstance: ReleaseGoldInstance - let proofOfWalletOwnership: Signature - const TOTAL_AMOUNT = ONE_GOLDTOKEN.times(10) - - const releaseGoldDefaultSchedule: ReleaseGoldConfig = { - releaseStartTime: null, // To be adjusted on every run - releaseCliffTime: HOUR, - numReleasePeriods: 4, - releasePeriod: 3 * MONTH, - amountReleasedPerPeriod: TOTAL_AMOUNT.div(4), - revocable: true, - beneficiary, - releaseOwner, - refundAddress, - subjectToLiquidityProvision: false, - initialDistributionRatio: 1000, // No distribution limit - canVote: true, - canValidate: false, - } - - const createNewReleaseGoldInstance = async ( - releaseGoldSchedule: ReleaseGoldConfig, - web3: Web3, - override = { - prefund: true, - startReleasing: false, - } - ) => { - const startDelay = 5 * MINUTE - releaseGoldSchedule.releaseStartTime = (await getCurrentBlockchainTimestamp(web3)) + startDelay - releaseGoldInstance = await ReleaseGold.new(isTest) - if (override.prefund) { - await goldTokenInstance.transfer( - releaseGoldInstance.address, - releaseGoldSchedule.amountReleasedPerPeriod.multipliedBy( - releaseGoldSchedule.numReleasePeriods - ), - { - from: owner, - } - ) - } - await releaseGoldInstance.initialize( - releaseGoldSchedule.releaseStartTime, - releaseGoldSchedule.releaseCliffTime, - releaseGoldSchedule.numReleasePeriods, - releaseGoldSchedule.releasePeriod, - releaseGoldSchedule.amountReleasedPerPeriod, - releaseGoldSchedule.revocable, - releaseGoldSchedule.beneficiary, - releaseGoldSchedule.releaseOwner, - releaseGoldSchedule.refundAddress, - releaseGoldSchedule.subjectToLiquidityProvision, - releaseGoldSchedule.initialDistributionRatio, - releaseGoldSchedule.canValidate, - releaseGoldSchedule.canVote, - registry.address, - { from: owner } - ) - if (override.startReleasing) { - await timeTravel( - startDelay + releaseGoldSchedule.releaseCliffTime + releaseGoldSchedule.releasePeriod, - web3 - ) - } - } - - const getCurrentBlockchainTimestamp = (web3: Web3): Promise => - web3.eth.getBlock('latest').then((block) => Number(block.timestamp)) - - beforeEach(async () => { - accountsInstance = await Accounts.new(true) - freezerInstance = await Freezer.new(true) - goldTokenInstance = await GoldToken.new(true) - lockedGoldInstance = await LockedGold.new(true) - mockElection = await MockElection.new() - mockGovernance = await MockGovernance.new() - mockValidators = await MockValidators.new() - mockStableToken = await MockStableToken.new() - - registry = await Registry.new(true) - await registry.setAddressFor(CeloContractName.Accounts, accountsInstance.address) - await registry.setAddressFor(CeloContractName.Election, mockElection.address) - await registry.setAddressFor(CeloContractName.Freezer, freezerInstance.address) - await registry.setAddressFor(CeloContractName.GoldToken, goldTokenInstance.address) - await registry.setAddressFor(CeloContractName.Governance, mockGovernance.address) - await registry.setAddressFor(CeloContractName.LockedGold, lockedGoldInstance.address) - await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) - await registry.setAddressFor(CeloContractName.StableToken, mockStableToken.address) - await lockedGoldInstance.initialize(registry.address, UNLOCKING_PERIOD) - await goldTokenInstance.initialize(registry.address) - await accountsInstance.initialize(registry.address) - await accountsInstance.createAccount({ from: beneficiary }) - }) - - describe('#initialize', () => { - it('should indicate isFunded() if deployment is prefunded', async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3, { - prefund: true, - startReleasing: false, - }) - const isFunded = await releaseGoldInstance.isFunded() - assert.isTrue(isFunded) - }) - - it('should not indicate isFunded() (and not revert) if deployment is not prefunded', async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3, { - prefund: false, - startReleasing: false, - }) - const isFunded = await releaseGoldInstance.isFunded() - assert.isFalse(isFunded) - }) - }) - - describe('#payable', () => { - it('should accept gold transfer by default from anyone', async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - await goldTokenInstance.transfer(releaseGoldInstance.address, ONE_GOLDTOKEN.times(2), { - from: accounts[8], - }) - }) - - it('should not update isFunded() if schedule principle not fulfilled', async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3, { - prefund: false, - startReleasing: false, - }) - const insufficientPrinciple = releaseGoldDefaultSchedule.amountReleasedPerPeriod - .multipliedBy(releaseGoldDefaultSchedule.numReleasePeriods) - .minus(1) - await goldTokenInstance.transfer(releaseGoldInstance.address, insufficientPrinciple, { - from: owner, - }) - const isFunded = await releaseGoldInstance.isFunded() - assert.isFalse(isFunded) - }) - - it('should update isFunded() if schedule principle is fulfilled after deployment', async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3, { - prefund: false, - startReleasing: false, - }) - const sufficientPrinciple = releaseGoldDefaultSchedule.amountReleasedPerPeriod.multipliedBy( - releaseGoldDefaultSchedule.numReleasePeriods - ) - await goldTokenInstance.transfer(releaseGoldInstance.address, sufficientPrinciple, { - from: owner, - }) - const isFunded = await releaseGoldInstance.isFunded() - assert.isTrue(isFunded) - }) - - it('should update isFunded() if schedule principle not fulfilled but has begun releasing', async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3, { - prefund: false, - startReleasing: true, - }) - const insufficientPrinciple = releaseGoldDefaultSchedule.amountReleasedPerPeriod - .multipliedBy(releaseGoldDefaultSchedule.numReleasePeriods) - .minus(1) - await goldTokenInstance.transfer(releaseGoldInstance.address, insufficientPrinciple, { - from: owner, - }) - const isFunded = await releaseGoldInstance.isFunded() - assert.isTrue(isFunded) - }) - }) - - describe('#transfer', () => { - const receiver = accounts[5] - const transferAmount = 10 - - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - await mockStableToken.mint(releaseGoldInstance.address, transferAmount) - }) - - it('should transfer stable token from the release gold instance', async () => { - await releaseGoldInstance.transfer(receiver, transferAmount, { from: beneficiary }) - const contractBalance = await mockStableToken.balanceOf(releaseGoldInstance.address) - const recipientBalance = await mockStableToken.balanceOf(receiver) - assertEqualBN(contractBalance, 0) - assertEqualBN(recipientBalance, transferAmount) - }) - }) - - describe('#genericTransfer', () => { - const receiver = accounts[5] - const transferAmount = 10 - - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - await mockStableToken.mint(releaseGoldInstance.address, transferAmount) - }) - - it('should transfer stable token from the release gold instance', async () => { - const startBalanceFrom = await mockStableToken.balanceOf(releaseGoldInstance.address) - const startBalanceTo = await mockStableToken.balanceOf(receiver) - await releaseGoldInstance.genericTransfer(mockStableToken.address, receiver, transferAmount, { - from: beneficiary, - }) - const endBalanceFrom = await mockStableToken.balanceOf(releaseGoldInstance.address) - const endBalanceTo = await mockStableToken.balanceOf(receiver) - assertEqualBN(endBalanceFrom, startBalanceFrom.minus(transferAmount)) - assertEqualBN(endBalanceTo, startBalanceTo.plus(transferAmount)) - }) - - it('should emit safeTransfer logs on erc20 revert', async () => { - const startBalanceFrom = await mockStableToken.balanceOf(releaseGoldInstance.address) - await assertTransactionRevertWithReason( - releaseGoldInstance.genericTransfer( - mockStableToken.address, - receiver, - startBalanceFrom.plus(1), - { - from: beneficiary, - } - ), - 'SafeERC20: ERC20 operation did not succeed' - ) - }) - - it('should revert when attempting transfer of goldtoken from the release gold instance', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.genericTransfer(goldTokenInstance.address, receiver, transferAmount, { - from: beneficiary, - }), - 'Transfer must not target celo balance' - ) - }) - }) - - describe('#creation', () => { - describe('when an instance is properly created', () => { - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - }) - - it('should have associated funds with a schedule upon creation', async () => { - const allocatedFunds = await goldTokenInstance.balanceOf(releaseGoldInstance.address) - assertEqualBN( - allocatedFunds, - new BigNumber(releaseGoldDefaultSchedule.numReleasePeriods).multipliedBy( - releaseGoldDefaultSchedule.amountReleasedPerPeriod - ) - ) - }) - - it('should set a beneficiary to releaseGold instance', async () => { - const releaseGoldBeneficiary = await releaseGoldInstance.beneficiary() - assert.equal(releaseGoldBeneficiary, releaseGoldDefaultSchedule.beneficiary) - }) - - it('should set a releaseOwner to releaseGold instance', async () => { - const releaseGoldOwner = await releaseGoldInstance.releaseOwner() - assert.equal(releaseGoldOwner, releaseGoldDefaultSchedule.releaseOwner) - }) - - it('should set releaseGold number of periods to releaseGold instance', async () => { - const { numReleasePeriods } = - (await releaseGoldInstance.releaseSchedule()) as unknown as ReleaseSchedule - assertEqualBN(numReleasePeriods, releaseGoldDefaultSchedule.numReleasePeriods) - }) - - it('should set releaseGold amount per period to releaseGold instance', async () => { - const { amountReleasedPerPeriod } = - (await releaseGoldInstance.releaseSchedule()) as unknown as ReleaseSchedule - assertEqualBN(amountReleasedPerPeriod, releaseGoldDefaultSchedule.amountReleasedPerPeriod) - }) - - it('should set releaseGold period to releaseGold instance', async () => { - const { releasePeriod } = - (await releaseGoldInstance.releaseSchedule()) as unknown as ReleaseSchedule - assertEqualBN(releasePeriod, releaseGoldDefaultSchedule.releasePeriod) - }) - - it('should set releaseGold start time to releaseGold instance', async () => { - const { releaseStartTime } = - (await releaseGoldInstance.releaseSchedule()) as unknown as ReleaseSchedule - assertEqualBN(releaseStartTime, releaseGoldDefaultSchedule.releaseStartTime) - }) - - it('should set releaseGold cliff to releaseGold instance', async () => { - const { releaseCliff } = - (await releaseGoldInstance.releaseSchedule()) as unknown as ReleaseSchedule - const releaseGoldCliffStartTimeComputed = new BigNumber( - releaseGoldDefaultSchedule.releaseStartTime - ).plus(releaseGoldDefaultSchedule.releaseCliffTime) - assertEqualBN(releaseCliff, releaseGoldCliffStartTimeComputed) - }) - - it('should set revocable flag to releaseGold instance', async () => { - const revocationInfo = await releaseGoldInstance.revocationInfo() - assert.equal(revocationInfo[0], releaseGoldDefaultSchedule.revocable) - }) - - it('should set releaseOwner to releaseGold instance', async () => { - const releaseGoldOwner = await releaseGoldInstance.releaseOwner() - assert.equal(releaseGoldOwner, releaseGoldDefaultSchedule.releaseOwner) - }) - - it('should set liquidity provision met to true', async () => { - const liquidityProvisionMet = await releaseGoldInstance.liquidityProvisionMet() - assert.equal(liquidityProvisionMet, true) - }) - - it('should have zero total withdrawn on init', async () => { - const totalWithdrawn = await releaseGoldInstance.totalWithdrawn() - assertEqualBN(totalWithdrawn, 0) - }) - - it('should be unrevoked on init and have revoke time equal zero', async () => { - const isRevoked = await releaseGoldInstance.isRevoked() - assert.equal(isRevoked, false) - const { revokeTime } = - (await releaseGoldInstance.revocationInfo()) as unknown as RevocationInfo - assertEqualBN(revokeTime, 0) - }) - - it('should have releaseGoldBalanceAtRevoke on init equal to zero', async () => { - const { releasedBalanceAtRevoke } = - (await releaseGoldInstance.revocationInfo()) as unknown as RevocationInfo - assertEqualBN(releasedBalanceAtRevoke, 0) - }) - - it('should revert when releaseGold beneficiary is the null address', async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.beneficiary = NULL_ADDRESS - await assertTransactionRevertWithReason( - createNewReleaseGoldInstance(releaseGoldSchedule, web3), - 'The release schedule beneficiary cannot be the zero addresss' - ) - }) - - it('should revert when releaseGold periods are zero', async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.numReleasePeriods = 0 - await assertTransactionRevertWithReason( - createNewReleaseGoldInstance(releaseGoldSchedule, web3), - 'There must be at least one releasing period' - ) - }) - - it('should revert when released amount per period is zero', async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.amountReleasedPerPeriod = new BigNumber('0') - await assertTransactionRevertWithReason( - createNewReleaseGoldInstance(releaseGoldSchedule, web3), - 'The released amount per period must be greater than zero' - ) - }) - - it('should overflow for very large combinations of release periods and amount per time', async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.numReleasePeriods = Number.MAX_SAFE_INTEGER - releaseGoldSchedule.amountReleasedPerPeriod = new BigNumber(2).pow(300) - await assertTransactionRevertWithReason( - createNewReleaseGoldInstance(releaseGoldSchedule, web3), - 'value out-of-bounds' - ) - }) - }) - }) - - describe('#setBeneficiary', () => { - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - }) - - it('should set a new beneficiary as the old beneficiary', async () => { - await releaseGoldInstance.setBeneficiary(newBeneficiary, { from: owner }) - const actualBeneficiary = await releaseGoldInstance.beneficiary() - assertSameAddress(actualBeneficiary, newBeneficiary) - }) - - it('should revert when setting a new beneficiary from the release owner', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setBeneficiary(newBeneficiary, { from: releaseOwner }), - 'Ownable: caller is not the owner' - ) - }) - - it('should emit the BeneficiarySet event', async () => { - const setNewBeneficiaryTx = await releaseGoldInstance.setBeneficiary(newBeneficiary, { - from: owner, - }) - assertLogMatches(setNewBeneficiaryTx.logs[0], 'BeneficiarySet', { - beneficiary: newBeneficiary, - }) - }) - }) - - describe('#createAccount', () => { - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - }) - - describe('when unrevoked', () => { - it('creates the account by beneficiary', async () => { - let isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isFalse(isAccount) - await releaseGoldInstance.createAccount({ from: beneficiary }) - isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isTrue(isAccount) - }) - - it('reverts if a non-beneficiary attempts account creation', async () => { - const isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isFalse(isAccount) - await assertTransactionRevertWithReason( - releaseGoldInstance.createAccount({ from: accounts[2] }), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - }) - - describe('when revoked', () => { - beforeEach(async () => { - await releaseGoldInstance.revoke({ from: releaseOwner }) - }) - - it('reverts if anyone attempts account creation', async () => { - const isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isFalse(isAccount) - await assertTransactionRevertWithReason( - releaseGoldInstance.createAccount({ from: beneficiary }), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - }) - }) - - describe('#setAccount', () => { - const accountName = 'name' - const dataEncryptionKey: any = - '0x02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' - - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - proofOfWalletOwnership = await getParsedSignatureOfAddress( - web3, - releaseGoldInstance.address, - beneficiary - ) - }) - - describe('when unrevoked', () => { - it('sets the account by beneficiary', async () => { - let isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isFalse(isAccount) - await releaseGoldInstance.setAccount( - accountName, - dataEncryptionKey, - walletAddress, - proofOfWalletOwnership.v, - proofOfWalletOwnership.r, - proofOfWalletOwnership.s, - { - from: beneficiary, - } - ) - isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isTrue(isAccount) - }) - - it('reverts if a non-beneficiary attempts to set the account', async () => { - const isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isFalse(isAccount) - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccount( - accountName, - dataEncryptionKey, - walletAddress, - proofOfWalletOwnership.v, - proofOfWalletOwnership.r, - proofOfWalletOwnership.s, - { - from: accounts[2], - } - ), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - - it('should set the name, dataEncryptionKey and walletAddress of the account by beneficiary', async () => { - let isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isFalse(isAccount) - await releaseGoldInstance.setAccount( - accountName, - dataEncryptionKey, - walletAddress, - proofOfWalletOwnership.v, - proofOfWalletOwnership.r, - proofOfWalletOwnership.s, - { - from: beneficiary, - } - ) - isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isTrue(isAccount) - const expectedWalletAddress = await accountsInstance.getWalletAddress( - releaseGoldInstance.address - ) - assert.equal(expectedWalletAddress, walletAddress) - // @ts-ignore - const expectedKey: string = await accountsInstance.getDataEncryptionKey( - releaseGoldInstance.address - ) - assert.equal(expectedKey, dataEncryptionKey) - const expectedName = await accountsInstance.getName(releaseGoldInstance.address) - assert.equal(expectedName, accountName) - }) - - it('should revert to set the name, dataEncryptionKey and walletAddress of the account by a non-beneficiary', async () => { - const isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isFalse(isAccount) - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccount( - accountName, - dataEncryptionKey, - walletAddress, - proofOfWalletOwnership.v, - proofOfWalletOwnership.r, - proofOfWalletOwnership.s, - { - from: releaseOwner, - } - ), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - }) - - describe('when revoked', () => { - beforeEach(async () => { - await releaseGoldInstance.revoke({ from: releaseOwner }) - }) - - it('reverts if anyone attempts to set the account', async () => { - const isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isFalse(isAccount) - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccount( - accountName, - dataEncryptionKey, - walletAddress, - proofOfWalletOwnership.v, - proofOfWalletOwnership.r, - proofOfWalletOwnership.s, - { - from: releaseOwner, - } - ), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - - it('should revert to set the name, dataEncryptionKey and walletAddress of the account', async () => { - const isAccount = await accountsInstance.isAccount(releaseGoldInstance.address) - assert.isFalse(isAccount) - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccount( - accountName, - dataEncryptionKey, - walletAddress, - proofOfWalletOwnership.v, - proofOfWalletOwnership.r, - proofOfWalletOwnership.s, - { - from: releaseOwner, - } - ), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - }) - }) - - describe('#setAccountName', () => { - const accountName = 'name' - - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - }) - - describe('when the account has not been created', () => { - it('should revert', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccountName(accountName, { from: beneficiary }), - 'Register with createAccount to set account name' - ) - }) - }) - - describe('when the account has been created', () => { - beforeEach(async () => { - await releaseGoldInstance.createAccount({ from: beneficiary }) - }) - - describe('when unrevoked', () => { - it('beneficiary should set the name', async () => { - await releaseGoldInstance.setAccountName(accountName, { from: beneficiary }) - const result = await accountsInstance.getName(releaseGoldInstance.address) - assert.equal(result, accountName) - }) - - it('should revert if non-beneficiary attempts to set the name', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccountName(accountName, { from: accounts[2] }), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - }) - - describe('when revoked', () => { - beforeEach(async () => { - await releaseGoldInstance.revoke({ from: releaseOwner }) - }) - - it('should revert if anyone attempts to set the name', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccountName(accountName, { from: releaseOwner }), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - }) - }) - }) - - describe('#setAccountWalletAddress', () => { - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - proofOfWalletOwnership = await getParsedSignatureOfAddress( - web3, - releaseGoldInstance.address, - beneficiary - ) - }) - - describe('when the releaseGold account has not been created', () => { - it('should revert', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccountWalletAddress( - walletAddress, - proofOfWalletOwnership.v, - proofOfWalletOwnership.r, - proofOfWalletOwnership.s, - { from: beneficiary } - ), - 'Unknown account' - ) - }) - }) - - describe('when the account has been created', () => { - beforeEach(async () => { - await releaseGoldInstance.createAccount({ from: beneficiary }) - }) - - describe('when unrevoked', () => { - it('beneficiary should set the walletAddress', async () => { - await releaseGoldInstance.setAccountWalletAddress( - walletAddress, - proofOfWalletOwnership.v, - proofOfWalletOwnership.r, - proofOfWalletOwnership.s, - { from: beneficiary } - ) - const result = await accountsInstance.getWalletAddress(releaseGoldInstance.address) - assert.equal(result, walletAddress) - }) - - it('should revert if non-beneficiary attempts to set the walletAddress', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccountWalletAddress( - walletAddress, - proofOfWalletOwnership.v, - proofOfWalletOwnership.r, - proofOfWalletOwnership.s, - { from: accounts[2] } - ), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - - it('beneficiary should set the NULL_ADDRESS', async () => { - await releaseGoldInstance.setAccountWalletAddress(NULL_ADDRESS, '0x0', '0x0', '0x0', { - from: beneficiary, - }) - const result = await accountsInstance.getWalletAddress(releaseGoldInstance.address) - assert.equal(result, NULL_ADDRESS) - }) - }) - - describe('when revoked', () => { - beforeEach(async () => { - await releaseGoldInstance.revoke({ from: releaseOwner }) - }) - - it('should revert if anyone attempts to set the walletAddress', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccountWalletAddress( - walletAddress, - proofOfWalletOwnership.v, - proofOfWalletOwnership.r, - proofOfWalletOwnership.s, - { from: releaseOwner } - ), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - }) - }) - }) - - describe('#setAccountMetadataURL', () => { - const metadataURL = 'meta' - - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - }) - - describe('when the account has not been created', () => { - it('should revert', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccountMetadataURL(metadataURL, { from: beneficiary }), - 'Unknown account' - ) - }) - }) - - describe('when the account has been created', () => { - beforeEach(async () => { - await releaseGoldInstance.createAccount({ from: beneficiary }) - }) - - describe('when unrevoked', () => { - it('only beneficiary should set the metadataURL', async () => { - await releaseGoldInstance.setAccountMetadataURL(metadataURL, { from: beneficiary }) - const result = await accountsInstance.getMetadataURL(releaseGoldInstance.address) - assert.equal(result, metadataURL) - }) - - it('should revert if non-beneficiary attempts to set the metadataURL', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccountMetadataURL(metadataURL, { from: accounts[2] }), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - }) - - describe('when revoked', () => { - beforeEach(async () => { - await releaseGoldInstance.revoke({ from: releaseOwner }) - }) - - it('should revert if anyone attempts to set the metadataURL', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccountMetadataURL(metadataURL, { from: releaseOwner }), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - }) - }) - }) - - describe('#setAccountDataEncryptionKey', () => { - const dataEncryptionKey: any = - '0x02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' - const longDataEncryptionKey: any = - '0x04f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' + - '02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' - - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - await releaseGoldInstance.createAccount({ from: beneficiary }) - }) - - it('beneficiary should set dataEncryptionKey', async () => { - await releaseGoldInstance.setAccountDataEncryptionKey(dataEncryptionKey, { - from: beneficiary, - }) - // @ts-ignore - const fetchedKey: string = await accountsInstance.getDataEncryptionKey( - releaseGoldInstance.address - ) - assert.equal(fetchedKey, dataEncryptionKey) - }) - - it('should revert if non-beneficiary attempts to set dataEncryptionKey', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccountDataEncryptionKey(dataEncryptionKey, { from: accounts[2] }), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - - it('should allow setting a key with leading zeros', async () => { - const keyWithZeros: any = - '0x00000000000000000000000000000000000000000000000f2f48ee19680706191111' - await releaseGoldInstance.setAccountDataEncryptionKey(keyWithZeros, { from: beneficiary }) - // @ts-ignore - const fetchedKey: string = await accountsInstance.getDataEncryptionKey( - releaseGoldInstance.address - ) - assert.equal(fetchedKey, keyWithZeros) - }) - - it('should revert when the key is invalid', async () => { - const invalidKey: any = '0x32132931293' - await assertTransactionRevertWithReason( - releaseGoldInstance.setAccountDataEncryptionKey(invalidKey, { from: beneficiary }), - 'data encryption key length <= 32' - ) - }) - - it('should allow a key that is longer than 33 bytes', async () => { - await releaseGoldInstance.setAccountDataEncryptionKey(longDataEncryptionKey, { - from: beneficiary, - }) - // @ts-ignore - const fetchedKey: string = await accountsInstance.getDataEncryptionKey( - releaseGoldInstance.address - ) - assert.equal(fetchedKey, longDataEncryptionKey) - }) - }) - - describe('#setMaxDistribution', () => { - beforeEach(async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.initialDistributionRatio = 0 - await createNewReleaseGoldInstance(releaseGoldSchedule, web3) - }) - - describe('when the max distribution is set to 50%', () => { - beforeEach(async () => { - await releaseGoldInstance.setMaxDistribution(500, { from: releaseOwner }) - }) - - it('should set max distribution to 5 gold', async () => { - const maxDistribution = await releaseGoldInstance.maxDistribution() - assertEqualBN(maxDistribution, TOTAL_AMOUNT.div(2)) - }) - }) - - describe('when the max distribution is set to 100%', () => { - beforeEach(async () => { - await releaseGoldInstance.setMaxDistribution(1000, { from: releaseOwner }) - }) - - it('should set max distribution to max uint256', async () => { - const maxDistribution = await releaseGoldInstance.maxDistribution() - assertGteBN(maxDistribution, TOTAL_AMOUNT) - }) - - it('cannot be lowered again', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.setMaxDistribution(500, { from: releaseOwner }), - 'Cannot set max distribution lower if already set to 1000' - ) - }) - }) - }) - type Key = keyof typeof authorizationTestDescriptions - - describe('authorization tests:', () => { - Object.keys(authorizationTestDescriptions).forEach((key0) => { - const key: Key = key0 as unknown as Key - let authorizationTest: any - const authorized = accounts[4] // the account that is to be authorized for whatever role - let sig: any - - describe(`#authorize${_.upperFirst(authorizationTestDescriptions[key].subject)}()`, () => { - beforeEach(async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.revocable = false - releaseGoldSchedule.refundAddress = '0x0000000000000000000000000000000000000000' - releaseGoldSchedule.canValidate = true - await createNewReleaseGoldInstance(releaseGoldSchedule, web3) - await releaseGoldInstance.createAccount({ from: beneficiary }) - - authorizationTests.voting = { - fn: releaseGoldInstance.authorizeVoteSigner, - eventName: 'VoteSignerAuthorized', - getAuthorizedFromAccount: accountsInstance.getVoteSigner, - authorizedSignerToAccount: accountsInstance.voteSignerToAccount, - } - authorizationTests.validating = { - fn: releaseGoldInstance.authorizeValidatorSigner, - eventName: 'ValidatorSignerAuthorized', - getAuthorizedFromAccount: accountsInstance.getValidatorSigner, - authorizedSignerToAccount: accountsInstance.validatorSignerToAccount, - } - authorizationTests.attestation = { - fn: releaseGoldInstance.authorizeAttestationSigner, - eventName: 'AttestationSignerAuthorized', - getAuthorizedFromAccount: accountsInstance.getAttestationSigner, - authorizedSignerToAccount: accountsInstance.attestationSignerToAccount, - } - authorizationTest = authorizationTests[key] - sig = await getParsedSignatureOfAddress(web3, releaseGoldInstance.address, authorized) - }) - - it(`should set the authorized ${authorizationTestDescriptions[key].me}`, async () => { - await authorizationTest.fn(authorized, sig.v, sig.r, sig.s, { from: beneficiary }) - assert.equal(await accountsInstance.authorizedBy(authorized), releaseGoldInstance.address) - assert.equal( - await authorizationTest.getAuthorizedFromAccount(releaseGoldInstance.address), - authorized - ) - assert.equal( - await authorizationTest.authorizedSignerToAccount(authorized), - releaseGoldInstance.address - ) - }) - - // The attestations signer does not send txs. - if (authorizationTestDescriptions[key].subject !== 'attestationSigner') { - it(`should transfer 1 CELO to the ${authorizationTestDescriptions[key].me}`, async () => { - const balance1 = await web3.eth.getBalance(authorized) - await authorizationTest.fn(authorized, sig.v, sig.r, sig.s, { from: beneficiary }) - const balance2 = await web3.eth.getBalance(authorized) - assertEqualBN(new BigNumber(balance2).minus(balance1), web3.utils.toWei('1')) - }) - } else { - it(`should not transfer 1 CELO to the ${authorizationTestDescriptions[key].me}`, async () => { - const balance1 = await web3.eth.getBalance(authorized) - await authorizationTest.fn(authorized, sig.v, sig.r, sig.s, { from: beneficiary }) - const balance2 = await web3.eth.getBalance(authorized) - assertEqualBN(new BigNumber(balance2).minus(balance1), 0) - }) - } - - it(`should revert if the ${authorizationTestDescriptions[key].me} is an account`, async () => { - await accountsInstance.createAccount({ from: authorized }) - await assertTransactionRevertWithReason( - authorizationTest.fn(authorized, sig.v, sig.r, sig.s, { from: beneficiary }), - 'Cannot re-authorize address or locked gold account for another account' - ) - }) - - it(`should revert if the ${authorizationTestDescriptions[key].me} is already authorized`, async () => { - const otherAccount = accounts[5] - const otherSig = await getParsedSignatureOfAddress( - web3, - releaseGoldInstance.address, - otherAccount - ) - await accountsInstance.createAccount({ from: otherAccount }) - await assertTransactionRevertWithReason( - authorizationTest.fn(otherAccount, otherSig.v, otherSig.r, otherSig.s, { - from: beneficiary, - }), - 'Cannot re-authorize address or locked gold account for another account' - ) - }) - - it('should revert if the signature is incorrect', async () => { - const nonVoter = accounts[5] - const incorrectSig = await getParsedSignatureOfAddress( - web3, - releaseGoldInstance.address, - nonVoter - ) - await assertTransactionRevertWithReason( - authorizationTest.fn(authorized, incorrectSig.v, incorrectSig.r, incorrectSig.s, { - from: beneficiary, - }), - 'Invalid signature' - ) - }) - - describe('when a previous authorization has been made', () => { - const newAuthorized = accounts[6] - let balance1: string - let newSig: any - beforeEach(async () => { - await authorizationTest.fn(authorized, sig.v, sig.r, sig.s, { from: beneficiary }) - newSig = await getParsedSignatureOfAddress( - web3, - releaseGoldInstance.address, - newAuthorized - ) - balance1 = await web3.eth.getBalance(newAuthorized) - await authorizationTest.fn(newAuthorized, newSig.v, newSig.r, newSig.s, { - from: beneficiary, - }) - }) - - it(`should set the new authorized ${authorizationTestDescriptions[key].me}`, async () => { - assert.equal( - await accountsInstance.authorizedBy(newAuthorized), - releaseGoldInstance.address - ) - assert.equal( - await authorizationTest.getAuthorizedFromAccount(releaseGoldInstance.address), - newAuthorized - ) - assert.equal( - await authorizationTest.authorizedSignerToAccount(newAuthorized), - releaseGoldInstance.address - ) - }) - - it(`should not transfer 1 CELO to the ${authorizationTestDescriptions[key].me}`, async () => { - const balance2 = await web3.eth.getBalance(newAuthorized) - assertEqualBN(new BigNumber(balance2).minus(balance1), 0) - }) - - it('should preserve the previous authorization', async () => { - assert.equal( - await accountsInstance.authorizedBy(authorized), - releaseGoldInstance.address - ) - }) - }) - }) - }) - }) - - describe('#authorizeWithPublicKeys', () => { - const authorized = accounts[4] // the account that is to be authorized for whatever role - - describe('with ECDSA public key', () => { - beforeEach(async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.revocable = false - releaseGoldSchedule.canValidate = true - releaseGoldSchedule.refundAddress = NULL_ADDRESS - await createNewReleaseGoldInstance(releaseGoldSchedule, web3) - await releaseGoldInstance.createAccount({ from: beneficiary }) - const ecdsaPublicKey = await addressToPublicKey(authorized, web3.eth.sign) - const sig = await getParsedSignatureOfAddress(web3, releaseGoldInstance.address, authorized) - await releaseGoldInstance.authorizeValidatorSignerWithPublicKey( - authorized, - sig.v, - sig.r, - sig.s, - ecdsaPublicKey as any, - { from: beneficiary } - ) - }) - - it('should set the authorized keys', async () => { - assert.equal(await accountsInstance.authorizedBy(authorized), releaseGoldInstance.address) - assert.equal( - await accountsInstance.getValidatorSigner(releaseGoldInstance.address), - authorized - ) - assert.equal( - await accountsInstance.validatorSignerToAccount(authorized), - releaseGoldInstance.address - ) - }) - }) - - describe('with bls keys', () => { - beforeEach(async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.revocable = false - releaseGoldSchedule.canValidate = true - releaseGoldSchedule.refundAddress = NULL_ADDRESS - await createNewReleaseGoldInstance(releaseGoldSchedule, web3) - await releaseGoldInstance.createAccount({ from: beneficiary }) - const ecdsaPublicKey = await addressToPublicKey(authorized, web3.eth.sign) - const newBlsPublicKey = web3.utils.randomHex(96) - const newBlsPoP = web3.utils.randomHex(48) - - const sig = await getParsedSignatureOfAddress(web3, releaseGoldInstance.address, authorized) - await releaseGoldInstance.authorizeValidatorSignerWithKeys( - authorized, - sig.v, - sig.r, - sig.s, - ecdsaPublicKey as any, - newBlsPublicKey, - newBlsPoP, - { from: beneficiary } - ) - }) - - it('should set the authorized keys', async () => { - assert.equal(await accountsInstance.authorizedBy(authorized), releaseGoldInstance.address) - assert.equal( - await accountsInstance.getValidatorSigner(releaseGoldInstance.address), - authorized - ) - assert.equal( - await accountsInstance.validatorSignerToAccount(authorized), - releaseGoldInstance.address - ) - }) - }) - }) - - describe('#revoke', () => { - it('releaseOwner should be able to revoke the releaseGold', async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - const releaseOwnereleaseGoldTx = await releaseGoldInstance.revoke({ from: releaseOwner }) - const revokeBlockTimestamp = await getCurrentBlockchainTimestamp(web3) - const { revokeTime } = - (await releaseGoldInstance.revocationInfo()) as unknown as RevocationInfo - assertEqualBN(revokeBlockTimestamp, revokeTime) - assert.isTrue(await releaseGoldInstance.isRevoked()) - assertLogMatches(releaseOwnereleaseGoldTx.logs[0], 'ReleaseScheduleRevoked', { - revokeTimestamp: revokeBlockTimestamp, - releasedBalanceAtRevoke: await releaseGoldInstance.getCurrentReleasedTotalAmount(), - }) - }) - - it('should revert when non-releaseOwner attempts to revoke the releaseGold', async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - await assertTransactionRevertWithReason( - releaseGoldInstance.revoke({ from: accounts[5] }), - 'Sender must be the registered releaseOwner address' - ) - }) - - it('should revert if releaseGold is already revoked', async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - await releaseGoldInstance.revoke({ from: releaseOwner }) - await assertTransactionRevertWithReason( - releaseGoldInstance.revoke({ from: releaseOwner }), - 'Release schedule instance must not already be revoked' - ) - }) - - it('should revert if releaseGold is non-revocable', async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.revocable = false - releaseGoldSchedule.refundAddress = '0x0000000000000000000000000000000000000000' - await createNewReleaseGoldInstance(releaseGoldSchedule, web3) - await assertTransactionRevertWithReason( - releaseGoldInstance.revoke({ from: releaseOwner }), - 'Release schedule instance must be revocable' - ) - }) - }) - - describe('#expire', () => { - describe('when the contract is expirable', () => { - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - }) - - describe('when called before expiration time has passed', () => { - it('should revert', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.expire({ from: releaseOwner }), - '`EXPIRATION_TIME` must have passed after the end of releasing' - ) - }) - }) - - describe('when the contract has finished releasing', () => { - beforeEach(async () => { - const { releasePeriod, numReleasePeriods } = - (await releaseGoldInstance.releaseSchedule()) as unknown as ReleaseSchedule - const grantTime = numReleasePeriods - .times(releasePeriod) - .plus(5 * MINUTE) - .toNumber() - await timeTravel(grantTime, web3) - }) - - it('should revert before `EXPIRATION_TIME` after release schedule end', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.expire({ from: releaseOwner }), - '`EXPIRATION_TIME` must have passed after the end of releasing' - ) - }) - - describe('when `EXPIRATION_TIME` has passed after release schedule completion', () => { - beforeEach(async () => { - const expirationTime = await releaseGoldInstance.EXPIRATION_TIME() - const timeToTravel = expirationTime.toNumber() - await timeTravel(timeToTravel, web3) - }) - describe('when not called by releaseOwner', () => { - it('should revert', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.expire(), - 'Sender must be the registered releaseOwner address' - ) - }) - }) - - describe('when called by releaseOwner', () => { - it('should succeed', async () => { - await releaseGoldInstance.expire({ from: releaseOwner }) - }) - }) - - describe('when an instance is expired', () => { - describe('when the beneficiary has not withdrawn any balance yet', () => { - beforeEach(async () => { - await releaseGoldInstance.expire({ from: releaseOwner }) - }) - - it('should revoke the contract', async () => { - const isRevoked = await releaseGoldInstance.isRevoked() - assert.equal(isRevoked, true) - }) - - it('should set the released balance at revocation to total withdrawn', async () => { - const { releasedBalanceAtRevoke } = - (await releaseGoldInstance.revocationInfo()) as unknown as RevocationInfo - // 0 gold withdrawn at this point - assertEqualBN(releasedBalanceAtRevoke, 0) - }) - - it('should allow refund of all remaining gold', async () => { - const refundAddressBalanceBefore = await goldTokenInstance.balanceOf(refundAddress) - await releaseGoldInstance.refundAndFinalize({ from: releaseOwner }) - const refundAddressBalanceAfter = await goldTokenInstance.balanceOf(refundAddress) - assertEqualBN( - refundAddressBalanceAfter.minus(refundAddressBalanceBefore), - TOTAL_AMOUNT - ) - }) - }) - - describe('when the beneficiary has withdrawn some balance', () => { - beforeEach(async () => { - await releaseGoldInstance.withdraw(TOTAL_AMOUNT.div(2), { from: beneficiary }) - await releaseGoldInstance.expire({ from: releaseOwner }) - }) - - it('should revoke the contract', async () => { - const isRevoked = await releaseGoldInstance.isRevoked() - assert.equal(isRevoked, true) - }) - - it('should set the released balance at revocation to total withdrawn', async () => { - const { releasedBalanceAtRevoke } = - (await releaseGoldInstance.revocationInfo()) as unknown as RevocationInfo - // half of gold withdrawn at this point - assertEqualBN(releasedBalanceAtRevoke, TOTAL_AMOUNT.div(2)) - }) - - it('should allow refund of all remaining gold', async () => { - const refundAddressBalanceBefore = await goldTokenInstance.balanceOf(refundAddress) - await releaseGoldInstance.refundAndFinalize({ from: releaseOwner }) - const refundAddressBalanceAfter = await goldTokenInstance.balanceOf(refundAddress) - assertEqualBN( - refundAddressBalanceAfter.minus(refundAddressBalanceBefore), - TOTAL_AMOUNT.div(2) - ) - }) - }) - }) - }) - }) - }) - - describe('when the contract is not expirable', () => { - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - await releaseGoldInstance.setCanExpire(false, { from: beneficiary }) - const { numReleasePeriods, releasePeriod } = - (await releaseGoldInstance.releaseSchedule()) as unknown as ReleaseSchedule - const expirationTime = await releaseGoldInstance.EXPIRATION_TIME() - const grantTime = numReleasePeriods.times(releasePeriod).plus(5 * MINUTE) - const timeToTravel = grantTime.plus(expirationTime).toNumber() - await timeTravel(timeToTravel, web3) - }) - - describe('when `expire` is called', () => { - it('should revert', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.expire({ from: releaseOwner }), - 'Contract must be expirable' - ) - }) - }) - }) - }) - - describe('#refundAndFinalize', () => { - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - // wait some time for some gold to release - const timeToTravel = 7 * MONTH - await timeTravel(timeToTravel, web3) - }) - - it('should only be callable by releaseOwner and when revoked', async () => { - await releaseGoldInstance.revoke({ from: releaseOwner }) - await releaseGoldInstance.refundAndFinalize({ from: releaseOwner }) - }) - - it('should revert when revoked but called by a non-releaseOwner', async () => { - await releaseGoldInstance.revoke({ from: releaseOwner }) - await assertTransactionRevertWithReason( - releaseGoldInstance.refundAndFinalize({ from: accounts[5] }), - 'Sender must be the releaseOwner and state must be revoked' - ) - }) - - it('should revert when non-revoked but called by a releaseOwner', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.refundAndFinalize({ from: releaseOwner }), - 'Sender must be the releaseOwner and state must be revoked' - ) - }) - - describe('when revoked()', () => { - beforeEach(async () => { - await releaseGoldInstance.revoke({ from: releaseOwner }) - }) - - it('should transfer gold proportions to both beneficiary and refundAddress when no gold locked', async () => { - const beneficiaryBalanceBefore = await goldTokenInstance.balanceOf(beneficiary) - const refundAddressBalanceBefore = await goldTokenInstance.balanceOf(refundAddress) - const { releasedBalanceAtRevoke } = - (await releaseGoldInstance.revocationInfo()) as unknown as RevocationInfo - const beneficiaryRefundAmount = new BigNumber(releasedBalanceAtRevoke).minus( - await releaseGoldInstance.totalWithdrawn() - ) - const refundAddressRefundAmount = new BigNumber( - await goldTokenInstance.balanceOf(releaseGoldInstance.address) - ).minus(beneficiaryRefundAmount) - await releaseGoldInstance.refundAndFinalize({ from: releaseOwner }) - const contractBalanceAfterFinalize = await goldTokenInstance.balanceOf( - releaseGoldInstance.address - ) - const beneficiaryBalanceAfter = await goldTokenInstance.balanceOf(beneficiary) - const refundAddressBalanceAfter = await goldTokenInstance.balanceOf(refundAddress) - - assertEqualBN( - new BigNumber(beneficiaryBalanceAfter).minus(new BigNumber(beneficiaryBalanceBefore)), - beneficiaryRefundAmount - ) - assertEqualBN( - new BigNumber(refundAddressBalanceAfter).minus(new BigNumber(refundAddressBalanceBefore)), - refundAddressRefundAmount - ) - - assertEqualBN(contractBalanceAfterFinalize, 0) - }) - - it('should destruct releaseGold instance after finalizing and prevent calling further actions', async () => { - await releaseGoldInstance.refundAndFinalize({ from: releaseOwner }) - try { - await releaseGoldInstance.getRemainingUnlockedBalance() - } catch (ex) { - return assert.isTrue(true) - } - - return assert.isTrue(false) - }) - }) - }) - - describe('#lockGold', () => { - let lockAmount: BigNumber = new BigNumber(0) - - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - lockAmount = releaseGoldDefaultSchedule.amountReleasedPerPeriod.multipliedBy( - releaseGoldDefaultSchedule.numReleasePeriods - ) - }) - - it('beneficiary should lock up any unlocked amount', async () => { - // beneficiary shall make the released gold instance an account - await releaseGoldInstance.createAccount({ from: beneficiary }) - await releaseGoldInstance.lockGold(lockAmount, { - from: beneficiary, - }) - assertEqualBN( - await lockedGoldInstance.getAccountTotalLockedGold(releaseGoldInstance.address), - lockAmount - ) - assertEqualBN( - await lockedGoldInstance.getAccountNonvotingLockedGold(releaseGoldInstance.address), - lockAmount - ) - assertEqualBN(await lockedGoldInstance.getNonvotingLockedGold(), lockAmount) - assertEqualBN(await lockedGoldInstance.getTotalLockedGold(), lockAmount) - }) - - it('should revert if releaseGold instance is not an account', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.lockGold(lockAmount, { - from: beneficiary, - }), - 'Must first register address with Account.createAccount' - ) - }) - - it('should revert if beneficiary tries to lock up more than there is remaining in the contract', async () => { - await releaseGoldInstance.createAccount({ from: beneficiary }) - await assertTransactionRevertWithoutReason( - releaseGoldInstance.lockGold(lockAmount.multipliedBy(1.1), { - from: beneficiary, - }) - ) - }) - - it('should revert if non-beneficiary tries to lock up any unlocked amount', async () => { - await releaseGoldInstance.createAccount({ from: beneficiary }) - await assertTransactionRevertWithReason( - releaseGoldInstance.lockGold(lockAmount, { from: accounts[6] }), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - }) - - describe('#unlockGold', () => { - let lockAmount: BigNumber = new BigNumber(0) - - beforeEach(async () => { - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - // beneficiary shall make the released gold instance an account - await releaseGoldInstance.createAccount({ from: beneficiary }) - lockAmount = releaseGoldDefaultSchedule.amountReleasedPerPeriod.multipliedBy( - releaseGoldDefaultSchedule.numReleasePeriods - ) - }) - - it('beneficiary should unlock his locked gold and add a pending withdrawal', async () => { - await releaseGoldInstance.lockGold(lockAmount, { - from: beneficiary, - }) - await releaseGoldInstance.unlockGold(lockAmount, { - from: beneficiary, - }) - - const data = await lockedGoldInstance.getPendingWithdrawals(releaseGoldInstance.address) - const values = data[0] - const timestamps = data[1] - assert.equal(values.length, 1) - assert.equal(timestamps.length, 1) - assertEqualBN(values[0], lockAmount) - assertEqualBN(timestamps[0], (await getCurrentBlockchainTimestamp(web3)) + UNLOCKING_PERIOD) - - assertEqualBN( - await lockedGoldInstance.getAccountTotalLockedGold(releaseGoldInstance.address), - 0 - ) - // ReleaseGold locked balance should still reflect pending withdrawals - assertEqualBN(await releaseGoldInstance.getRemainingLockedBalance(), lockAmount) - assertEqualBN( - await lockedGoldInstance.getAccountNonvotingLockedGold(releaseGoldInstance.address), - 0 - ) - assertEqualBN(await lockedGoldInstance.getNonvotingLockedGold(), 0) - assertEqualBN(await lockedGoldInstance.getTotalLockedGold(), 0) - }) - - it('should revert if non-beneficiary tries to unlock the locked amount', async () => { - // lock the entire releaseGold amount - await releaseGoldInstance.lockGold(lockAmount, { - from: beneficiary, - }) - // unlock the latter - await assertTransactionRevertWithReason( - releaseGoldInstance.unlockGold(lockAmount, { from: accounts[5] }), - 'Must be called by releaseOwner when revoked or beneficiary before revocation' - ) - }) - - it('should revert if beneficiary in voting tries to unlock the locked amount', async () => { - // set the contract in voting - await mockGovernance.setVoting(releaseGoldInstance.address) - // lock the entire releaseGold amount - await releaseGoldInstance.lockGold(lockAmount, { - from: beneficiary, - }) - // unlock the latter - await assertTransactionRevertWithReason( - releaseGoldInstance.unlockGold(lockAmount, { from: accounts[5] }), - 'Must be called by releaseOwner when revoked or beneficiary before revocation' - ) - }) - - it('should revert if beneficiary with balance requirements tries to unlock the locked amount', async () => { - // set the contract in voting - await mockGovernance.setVoting(releaseGoldInstance.address) - // lock the entire releaseGold amount - await releaseGoldInstance.lockGold(lockAmount, { - from: beneficiary, - }) - // set some balance requirements - const balanceRequirement = 10 - await mockValidators.setAccountLockedGoldRequirement( - releaseGoldInstance.address, - balanceRequirement - ) - // unlock the latter - await assertTransactionRevertWithReason( - releaseGoldInstance.unlockGold(lockAmount, { from: beneficiary }), - "Either account doesn't have enough locked Celo or locked Celo is being used for voting." - ) - }) - }) - - describe('#withdrawLockedGold', () => { - const value = 1000 - const index = 0 - - describe('when a pending withdrawal exists', () => { - beforeEach(async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - await releaseGoldInstance.createAccount({ from: beneficiary }) - await releaseGoldInstance.lockGold(value, { from: beneficiary }) - await releaseGoldInstance.unlockGold(value, { from: beneficiary }) - }) - - describe('when it is after the availablity time', () => { - beforeEach(async () => { - await timeTravel(UNLOCKING_PERIOD, web3) - await releaseGoldInstance.withdrawLockedGold(index, { from: beneficiary }) - }) - - it('should remove the pending withdrawal', async () => { - const data = await lockedGoldInstance.getPendingWithdrawals(releaseGoldInstance.address) - const values = data[0] - const timestamps = data[1] - assert.equal(values.length, 0) - assert.equal(timestamps.length, 0) - assertEqualBN(await releaseGoldInstance.getRemainingLockedBalance(), 0) - }) - }) - - describe('when it is before the availablity time', () => { - it('should revert', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.withdrawLockedGold(index, { from: beneficiary }), - 'Pending withdrawal not available' - ) - }) - }) - - describe('when non-beneficiary attempts to withdraw the gold', () => { - it('should revert', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.withdrawLockedGold(index, { from: accounts[4] }), - 'Must be called by releaseOwner when revoked or beneficiary before revocation' - ) - }) - }) - }) - - describe('when a pending withdrawal does not exist', () => { - it('should revert', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.withdrawLockedGold(index, { from: beneficiary }), - 'Pending withdrawal not available' - ) - }) - }) - }) - - describe('#relockGold', () => { - const pendingWithdrawalValue = 1000 - const index = 0 - - beforeEach(async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await createNewReleaseGoldInstance(releaseGoldDefaultSchedule, web3) - await releaseGoldInstance.createAccount({ from: beneficiary }) - await releaseGoldInstance.lockGold(pendingWithdrawalValue, { from: beneficiary }) - await releaseGoldInstance.unlockGold(pendingWithdrawalValue, { from: beneficiary }) - }) - - describe('when a pending withdrawal exists', () => { - describe('when relocking value equal to the value of the pending withdrawal', () => { - const value = pendingWithdrawalValue - beforeEach(async () => { - await releaseGoldInstance.relockGold(index, value, { from: beneficiary }) - }) - - it("should increase the account's nonvoting locked gold balance", async () => { - assertEqualBN( - await lockedGoldInstance.getAccountNonvotingLockedGold(releaseGoldInstance.address), - value - ) - }) - - it("should increase the account's total locked gold balance", async () => { - assertEqualBN( - await lockedGoldInstance.getAccountTotalLockedGold(releaseGoldInstance.address), - value - ) - }) - - it('should increase the nonvoting locked gold balance', async () => { - assertEqualBN(await lockedGoldInstance.getNonvotingLockedGold(), value) - }) - - it('should increase the total locked gold balance', async () => { - assertEqualBN(await lockedGoldInstance.getTotalLockedGold(), value) - }) - - it('should remove the pending withdrawal', async () => { - const data = await lockedGoldInstance.getPendingWithdrawals(releaseGoldInstance.address) - const values = data[0] - const timestamps = data[1] - assert.equal(values.length, 0) - assert.equal(timestamps.length, 0) - }) - }) - - describe('when relocking value less than the value of the pending withdrawal', () => { - const value = pendingWithdrawalValue - 1 - beforeEach(async () => { - await releaseGoldInstance.relockGold(index, value, { from: beneficiary }) - }) - - it("should increase the account's nonvoting locked gold balance", async () => { - assertEqualBN( - await lockedGoldInstance.getAccountNonvotingLockedGold(releaseGoldInstance.address), - value - ) - }) - - it("should increase the account's total locked gold balance", async () => { - assertEqualBN( - await lockedGoldInstance.getAccountTotalLockedGold(releaseGoldInstance.address), - value - ) - }) - - it('should increase the nonvoting locked gold balance', async () => { - assertEqualBN(await lockedGoldInstance.getNonvotingLockedGold(), value) - }) - - it('should increase the total locked gold balance', async () => { - assertEqualBN(await lockedGoldInstance.getTotalLockedGold(), value) - }) - - it('should decrement the value of the pending withdrawal', async () => { - const [values, timestamps] = await lockedGoldInstance.getPendingWithdrawals( - releaseGoldInstance.address - ) - assert.equal(values.length, 1) - assert.equal(timestamps.length, 1) - assertEqualBN(values[0], 1) - }) - }) - - describe('when relocking value greater than the value of the pending withdrawal', () => { - const value = pendingWithdrawalValue + 1 - it('should revert', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.relockGold(index, value, { from: beneficiary }), - 'Requested value larger than pending value' - ) - }) - }) - }) - - describe('when a pending withdrawal does not exist', () => { - it('should revert', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.relockGold(index, pendingWithdrawalValue), - 'Sender must be the beneficiary and state must not be revoked' - ) - }) - }) - }) - - describe('#withdraw', () => { - let initialreleaseGoldAmount: any - - beforeEach(async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.releaseStartTime = Math.round(Date.now() / 1000) - releaseGoldSchedule.initialDistributionRatio = 0 - await createNewReleaseGoldInstance(releaseGoldSchedule, web3) - initialreleaseGoldAmount = releaseGoldSchedule.amountReleasedPerPeriod.multipliedBy( - releaseGoldSchedule.numReleasePeriods - ) - }) - - it('should revert before the release cliff has passed', async () => { - await releaseGoldInstance.setMaxDistribution(1000, { from: releaseOwner }) - const timeToTravel = 0.5 * HOUR - await timeTravel(timeToTravel, web3) - await assertTransactionRevertWithReason( - releaseGoldInstance.withdraw(initialreleaseGoldAmount.div(20), { from: beneficiary }), - 'Requested amount is greater than available released funds' - ) - }) - - it('should revert when withdrawable amount is zero', async () => { - await releaseGoldInstance.setMaxDistribution(1000, { from: releaseOwner }) - const timeToTravel = 3 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - await assertTransactionRevertWithReason( - releaseGoldInstance.withdraw(new BigNumber(0), { from: beneficiary }), - 'Requested withdrawal amount must be greater than zero' - ) - }) - - describe('when not revoked', () => { - describe('when max distribution is 100%', () => { - beforeEach(async () => { - await releaseGoldInstance.setMaxDistribution(1000, { from: releaseOwner }) - }) - it('should revert since beneficiary should not be able to withdraw anything within the first quarter', async () => { - const beneficiaryBalanceBefore = await goldTokenInstance.balanceOf(beneficiary) - const timeToTravel = 2.9 * MONTH - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = await releaseGoldInstance.getCurrentReleasedTotalAmount() - const beneficiaryBalanceAfter = await goldTokenInstance.balanceOf(beneficiary) - assertEqualBN(expectedWithdrawalAmount, 0) - await assertTransactionRevertWithReason( - releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }), - 'Requested withdrawal amount must be greater than zero' - ) - assertEqualBN( - new BigNumber(beneficiaryBalanceAfter).minus(new BigNumber(beneficiaryBalanceBefore)), - 0 - ) - }) - - it('should allow the beneficiary to withdraw 25% of the released amount of gold right after the beginning of the first quarter', async () => { - const beneficiaryBalanceBefore = await goldTokenInstance.balanceOf(beneficiary) - const timeToTravel = 3 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = initialreleaseGoldAmount.div(4) - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - const totalWithdrawn = await releaseGoldInstance.totalWithdrawn() - const beneficiaryBalanceAfter = await goldTokenInstance.balanceOf(beneficiary) - expectBigNumberInRange(new BigNumber(totalWithdrawn), expectedWithdrawalAmount) - expectBigNumberInRange( - new BigNumber(beneficiaryBalanceAfter).minus(new BigNumber(beneficiaryBalanceBefore)), - expectedWithdrawalAmount - ) - }) - - it('should allow the beneficiary to withdraw 50% the released amount of gold when half of the release periods have passed', async () => { - const beneficiaryBalanceBefore = await goldTokenInstance.balanceOf(beneficiary) - const timeToTravel = 6 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = initialreleaseGoldAmount.div(2) - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - const totalWithdrawn = await releaseGoldInstance.totalWithdrawn() - const beneficiaryBalanceAfter = await goldTokenInstance.balanceOf(beneficiary) - assertEqualBN(new BigNumber(totalWithdrawn), expectedWithdrawalAmount) - expectBigNumberInRange( - new BigNumber(beneficiaryBalanceAfter).minus(new BigNumber(beneficiaryBalanceBefore)), - expectedWithdrawalAmount - ) - }) - - it('should allow the beneficiary to withdraw 75% of the released amount of gold right after the beginning of the third quarter', async () => { - const beneficiaryBalanceBefore = await goldTokenInstance.balanceOf(beneficiary) - const timeToTravel = 9 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = initialreleaseGoldAmount.multipliedBy(3).div(4) - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - const beneficiaryBalanceAfter = await goldTokenInstance.balanceOf(beneficiary) - const totalWithdrawn = await releaseGoldInstance.totalWithdrawn() - assertEqualBN(new BigNumber(totalWithdrawn), expectedWithdrawalAmount) - expectBigNumberInRange( - new BigNumber(beneficiaryBalanceAfter).minus(new BigNumber(beneficiaryBalanceBefore)), - expectedWithdrawalAmount - ) - }) - - it('should allow the beneficiary to withdraw 100% of the amount right after the end of the release period', async () => { - const beneficiaryBalanceBefore = await goldTokenInstance.balanceOf(beneficiary) - const timeToTravel = 12 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = initialreleaseGoldAmount - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - const beneficiaryBalanceAfter = await goldTokenInstance.balanceOf(beneficiary) - - expectBigNumberInRange( - new BigNumber(beneficiaryBalanceAfter).minus(new BigNumber(beneficiaryBalanceBefore)), - expectedWithdrawalAmount - ) - }) - - it('should destruct releaseGold instance when the entire balance is withdrawn', async () => { - const timeToTravel = 12 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = initialreleaseGoldAmount - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - - try { - await releaseGoldInstance.totalWithdrawn() - return assert.isTrue(false) - } catch (ex) { - return assert.isTrue(true) - } - }) - - describe('when rewards are simulated', () => { - beforeEach(async () => { - // Simulate rewards of 0.5 Gold - await goldTokenInstance.transfer(releaseGoldInstance.address, ONE_GOLDTOKEN.div(2), { - from: owner, - }) - // Default distribution is 100% - }) - - describe('when the grant has fully released', () => { - beforeEach(async () => { - const timeToTravel = 12 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - }) - - it('should allow distribution of initial balance and rewards', async () => { - const expectedWithdrawalAmount = TOTAL_AMOUNT.plus(ONE_GOLDTOKEN.div(2)) - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - }) - }) - - describe('when the grant is only halfway released', () => { - beforeEach(async () => { - const timeToTravel = 6 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - }) - - it('should scale released amount to 50% of initial balance plus rewards', async () => { - const expectedWithdrawalAmount = TOTAL_AMOUNT.plus(ONE_GOLDTOKEN.div(2)).div(2) - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - }) - - it('should not allow withdrawal of more than 50% gold', async () => { - const unexpectedWithdrawalAmount = TOTAL_AMOUNT.plus(ONE_GOLDTOKEN).div(2).plus(1) - await assertTransactionRevertWithReason( - releaseGoldInstance.withdraw(unexpectedWithdrawalAmount, { from: beneficiary }), - 'Requested amount is greater than available released funds' - ) - }) - }) - }) - }) - - // Max distribution should set a static value of `ratio` of total funds at call time of `setMaxDistribution` - // So this is testing that the maxDistribution is unrelated to rewards, except the 100% special case. - describe('when max distribution is 50% and all gold is released', () => { - beforeEach(async () => { - await releaseGoldInstance.setMaxDistribution(500, { from: releaseOwner }) - // Simulate rewards of 0.5 Gold - // Have to send after setting max distribution as mentioned above - await goldTokenInstance.transfer(releaseGoldInstance.address, ONE_GOLDTOKEN.div(2), { - from: owner, - }) - const timeToTravel = 12 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - }) - - it('should only allow withdrawal of 50% of initial grant (not including rewards)', async () => { - const expectedWithdrawalAmount = TOTAL_AMOUNT.div(2) - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - const unexpectedWithdrawalAmount = 1 - await assertTransactionRevertWithReason( - releaseGoldInstance.withdraw(unexpectedWithdrawalAmount, { from: beneficiary }), - 'Requested amount exceeds current alloted maximum distribution' - ) - }) - }) - }) - - describe('when revoked', () => { - describe('when max distribution is 100%', () => { - beforeEach(async () => { - await releaseGoldInstance.setMaxDistribution(1000, { from: releaseOwner }) - }) - it('should allow the beneficiary to withdraw up to the releasedBalanceAtRevoke', async () => { - const beneficiaryBalanceBefore = await goldTokenInstance.balanceOf(beneficiary) - const timeToTravel = 6 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - await releaseGoldInstance.revoke({ from: releaseOwner }) - const info = (await releaseGoldInstance.revocationInfo()) as unknown as RevocationInfo - const expectedWithdrawalAmount = info[2] - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - const totalWithdrawn = await releaseGoldInstance.totalWithdrawn() - const beneficiaryBalanceAfter = await goldTokenInstance.balanceOf(beneficiary) - expectBigNumberInRange(new BigNumber(totalWithdrawn), expectedWithdrawalAmount) - expectBigNumberInRange( - new BigNumber(beneficiaryBalanceAfter).minus(new BigNumber(beneficiaryBalanceBefore)), - expectedWithdrawalAmount - ) - }) - - it('should revert if beneficiary attempts to withdraw more than releasedBalanceAtRevoke', async () => { - const timeToTravel = 6 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - await releaseGoldInstance.revoke({ from: releaseOwner }) - const { releasedBalanceAtRevoke } = - (await releaseGoldInstance.revocationInfo()) as unknown as RevocationInfo - await assertTransactionRevertWithReason( - releaseGoldInstance.withdraw(new BigNumber(releasedBalanceAtRevoke).multipliedBy(1.1), { - from: beneficiary, - }), - 'Requested amount is greater than available released funds' - ) - }) - - it('should selfdestruct if beneficiary withdraws the entire amount', async () => { - const beneficiaryBalanceBefore = await goldTokenInstance.balanceOf(beneficiary) - const timeToTravel = 12 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - await releaseGoldInstance.revoke({ from: releaseOwner }) - const [, , expectedWithdrawalAmount] = await releaseGoldInstance.revocationInfo() - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - const beneficiaryBalanceAfter = await goldTokenInstance.balanceOf(beneficiary) - - expectBigNumberInRange( - new BigNumber(beneficiaryBalanceAfter).minus(new BigNumber(beneficiaryBalanceBefore)), - expectedWithdrawalAmount - ) - - try { - await releaseGoldInstance.totalWithdrawn() - return assert.isTrue(false) - } catch (ex) { - return assert.isTrue(true) - } - }) - }) - }) - - describe('when max distribution is set lower', () => { - let beneficiaryBalanceBefore: any - beforeEach(async () => { - beneficiaryBalanceBefore = await goldTokenInstance.balanceOf(beneficiary) - const timeToTravel = 12 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - }) - - describe('when max distribution is 50%', () => { - beforeEach(async () => { - await releaseGoldInstance.setMaxDistribution(500, { from: releaseOwner }) - }) - - it('should allow withdrawal of 50%', async () => { - const expectedWithdrawalAmount = initialreleaseGoldAmount.multipliedBy(0.5) - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - const beneficiaryBalanceAfter = await goldTokenInstance.balanceOf(beneficiary) - - expectBigNumberInRange( - new BigNumber(beneficiaryBalanceAfter).minus(new BigNumber(beneficiaryBalanceBefore)), - expectedWithdrawalAmount - ) - }) - - it('should revert on withdrawal of more than 50%', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.withdraw(initialreleaseGoldAmount, { from: beneficiary }), - 'Requested amount exceeds current alloted maximum distribution' - ) - }) - }) - - describe('when max distribution is 100%', () => { - beforeEach(async () => { - await releaseGoldInstance.setMaxDistribution(1000, { from: releaseOwner }) - }) - - it('should allow withdrawal of all gold', async () => { - const expectedWithdrawalAmount = initialreleaseGoldAmount - await releaseGoldInstance.withdraw(expectedWithdrawalAmount, { from: beneficiary }) - const beneficiaryBalanceAfter = await goldTokenInstance.balanceOf(beneficiary) - - expectBigNumberInRange( - new BigNumber(beneficiaryBalanceAfter).minus(new BigNumber(beneficiaryBalanceBefore)), - expectedWithdrawalAmount - ) - }) - }) - }) - - describe('when the liquidity provision is observed and set false', () => { - beforeEach(async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.subjectToLiquidityProvision = true - await createNewReleaseGoldInstance(releaseGoldSchedule, web3) - // Withdraw `beforeEach` creates one instance, have to grab the second - const timeToTravel = 12 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - }) - - it('should revert on withdraw of any amount', async () => { - await assertTransactionRevertWithReason( - releaseGoldInstance.withdraw(initialreleaseGoldAmount.multipliedBy(0.5), { - from: beneficiary, - }), - 'Requested withdrawal before liquidity provision is met' - ) - await assertTransactionRevertWithReason( - releaseGoldInstance.withdraw(initialreleaseGoldAmount, { from: beneficiary }), - 'Requested withdrawal before liquidity provision is met' - ) - }) - }) - }) - - describe('#getCurrentReleasedTotalAmount', () => { - let initialreleaseGoldAmount: any - - beforeEach(async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.releaseStartTime = Math.round(Date.now() / 1000) - await createNewReleaseGoldInstance(releaseGoldSchedule, web3) - initialreleaseGoldAmount = releaseGoldSchedule.amountReleasedPerPeriod.multipliedBy( - releaseGoldSchedule.numReleasePeriods - ) - }) - - it('should return zero if before cliff start time', async () => { - const timeToTravel = 0.5 * HOUR - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = 0 - assertEqualBN( - await releaseGoldInstance.getCurrentReleasedTotalAmount(), - expectedWithdrawalAmount - ) - }) - - it('should return 25% of the released amount of gold right after the beginning of the first quarter', async () => { - const timeToTravel = 3 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = initialreleaseGoldAmount.div(4) - assertEqualBN( - await releaseGoldInstance.getCurrentReleasedTotalAmount(), - expectedWithdrawalAmount - ) - }) - - it('should return 50% the released amount of gold right after the beginning of the second quarter', async () => { - const timeToTravel = 6 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = initialreleaseGoldAmount.div(2) - assertEqualBN( - await releaseGoldInstance.getCurrentReleasedTotalAmount(), - expectedWithdrawalAmount - ) - }) - - it('should return 75% of the released amount of gold right after the beginning of the third quarter', async () => { - const timeToTravel = 9 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = initialreleaseGoldAmount.multipliedBy(3).div(4) - assertEqualBN( - await releaseGoldInstance.getCurrentReleasedTotalAmount(), - expectedWithdrawalAmount - ) - }) - - it('should return 100% of the amount right after the end of the releaseGold period', async () => { - const timeToTravel = 12 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = initialreleaseGoldAmount - assertEqualBN( - await releaseGoldInstance.getCurrentReleasedTotalAmount(), - expectedWithdrawalAmount - ) - }) - }) - - describe('#getWithdrawableAmount', () => { - let initialReleaseGoldAmount: any - - beforeEach(async () => { - const releaseGoldSchedule = _.clone(releaseGoldDefaultSchedule) - releaseGoldSchedule.canValidate = true - releaseGoldSchedule.revocable = false - releaseGoldSchedule.refundAddress = '0x0000000000000000000000000000000000000000' - releaseGoldSchedule.releaseStartTime = Math.round(Date.now() / 1000) - releaseGoldSchedule.initialDistributionRatio = 500 - await createNewReleaseGoldInstance(releaseGoldSchedule, web3) - initialReleaseGoldAmount = releaseGoldSchedule.amountReleasedPerPeriod.multipliedBy( - releaseGoldSchedule.numReleasePeriods - ) - - await releaseGoldInstance.createAccount({ from: beneficiary }) - }) - - describe('should return 50% of the released amount of gold right after the beginning of the second quarter', async () => { - beforeEach(async () => { - await releaseGoldInstance.setMaxDistribution(1000, { from: releaseOwner }) - - const timeToTravel = 6 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - }) - - it('should return the full amount available for this release period', async () => { - const expectedWithdrawalAmount = initialReleaseGoldAmount.div(2) - const withdrawableAmount = await releaseGoldInstance.getWithdrawableAmount() - assertEqualBN(withdrawableAmount, expectedWithdrawalAmount) - }) - - it('should return only amount not yet withdrawn', async () => { - const expectedWithdrawalAmount = initialReleaseGoldAmount.div(2) - await releaseGoldInstance.withdraw(expectedWithdrawalAmount.div(2), { from: beneficiary }) - - const afterWithdrawal = await releaseGoldInstance.getWithdrawableAmount() - await releaseGoldInstance.getWithdrawableAmount() - assertEqualBN(afterWithdrawal, expectedWithdrawalAmount.div(2)) - }) - }) - - it('should return only up to its own balance', async () => { - await releaseGoldInstance.setMaxDistribution(1000, { from: releaseOwner }) - const timeToTravel = 6 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - const signerFund = new BigNumber('1000000000000000000') - const expectedWithdrawalAmount = initialReleaseGoldAmount.minus(signerFund).div(2) - - const authorized = accounts[4] - const ecdsaPublicKey = await addressToPublicKey(authorized, web3.eth.sign) - const sig = await getParsedSignatureOfAddress(web3, releaseGoldInstance.address, authorized) - // this will send 1 CELO from release gold balance to authorized - await releaseGoldInstance.authorizeValidatorSignerWithPublicKey( - authorized, - sig.v, - sig.r, - sig.s, - ecdsaPublicKey as any, - { from: beneficiary } - ) - - const withdrawableAmount = await releaseGoldInstance.getWithdrawableAmount() - assertEqualBN(withdrawableAmount, expectedWithdrawalAmount) - }) - - it('should return only up to max distribution', async () => { - const timeToTravel = 6 * MONTH + 1 * DAY - await timeTravel(timeToTravel, web3) - const expectedWithdrawalAmount = initialReleaseGoldAmount.div(2) - - await releaseGoldInstance.setMaxDistribution(250, { from: releaseOwner }) - - const withdrawableAmount = await releaseGoldInstance.getWithdrawableAmount() - assertEqualBN(withdrawableAmount, expectedWithdrawalAmount.div(2)) - }) - }) -})