diff --git a/contracts/Airdrop.sol b/contracts/Airdrop.sol index 26bb13f..cab8f9d 100644 --- a/contracts/Airdrop.sol +++ b/contracts/Airdrop.sol @@ -1,24 +1,25 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity =0.5.17; - -pragma experimental ABIEncoderV2; +pragma solidity ^0.8.9; import "./interfaces/IERC20.sol"; import "./interfaces/IGovernance.sol"; -import "./libraries/SafeERC20.sol"; import "./ReentrancyGuard.sol"; -import "./CarefulMath.sol"; import "./interfaces/ISablierAirdrop.sol"; import "./interfaces/IRecipientStorage.sol"; import "./libraries/Types.sol"; -contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { - using SafeERC20 for IERC20; - +contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard { /*** Storage Properties ***/ + /** + * @notice Tornado Governance contract is an owner of airdrop contract and can create or cancel airdrops + */ address public constant tornadoGovernance = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce; + + /** + * @notice TORN token - token for airdrops + */ IERC20 public constant torn = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C); /** @@ -59,7 +60,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { /*** Contract Logic Starts Here */ - constructor() public { + constructor() { nextStreamId = 1; } @@ -69,7 +70,6 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { * @notice Returns the stream with all its properties. * @dev Throws if the id does not point to a valid stream. * @param streamId The id of the stream to query. - * @return The stream object. */ function getStream( uint256 streamId @@ -100,7 +100,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { * `startTime`, it returns 0. * @dev Throws if the id does not point to a valid stream. * @param streamId The id of the stream for which to query the delta. - * @return The time delta in seconds. + * @return delta The time delta in seconds. */ function deltaOf(uint256 streamId) public view streamExists(streamId) returns (uint256 delta) { Types.Stream memory stream = streams[streamId]; @@ -109,26 +109,17 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { return stream.stopTime - stream.startTime; } - struct BalanceOfLocalVars { - MathError mathErr; - uint256 recipientBalance; - uint256 withdrawalAmount; - } - /** * @notice Returns the available funds for the given stream id and address. * @dev Throws if the id does not point to a valid stream. * @param streamId The id of the stream for which to query the balance. * @param who The address for which to query the balance. - * @return The total funds allocated to `who` as uint256. + * @return balance The total funds allocated to `who` as uint256. */ function balanceOf(uint256 streamId, address who) public view streamExists(streamId) returns (uint256 balance) { Types.Stream memory stream = streams[streamId]; - BalanceOfLocalVars memory vars; - uint256 delta = deltaOf(streamId); - (vars.mathErr, vars.recipientBalance) = mulUInt(delta, stream.ratePerSecond); - require(vars.mathErr == MathError.NO_ERROR, "recipient balance calculation error"); + uint256 recipientBalance = delta * stream.ratePerSecond; /* * If the stream `balance` does not equal `deposit`, it means there have been withdrawals. @@ -136,14 +127,11 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { * streamed until now. */ if (stream.deposit > stream.remainingBalance) { - (vars.mathErr, vars.withdrawalAmount) = subUInt(stream.deposit, stream.remainingBalance); - assert(vars.mathErr == MathError.NO_ERROR); - (vars.mathErr, vars.recipientBalance) = subUInt(vars.recipientBalance, vars.withdrawalAmount); - /* `withdrawalAmount` cannot and should not be bigger than `recipientBalance`. */ - assert(vars.mathErr == MathError.NO_ERROR); + uint256 withdrawalAmount = stream.deposit - stream.remainingBalance; + recipientBalance = recipientBalance - withdrawalAmount; } - if (who == stream.recipient) return vars.recipientBalance; + if (who == stream.recipient) return recipientBalance; return 0; } @@ -151,7 +139,6 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { * @notice To avoid "stack to deep" error */ struct CreateAirdropLocalVars { - MathError mathErr; uint256 airdropDuration; uint256 ratePerSecond; uint256 firstStream; @@ -163,7 +150,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { uint256 startTime, uint256 stopTime, address recipientStorage - ) public onlyGovernance returns (bool) { + ) external onlyGovernance returns (bool) { CreateAirdropLocalVars memory vars; vars.airdropDuration = stopTime - startTime; vars.airdropRecipients = IRecipientStorage(recipientStorage).getAirdropRecipients(); @@ -180,9 +167,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { /* Without this, the rate per second would be zero. */ require(normalizedDeposit >= vars.airdropDuration, "deposit smaller than time delta"); - (vars.mathErr, vars.ratePerSecond) = divUInt(normalizedDeposit, vars.airdropDuration); - /* `divUInt` can only return MathError.DIVISION_BY_ZERO but we know `duration` is not zero. */ - assert(vars.mathErr == MathError.NO_ERROR); + vars.ratePerSecond = normalizedDeposit / vars.airdropDuration; /* Create and store the stream object. */ uint256 streamId = nextStreamId; @@ -198,8 +183,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { }); /* Increment the next stream id. */ - (vars.mathErr, nextStreamId) = addUInt(nextStreamId, uint256(1)); - require(vars.mathErr == MathError.NO_ERROR, "next stream id calculation error"); + nextStreamId = nextStreamId + 1; emit CreateStream( streamId, @@ -216,39 +200,31 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { } /** - * @notice Withdraws from the contract to the recipient's account. + * @notice Withdraws from the contract to the recipient's account all available recipient stream balance. * @dev Throws if the id does not point to a valid stream. * Throws if the caller is not the sender or the recipient of the stream. * Throws if the amount exceeds the available balance. * Throws if there is a token transfer failure. * @param streamId The id of the stream to withdraw tokens from. - * @param amount The amount of tokens to withdraw. */ function withdrawFromStream( - uint256 streamId, - uint256 amount + uint256 streamId ) external nonReentrant streamExists(streamId) onlyRecipient(streamId) returns (bool) { - require(amount > 0, "amount is zero"); Types.Stream memory stream = streams[streamId]; + uint256 balance = balanceOf(streamId, stream.recipient); + require(balance > 0, "amount is zero"); + uint256 recipientLockedBalance = IGovernance(tornadoGovernance).lockedBalance(stream.recipient); require(recipientLockedBalance >= stream.initialLockedBalance, "not enough locked tokens in governance"); - uint256 balance = balanceOf(streamId, stream.recipient); - require(balance >= amount, "amount exceeds the available balance"); - - MathError mathErr; - (mathErr, streams[streamId].remainingBalance) = subUInt(stream.remainingBalance, amount); - /** - * `subUInt` can only return MathError.INTEGER_UNDERFLOW but we know that `remainingBalance` is at least - * as big as `amount`. - */ - assert(mathErr == MathError.NO_ERROR); + /* Remaining balance can not be less than recipient stream balance */ + streams[streamId].remainingBalance = stream.remainingBalance - balance; if (streams[streamId].remainingBalance == 0) delete streams[streamId]; - torn.safeTransfer(stream.recipient, amount); - emit WithdrawFromStream(streamId, stream.recipient, amount); + torn.transfer(stream.recipient, balance); + emit WithdrawFromStream(streamId, stream.recipient, balance); return true; } @@ -266,7 +242,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { delete streams[streamId]; - if (remainingBalance > 0) torn.safeTransfer(tornadoGovernance, remainingBalance); + if (remainingBalance > 0) torn.transfer(tornadoGovernance, remainingBalance); emit CancelStream(streamId, stream.recipient, remainingBalance); return true; @@ -293,7 +269,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { } } - torn.safeTransfer(tornadoGovernance, airdropRemainingBalance); + torn.transfer(tornadoGovernance, airdropRemainingBalance); return true; } @@ -303,7 +279,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { * @return bool true=success, otherwise false. */ function withdrawFunds() external onlyGovernance returns (bool) { - torn.safeTransfer(tornadoGovernance, torn.balanceOf(address(this))); + torn.transfer(tornadoGovernance, torn.balanceOf(address(this))); return true; } } diff --git a/contracts/CarefulMath.sol b/contracts/CarefulMath.sol deleted file mode 100644 index cf13b77..0000000 --- a/contracts/CarefulMath.sol +++ /dev/null @@ -1,84 +0,0 @@ -pragma solidity >=0.5.17; - -/** - * @title Careful Math - * @author Compound - * @notice Derived from OpenZeppelin's SafeMath library - * https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol - */ -contract CarefulMath { - /** - * @dev Possible error codes that we can return - */ - enum MathError { - NO_ERROR, - DIVISION_BY_ZERO, - INTEGER_OVERFLOW, - INTEGER_UNDERFLOW - } - - /** - * @dev Multiplies two numbers, returns an error on overflow. - */ - function mulUInt(uint a, uint b) internal pure returns (MathError, uint) { - if (a == 0) { - return (MathError.NO_ERROR, 0); - } - - uint c = a * b; - - if (c / a != b) { - return (MathError.INTEGER_OVERFLOW, 0); - } else { - return (MathError.NO_ERROR, c); - } - } - - /** - * @dev Integer division of two numbers, truncating the quotient. - */ - function divUInt(uint a, uint b) internal pure returns (MathError, uint) { - if (b == 0) { - return (MathError.DIVISION_BY_ZERO, 0); - } - - return (MathError.NO_ERROR, a / b); - } - - /** - * @dev Subtracts two numbers, returns an error on overflow (i.e. if subtrahend is greater than minuend). - */ - function subUInt(uint a, uint b) internal pure returns (MathError, uint) { - if (b <= a) { - return (MathError.NO_ERROR, a - b); - } else { - return (MathError.INTEGER_UNDERFLOW, 0); - } - } - - /** - * @dev Adds two numbers, returns an error on overflow. - */ - function addUInt(uint a, uint b) internal pure returns (MathError, uint) { - uint c = a + b; - - if (c >= a) { - return (MathError.NO_ERROR, c); - } else { - return (MathError.INTEGER_OVERFLOW, 0); - } - } - - /** - * @dev add a and b and then subtract c - */ - function addThenSubUInt(uint a, uint b, uint c) internal pure returns (MathError, uint) { - (MathError err0, uint sum) = addUInt(a, b); - - if (err0 != MathError.NO_ERROR) { - return (err0, 0); - } - - return subUInt(sum, c); - } -} diff --git a/contracts/ReentrancyGuard.sol b/contracts/ReentrancyGuard.sol index 296370f..d0eaf12 100644 --- a/contracts/ReentrancyGuard.sol +++ b/contracts/ReentrancyGuard.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.5.0; +pragma solidity ^0.8.9; /** * @dev Contract module that helps prevent reentrant calls to a function. @@ -16,7 +16,7 @@ contract ReentrancyGuard { /// @dev counter to allow mutex lock with only one SSTORE operation uint256 private _guardCounter; - constructor() internal { + constructor() { // The counter starts at one to prevent changing it from zero to a non-zero // value, which is a more expensive operation. _guardCounter = 1; diff --git a/contracts/interfaces/IERC20.sol b/contracts/interfaces/IERC20.sol index f9d13c5..147fac4 100644 --- a/contracts/interfaces/IERC20.sol +++ b/contracts/interfaces/IERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.0; +pragma solidity ^0.8.9; /** * @dev Interface of the ERC20 standard as defined in the EIP. Does not include diff --git a/contracts/interfaces/IGovernance.sol b/contracts/interfaces/IGovernance.sol index 24baa3d..174dca0 100644 --- a/contracts/interfaces/IGovernance.sol +++ b/contracts/interfaces/IGovernance.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.0; +pragma solidity ^0.8.9; interface IGovernance { function lockedBalance(address staker) external view returns (uint256); diff --git a/contracts/interfaces/IRecipientStorage.sol b/contracts/interfaces/IRecipientStorage.sol index ac6bb7b..80cb675 100644 --- a/contracts/interfaces/IRecipientStorage.sol +++ b/contracts/interfaces/IRecipientStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.17; +pragma solidity ^0.8.9; pragma experimental ABIEncoderV2; diff --git a/contracts/interfaces/ISablierAirdrop.sol b/contracts/interfaces/ISablierAirdrop.sol index 93c64f8..7f8829a 100644 --- a/contracts/interfaces/ISablierAirdrop.sol +++ b/contracts/interfaces/ISablierAirdrop.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.17; +pragma solidity ^0.8.9; pragma experimental ABIEncoderV2; @@ -57,7 +57,7 @@ interface ISablierAirdrop { function createAirdrop(uint256 startTime, uint256 stopTime, address recipientStorage) external returns (bool); - function withdrawFromStream(uint256 streamId, uint256 funds) external returns (bool); + function withdrawFromStream(uint256 streamId) external returns (bool); function cancelStream(uint256 streamId) external returns (bool); } diff --git a/contracts/libraries/Address.sol b/contracts/libraries/Address.sol index 4201274..1ebcaba 100644 --- a/contracts/libraries/Address.sol +++ b/contracts/libraries/Address.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.5.0; +pragma solidity ^0.8.9; /** * @dev Collection of functions related to the address type, diff --git a/contracts/libraries/SafeERC20.sol b/contracts/libraries/SafeERC20.sol deleted file mode 100644 index 8fd67fb..0000000 --- a/contracts/libraries/SafeERC20.sol +++ /dev/null @@ -1,77 +0,0 @@ -pragma solidity ^0.5.0; - -import "../interfaces/IERC20.sol"; -import "./SafeMath.sol"; -import "./Address.sol"; - -/** - * @title SafeERC20 - * @dev Wrappers around ERC20 operations that throw on failure (when the token - * contract returns false). Tokens that return no value (and instead revert or - * throw on failure) are also supported, non-reverting calls are assumed to be - * successful. - * To use this library you can add a `using SafeERC20 for ERC20;` statement to your contract, - * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. - */ -library SafeERC20 { - using SafeMath for uint256; - using Address for address; - - function safeTransfer(IERC20 token, address to, uint256 value) internal { - callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); - } - - function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { - callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); - } - - function safeApprove(IERC20 token, address spender, uint256 value) internal { - // safeApprove should only be called when setting an initial allowance, - // or when resetting it to zero. To increase and decrease it, use - // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' - // solhint-disable-next-line max-line-length - require( - (value == 0) || (token.allowance(address(this), spender) == 0), - "SafeERC20: approve from non-zero to non-zero allowance" - ); - callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); - } - - function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { - uint256 newAllowance = token.allowance(address(this), spender).add(value); - callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); - } - - function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { - uint256 newAllowance = token.allowance(address(this), spender).sub(value); - callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); - } - - /** - * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement - * on the return value: the return value is optional (but if data is returned, it must not be false). - * @param token The token targeted by the call. - * @param data The call data (encoded using abi.encode or one of its variants). - */ - function callOptionalReturn(IERC20 token, bytes memory data) private { - // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since - // we're implementing it ourselves. - - // A Solidity high level call has three parts: - // 1. The target address is checked to verify it contains contract code - // 2. The call itself is made, and success asserted - // 3. The return value is decoded, which in turn checks the size of the returned data. - // solhint-disable-next-line max-line-length - require(address(token).isContract(), "SafeERC20: call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = address(token).call(data); - require(success, "SafeERC20: low-level call failed"); - - if (returndata.length > 0) { - // Return data is optional - // solhint-disable-next-line max-line-length - require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); - } - } -} diff --git a/contracts/libraries/SafeMath.sol b/contracts/libraries/SafeMath.sol deleted file mode 100644 index 2f1192f..0000000 --- a/contracts/libraries/SafeMath.sol +++ /dev/null @@ -1,107 +0,0 @@ -pragma solidity ^0.5.0; - -/** - * @dev Wrappers over Solidity's arithmetic operations with added overflow - * checks. - * - * Arithmetic operations in Solidity wrap on overflow. This can easily result - * in bugs, because programmers usually assume that an overflow raises an - * error, which is the standard behavior in high level programming languages. - * `SafeMath` restores this intuition by reverting the transaction when an - * operation overflows. - * - * Using this library instead of the unchecked operations eliminates an entire - * class of bugs, so it's recommended to use it always. - */ -library SafeMath { - /** - * @dev Returns the addition of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `+` operator. - * - * Requirements: - * - Addition cannot overflow. - */ - function add(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 c = a + b; - require(c >= a, "SafeMath: addition overflow"); - - return c; - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b) internal pure returns (uint256) { - require(b <= a, "SafeMath: subtraction overflow"); - uint256 c = a - b; - - return c; - } - - /** - * @dev Returns the multiplication of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `*` operator. - * - * Requirements: - * - Multiplication cannot overflow. - */ - function mul(uint256 a, uint256 b) internal pure returns (uint256) { - // Gas optimization: this is cheaper than requiring 'a' not being zero, but the - // benefit is lost if 'b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 - if (a == 0) { - return 0; - } - - uint256 c = a * b; - require(c / a == b, "SafeMath: multiplication overflow"); - - return c; - } - - /** - * @dev Returns the integer division of two unsigned integers. Reverts on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b) internal pure returns (uint256) { - // Solidity only automatically asserts when dividing by 0 - require(b > 0, "SafeMath: division by zero"); - uint256 c = a / b; - // assert(a == b * c + a % b); // There is no case in which this doesn't hold - - return c; - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * Reverts when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b) internal pure returns (uint256) { - require(b != 0, "SafeMath: modulo by zero"); - return a % b; - } -} diff --git a/contracts/libraries/Types.sol b/contracts/libraries/Types.sol index 567abba..3ac6bb4 100644 --- a/contracts/libraries/Types.sol +++ b/contracts/libraries/Types.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.17; +pragma solidity ^0.8.9; library Types { struct Stream { diff --git a/hardhat.config.js b/hardhat.config.js index 1175890..ff3c472 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -8,13 +8,7 @@ module.exports = { compilers: [ { version: "0.8.20", - }, - { - version: "0.6.12", - }, - { - version: "0.5.17", - }, + } ], }, settings: { diff --git a/package-lock.json b/package-lock.json index 3737211..0a9558d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@openzeppelin/contracts": "^3.2.0-rc.0", + "@openzeppelin/contracts": "^3.4.2", "@openzeppelin/upgrades-core": "^1.30.1", "base58-solidity": "^1.0.2", "bignumber.js": "^9.0.1", diff --git a/package.json b/package.json index d1fdd69..117843d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "typescript": "^5.2.2" }, "dependencies": { - "@openzeppelin/contracts": "^3.2.0-rc.0", + "@openzeppelin/contracts": "^3.4.2", "@openzeppelin/upgrades-core": "^1.30.1", "base58-solidity": "^1.0.2", "bignumber.js": "^9.0.1", diff --git a/test/Proposal.js b/test/Proposal.js index 0a39cb1..5b0eae6 100644 --- a/test/Proposal.js +++ b/test/Proposal.js @@ -125,37 +125,39 @@ describe("Proposal results check", function () { const torn = await getTorn(); const recipientBalance = await torn.balanceOf(someRecipient); const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); - await connectedAirdrop.withdrawFromStream(recipientStreamId, normalizedAmount); + await connectedAirdrop.withdrawFromStream(recipientStreamId); expect(await torn.balanceOf(someRecipient)).to.be.equal(recipientBalance + normalizedAmount); }); - it("Airdrop recipient should not be able to withdraw all funds early", async function () { + it("Airdrop recipient should be able to withdraw part of funds early", async function () { const someRecipient = await resolveAddr("butterfly-effect.eth"); - const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); - const recipients = await airdropRecipientsContract.getAirdropRecipients(); - const selectedRecipientInfo = recipients.find((r) => r[0] === someRecipient); - const recipientAirdropAmount = selectedRecipientInfo[1]; + const { airdropContract } = await deployAndExecuteProposal(); const halfYear = 180 * 24 * 60 * 60; - const normalizedAmount = normalizeAirdropAmount(recipientAirdropAmount); const events = await getEvents(airdropContract, "CreateStream"); const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; await time.increase(halfYear / 2); + const torn = await getTorn(); + const recipientBalance = await torn.balanceOf(someRecipient); const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); - await expect(connectedAirdrop.withdrawFromStream(recipientStreamId, normalizedAmount)).to.be.revertedWith( - "amount exceeds the available balance", - ); + + // Add timestamps and manual calculation bcs withdrawFromStream call mine next block and increase stream balance + const streamBalance = await connectedAirdrop.balanceOf(recipientStreamId, someRecipient); + const beforeWithdrawalTimestamp = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp; + await connectedAirdrop.withdrawFromStream(recipientStreamId); + const afterWithdrawalTimestamp = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp; + const blockChangeTime = afterWithdrawalTimestamp - beforeWithdrawalTimestamp; + const stream = await connectedAirdrop.getStream(recipientStreamId); + const ratePerSecond = stream[5]; + const addedInCurrentBlock = BigInt(blockChangeTime) * ratePerSecond; + expect(await torn.balanceOf(someRecipient)).to.be.equal(recipientBalance + streamBalance + addedInCurrentBlock); }); it("Airdrop recipient should not be able to withdraw funds if his stake balance is lower than initial", async function () { const someRecipient = await resolveAddr("butterfly-effect.eth"); - const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); - const recipients = await airdropRecipientsContract.getAirdropRecipients(); - const selectedRecipientInfo = recipients.find((r) => r[0] === someRecipient); - const recipientAirdropAmount = selectedRecipientInfo[1]; + const { airdropContract } = await deployAndExecuteProposal(); const halfYear = 180 * 24 * 60 * 60; - const normalizedAmount = normalizeAirdropAmount(recipientAirdropAmount); const events = await getEvents(airdropContract, "CreateStream"); const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; @@ -163,44 +165,22 @@ describe("Proposal results check", function () { const governance = await getGovernance(await ethers.getImpersonatedSigner(someRecipient)); governance.unlock(1000n * 10n ** 18n); const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); - await expect(connectedAirdrop.withdrawFromStream(recipientStreamId, normalizedAmount)).to.be.revertedWith( + await expect(connectedAirdrop.withdrawFromStream(recipientStreamId)).to.be.revertedWith( "not enough locked tokens in governance", ); }); - it("Airdrop recipient should be able to withdraw part of funds", async function () { - const someRecipient = await resolveAddr("butterfly-effect.eth"); - const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); - const recipients = await airdropRecipientsContract.getAirdropRecipients(); - const selectedRecipientInfo = recipients.find((r) => r[0] === someRecipient); - const recipientAirdropAmount = selectedRecipientInfo[1]; - const quarter = 90 * 24 * 60 * 60; - const events = await getEvents(airdropContract, "CreateStream"); - const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; - - await time.increase(quarter); - const torn = await getTorn(); - const recipientBalance = await torn.balanceOf(someRecipient); - const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); - await connectedAirdrop.withdrawFromStream(recipientStreamId, recipientAirdropAmount / 4n); - expect(await torn.balanceOf(someRecipient)).to.be.equal(recipientBalance + recipientAirdropAmount / 4n); - }); - it("Airdrop recipient should not be able to withdraw funds if stream is canceled", async function () { const someRecipient = await resolveAddr("butterfly-effect.eth"); const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); - const recipients = await airdropRecipientsContract.getAirdropRecipients(); - const selectedRecipientInfo = recipients.find((r) => r[0] === someRecipient); - const recipientAirdropAmount = selectedRecipientInfo[1]; const halfYear = 180 * 24 * 60 * 60; - const normalizedAmount = recipientAirdropAmount - (recipientAirdropAmount % BigInt(halfYear)); const events = await getEvents(airdropContract, "CreateStream"); const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; await time.increase(halfYear); await airdropContract.cancelStream(recipientStreamId); const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); - await expect(connectedAirdrop.withdrawFromStream(recipientStreamId, normalizedAmount)).to.be.revertedWith( + await expect(connectedAirdrop.withdrawFromStream(recipientStreamId)).to.be.revertedWith( "stream does not exist", ); }); @@ -209,17 +189,14 @@ describe("Proposal results check", function () { const someRecipient = await resolveAddr("butterfly-effect.eth"); const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); const recipients = await airdropRecipientsContract.getAirdropRecipients(); - const selectedRecipientInfo = recipients.find((r) => r[0] === someRecipient); - const recipientAirdropAmount = selectedRecipientInfo[1]; const halfYear = 180 * 24 * 60 * 60; - const normalizedAmount = normalizeAirdropAmount(recipientAirdropAmount); const events = await getEvents(airdropContract, "CreateStream"); const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; await time.increase(halfYear); await airdropContract.cancelAirdrop(1, recipients.length); const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); - await expect(connectedAirdrop.withdrawFromStream(recipientStreamId, normalizedAmount)).to.be.revertedWith( + await expect(connectedAirdrop.withdrawFromStream(recipientStreamId)).to.be.revertedWith( "stream does not exist", ); }); diff --git a/test/abi/airdrop.abi.json b/test/abi/airdrop.abi.json index c976b59..bb7fb7e 100644 --- a/test/abi/airdrop.abi.json +++ b/test/abi/airdrop.abi.json @@ -1,7 +1,6 @@ [ { "inputs": [], - "payable": false, "stateMutability": "nonpayable", "type": "constructor" }, @@ -136,7 +135,6 @@ "type": "event" }, { - "constant": true, "inputs": [ { "internalType": "uint256", @@ -157,12 +155,10 @@ "type": "uint256" } ], - "payable": false, "stateMutability": "view", "type": "function" }, { - "constant": false, "inputs": [ { "internalType": "uint256", @@ -183,12 +179,10 @@ "type": "bool" } ], - "payable": false, "stateMutability": "nonpayable", "type": "function" }, { - "constant": false, "inputs": [ { "internalType": "uint256", @@ -204,12 +198,10 @@ "type": "bool" } ], - "payable": false, "stateMutability": "nonpayable", "type": "function" }, { - "constant": false, "inputs": [ { "internalType": "uint256", @@ -235,12 +227,10 @@ "type": "bool" } ], - "payable": false, "stateMutability": "nonpayable", "type": "function" }, { - "constant": true, "inputs": [ { "internalType": "uint256", @@ -256,12 +246,10 @@ "type": "uint256" } ], - "payable": false, "stateMutability": "view", "type": "function" }, { - "constant": true, "inputs": [ { "internalType": "uint256", @@ -302,12 +290,10 @@ "type": "uint256" } ], - "payable": false, "stateMutability": "view", "type": "function" }, { - "constant": true, "inputs": [], "name": "nextStreamId", "outputs": [ @@ -317,12 +303,10 @@ "type": "uint256" } ], - "payable": false, "stateMutability": "view", "type": "function" }, { - "constant": true, "inputs": [], "name": "torn", "outputs": [ @@ -332,12 +316,10 @@ "type": "address" } ], - "payable": false, "stateMutability": "view", "type": "function" }, { - "constant": true, "inputs": [], "name": "tornadoGovernance", "outputs": [ @@ -347,22 +329,15 @@ "type": "address" } ], - "payable": false, "stateMutability": "view", "type": "function" }, { - "constant": false, "inputs": [ { "internalType": "uint256", "name": "streamId", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" } ], "name": "withdrawFromStream", @@ -373,12 +348,10 @@ "type": "bool" } ], - "payable": false, "stateMutability": "nonpayable", "type": "function" }, { - "constant": false, "inputs": [], "name": "withdrawFunds", "outputs": [ @@ -388,7 +361,6 @@ "type": "bool" } ], - "payable": false, "stateMutability": "nonpayable", "type": "function" } diff --git a/test/utils.js b/test/utils.js index b749b89..4a3db75 100644 --- a/test/utils.js +++ b/test/utils.js @@ -2,8 +2,6 @@ const { ethers, network } = require("hardhat"); const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); const stakingAddr = "0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29"; -const gasCompensationAddr = "0xFA4C1f3f7D5dd7c12a9Adb82Cd7dDA542E3d59ef"; -const userVaultAddr = "0x2F50508a8a3D323B91336FA3eA6ae50E55f32185"; const governanceAddr = "0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce"; const tornAddr = "0x77777FeDdddFfC19Ff86DB637967013e6C6A116C";