fix bugs change tests

This commit is contained in:
ButterflyEffect 2024-02-12 09:27:34 +00:00
parent 8793ef1daf
commit 5535e55895
17 changed files with 63 additions and 414 deletions

@ -1,24 +1,25 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity =0.5.17; pragma solidity ^0.8.9;
pragma experimental ABIEncoderV2;
import "./interfaces/IERC20.sol"; import "./interfaces/IERC20.sol";
import "./interfaces/IGovernance.sol"; import "./interfaces/IGovernance.sol";
import "./libraries/SafeERC20.sol";
import "./ReentrancyGuard.sol"; import "./ReentrancyGuard.sol";
import "./CarefulMath.sol";
import "./interfaces/ISablierAirdrop.sol"; import "./interfaces/ISablierAirdrop.sol";
import "./interfaces/IRecipientStorage.sol"; import "./interfaces/IRecipientStorage.sol";
import "./libraries/Types.sol"; import "./libraries/Types.sol";
contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard {
using SafeERC20 for IERC20;
/*** Storage Properties ***/ /*** Storage Properties ***/
/**
* @notice Tornado Governance contract is an owner of airdrop contract and can create or cancel airdrops
*/
address public constant tornadoGovernance = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce; address public constant tornadoGovernance = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
/**
* @notice TORN token - token for airdrops
*/
IERC20 public constant torn = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C); IERC20 public constant torn = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C);
/** /**
@ -59,7 +60,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath {
/*** Contract Logic Starts Here */ /*** Contract Logic Starts Here */
constructor() public { constructor() {
nextStreamId = 1; nextStreamId = 1;
} }
@ -69,7 +70,6 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath {
* @notice Returns the stream with all its properties. * @notice Returns the stream with all its properties.
* @dev Throws if the id does not point to a valid stream. * @dev Throws if the id does not point to a valid stream.
* @param streamId The id of the stream to query. * @param streamId The id of the stream to query.
* @return The stream object.
*/ */
function getStream( function getStream(
uint256 streamId uint256 streamId
@ -100,7 +100,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath {
* `startTime`, it returns 0. * `startTime`, it returns 0.
* @dev Throws if the id does not point to a valid stream. * @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. * @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) { function deltaOf(uint256 streamId) public view streamExists(streamId) returns (uint256 delta) {
Types.Stream memory stream = streams[streamId]; Types.Stream memory stream = streams[streamId];
@ -109,26 +109,17 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath {
return stream.stopTime - stream.startTime; 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. * @notice Returns the available funds for the given stream id and address.
* @dev Throws if the id does not point to a valid stream. * @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 streamId The id of the stream for which to query the balance.
* @param who The address 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) { function balanceOf(uint256 streamId, address who) public view streamExists(streamId) returns (uint256 balance) {
Types.Stream memory stream = streams[streamId]; Types.Stream memory stream = streams[streamId];
BalanceOfLocalVars memory vars;
uint256 delta = deltaOf(streamId); uint256 delta = deltaOf(streamId);
(vars.mathErr, vars.recipientBalance) = mulUInt(delta, stream.ratePerSecond); uint256 recipientBalance = delta * stream.ratePerSecond;
require(vars.mathErr == MathError.NO_ERROR, "recipient balance calculation error");
/* /*
* If the stream `balance` does not equal `deposit`, it means there have been withdrawals. * 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. * streamed until now.
*/ */
if (stream.deposit > stream.remainingBalance) { if (stream.deposit > stream.remainingBalance) {
(vars.mathErr, vars.withdrawalAmount) = subUInt(stream.deposit, stream.remainingBalance); uint256 withdrawalAmount = stream.deposit - stream.remainingBalance;
assert(vars.mathErr == MathError.NO_ERROR); recipientBalance = recipientBalance - withdrawalAmount;
(vars.mathErr, vars.recipientBalance) = subUInt(vars.recipientBalance, vars.withdrawalAmount);
/* `withdrawalAmount` cannot and should not be bigger than `recipientBalance`. */
assert(vars.mathErr == MathError.NO_ERROR);
} }
if (who == stream.recipient) return vars.recipientBalance; if (who == stream.recipient) return recipientBalance;
return 0; return 0;
} }
@ -151,7 +139,6 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath {
* @notice To avoid "stack to deep" error * @notice To avoid "stack to deep" error
*/ */
struct CreateAirdropLocalVars { struct CreateAirdropLocalVars {
MathError mathErr;
uint256 airdropDuration; uint256 airdropDuration;
uint256 ratePerSecond; uint256 ratePerSecond;
uint256 firstStream; uint256 firstStream;
@ -163,7 +150,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath {
uint256 startTime, uint256 startTime,
uint256 stopTime, uint256 stopTime,
address recipientStorage address recipientStorage
) public onlyGovernance returns (bool) { ) external onlyGovernance returns (bool) {
CreateAirdropLocalVars memory vars; CreateAirdropLocalVars memory vars;
vars.airdropDuration = stopTime - startTime; vars.airdropDuration = stopTime - startTime;
vars.airdropRecipients = IRecipientStorage(recipientStorage).getAirdropRecipients(); vars.airdropRecipients = IRecipientStorage(recipientStorage).getAirdropRecipients();
@ -180,9 +167,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath {
/* Without this, the rate per second would be zero. */ /* Without this, the rate per second would be zero. */
require(normalizedDeposit >= vars.airdropDuration, "deposit smaller than time delta"); require(normalizedDeposit >= vars.airdropDuration, "deposit smaller than time delta");
(vars.mathErr, vars.ratePerSecond) = divUInt(normalizedDeposit, vars.airdropDuration); vars.ratePerSecond = normalizedDeposit / vars.airdropDuration;
/* `divUInt` can only return MathError.DIVISION_BY_ZERO but we know `duration` is not zero. */
assert(vars.mathErr == MathError.NO_ERROR);
/* Create and store the stream object. */ /* Create and store the stream object. */
uint256 streamId = nextStreamId; uint256 streamId = nextStreamId;
@ -198,8 +183,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath {
}); });
/* Increment the next stream id. */ /* Increment the next stream id. */
(vars.mathErr, nextStreamId) = addUInt(nextStreamId, uint256(1)); nextStreamId = nextStreamId + 1;
require(vars.mathErr == MathError.NO_ERROR, "next stream id calculation error");
emit CreateStream( emit CreateStream(
streamId, 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. * @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 caller is not the sender or the recipient of the stream.
* Throws if the amount exceeds the available balance. * Throws if the amount exceeds the available balance.
* Throws if there is a token transfer failure. * Throws if there is a token transfer failure.
* @param streamId The id of the stream to withdraw tokens from. * @param streamId The id of the stream to withdraw tokens from.
* @param amount The amount of tokens to withdraw.
*/ */
function withdrawFromStream( function withdrawFromStream(
uint256 streamId, uint256 streamId
uint256 amount
) external nonReentrant streamExists(streamId) onlyRecipient(streamId) returns (bool) { ) external nonReentrant streamExists(streamId) onlyRecipient(streamId) returns (bool) {
require(amount > 0, "amount is zero");
Types.Stream memory stream = streams[streamId]; 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); uint256 recipientLockedBalance = IGovernance(tornadoGovernance).lockedBalance(stream.recipient);
require(recipientLockedBalance >= stream.initialLockedBalance, "not enough locked tokens in governance"); require(recipientLockedBalance >= stream.initialLockedBalance, "not enough locked tokens in governance");
uint256 balance = balanceOf(streamId, stream.recipient); /* Remaining balance can not be less than recipient stream balance */
require(balance >= amount, "amount exceeds the available balance"); streams[streamId].remainingBalance = stream.remainingBalance - 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);
if (streams[streamId].remainingBalance == 0) delete streams[streamId]; if (streams[streamId].remainingBalance == 0) delete streams[streamId];
torn.safeTransfer(stream.recipient, amount); torn.transfer(stream.recipient, balance);
emit WithdrawFromStream(streamId, stream.recipient, amount); emit WithdrawFromStream(streamId, stream.recipient, balance);
return true; return true;
} }
@ -266,7 +242,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath {
delete streams[streamId]; delete streams[streamId];
if (remainingBalance > 0) torn.safeTransfer(tornadoGovernance, remainingBalance); if (remainingBalance > 0) torn.transfer(tornadoGovernance, remainingBalance);
emit CancelStream(streamId, stream.recipient, remainingBalance); emit CancelStream(streamId, stream.recipient, remainingBalance);
return true; return true;
@ -293,7 +269,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath {
} }
} }
torn.safeTransfer(tornadoGovernance, airdropRemainingBalance); torn.transfer(tornadoGovernance, airdropRemainingBalance);
return true; return true;
} }
@ -303,7 +279,7 @@ contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath {
* @return bool true=success, otherwise false. * @return bool true=success, otherwise false.
*/ */
function withdrawFunds() external onlyGovernance returns (bool) { function withdrawFunds() external onlyGovernance returns (bool) {
torn.safeTransfer(tornadoGovernance, torn.balanceOf(address(this))); torn.transfer(tornadoGovernance, torn.balanceOf(address(this)));
return true; return true;
} }
} }

@ -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);
}
}

@ -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. * @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 /// @dev counter to allow mutex lock with only one SSTORE operation
uint256 private _guardCounter; uint256 private _guardCounter;
constructor() internal { constructor() {
// The counter starts at one to prevent changing it from zero to a non-zero // The counter starts at one to prevent changing it from zero to a non-zero
// value, which is a more expensive operation. // value, which is a more expensive operation.
_guardCounter = 1; _guardCounter = 1;

@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // 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 * @dev Interface of the ERC20 standard as defined in the EIP. Does not include

@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.5.0; pragma solidity ^0.8.9;
interface IGovernance { interface IGovernance {
function lockedBalance(address staker) external view returns (uint256); function lockedBalance(address staker) external view returns (uint256);

@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.5.17; pragma solidity ^0.8.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;

@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.5.17; pragma solidity ^0.8.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
@ -57,7 +57,7 @@ interface ISablierAirdrop {
function createAirdrop(uint256 startTime, uint256 stopTime, address recipientStorage) external returns (bool); 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); function cancelStream(uint256 streamId) external returns (bool);
} }

@ -1,4 +1,4 @@
pragma solidity ^0.5.0; pragma solidity ^0.8.9;
/** /**
* @dev Collection of functions related to the address type, * @dev Collection of functions related to the address type,

@ -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");
}
}
}

@ -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;
}
}

@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.5.17; pragma solidity ^0.8.9;
library Types { library Types {
struct Stream { struct Stream {

@ -8,13 +8,7 @@ module.exports = {
compilers: [ compilers: [
{ {
version: "0.8.20", version: "0.8.20",
}, }
{
version: "0.6.12",
},
{
version: "0.5.17",
},
], ],
}, },
settings: { settings: {

2
package-lock.json generated

@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@openzeppelin/contracts": "^3.2.0-rc.0", "@openzeppelin/contracts": "^3.4.2",
"@openzeppelin/upgrades-core": "^1.30.1", "@openzeppelin/upgrades-core": "^1.30.1",
"base58-solidity": "^1.0.2", "base58-solidity": "^1.0.2",
"bignumber.js": "^9.0.1", "bignumber.js": "^9.0.1",

@ -36,7 +36,7 @@
"typescript": "^5.2.2" "typescript": "^5.2.2"
}, },
"dependencies": { "dependencies": {
"@openzeppelin/contracts": "^3.2.0-rc.0", "@openzeppelin/contracts": "^3.4.2",
"@openzeppelin/upgrades-core": "^1.30.1", "@openzeppelin/upgrades-core": "^1.30.1",
"base58-solidity": "^1.0.2", "base58-solidity": "^1.0.2",
"bignumber.js": "^9.0.1", "bignumber.js": "^9.0.1",

@ -125,37 +125,39 @@ describe("Proposal results check", function () {
const torn = await getTorn(); const torn = await getTorn();
const recipientBalance = await torn.balanceOf(someRecipient); const recipientBalance = await torn.balanceOf(someRecipient);
const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(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); 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 someRecipient = await resolveAddr("butterfly-effect.eth");
const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); const { airdropContract } = 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 halfYear = 180 * 24 * 60 * 60;
const normalizedAmount = normalizeAirdropAmount(recipientAirdropAmount);
const events = await getEvents(airdropContract, "CreateStream"); const events = await getEvents(airdropContract, "CreateStream");
const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0];
await time.increase(halfYear / 2); await time.increase(halfYear / 2);
const torn = await getTorn();
const recipientBalance = await torn.balanceOf(someRecipient);
const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(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 () { 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 someRecipient = await resolveAddr("butterfly-effect.eth");
const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); const { airdropContract } = 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 halfYear = 180 * 24 * 60 * 60;
const normalizedAmount = normalizeAirdropAmount(recipientAirdropAmount);
const events = await getEvents(airdropContract, "CreateStream"); const events = await getEvents(airdropContract, "CreateStream");
const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; 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)); const governance = await getGovernance(await ethers.getImpersonatedSigner(someRecipient));
governance.unlock(1000n * 10n ** 18n); governance.unlock(1000n * 10n ** 18n);
const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); 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", "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 () { it("Airdrop recipient should not be able to withdraw funds if stream is canceled", async function () {
const someRecipient = await resolveAddr("butterfly-effect.eth"); const someRecipient = await resolveAddr("butterfly-effect.eth");
const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); 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 halfYear = 180 * 24 * 60 * 60;
const normalizedAmount = recipientAirdropAmount - (recipientAirdropAmount % BigInt(halfYear));
const events = await getEvents(airdropContract, "CreateStream"); const events = await getEvents(airdropContract, "CreateStream");
const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0];
await time.increase(halfYear); await time.increase(halfYear);
await airdropContract.cancelStream(recipientStreamId); await airdropContract.cancelStream(recipientStreamId);
const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); 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", "stream does not exist",
); );
}); });
@ -209,17 +189,14 @@ describe("Proposal results check", function () {
const someRecipient = await resolveAddr("butterfly-effect.eth"); const someRecipient = await resolveAddr("butterfly-effect.eth");
const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal();
const recipients = await airdropRecipientsContract.getAirdropRecipients(); const recipients = await airdropRecipientsContract.getAirdropRecipients();
const selectedRecipientInfo = recipients.find((r) => r[0] === someRecipient);
const recipientAirdropAmount = selectedRecipientInfo[1];
const halfYear = 180 * 24 * 60 * 60; const halfYear = 180 * 24 * 60 * 60;
const normalizedAmount = normalizeAirdropAmount(recipientAirdropAmount);
const events = await getEvents(airdropContract, "CreateStream"); const events = await getEvents(airdropContract, "CreateStream");
const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0];
await time.increase(halfYear); await time.increase(halfYear);
await airdropContract.cancelAirdrop(1, recipients.length); await airdropContract.cancelAirdrop(1, recipients.length);
const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); 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", "stream does not exist",
); );
}); });

@ -1,7 +1,6 @@
[ [
{ {
"inputs": [], "inputs": [],
"payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "constructor" "type": "constructor"
}, },
@ -136,7 +135,6 @@
"type": "event" "type": "event"
}, },
{ {
"constant": true,
"inputs": [ "inputs": [
{ {
"internalType": "uint256", "internalType": "uint256",
@ -157,12 +155,10 @@
"type": "uint256" "type": "uint256"
} }
], ],
"payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": false,
"inputs": [ "inputs": [
{ {
"internalType": "uint256", "internalType": "uint256",
@ -183,12 +179,10 @@
"type": "bool" "type": "bool"
} }
], ],
"payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{ {
"constant": false,
"inputs": [ "inputs": [
{ {
"internalType": "uint256", "internalType": "uint256",
@ -204,12 +198,10 @@
"type": "bool" "type": "bool"
} }
], ],
"payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{ {
"constant": false,
"inputs": [ "inputs": [
{ {
"internalType": "uint256", "internalType": "uint256",
@ -235,12 +227,10 @@
"type": "bool" "type": "bool"
} }
], ],
"payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{ {
"constant": true,
"inputs": [ "inputs": [
{ {
"internalType": "uint256", "internalType": "uint256",
@ -256,12 +246,10 @@
"type": "uint256" "type": "uint256"
} }
], ],
"payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": true,
"inputs": [ "inputs": [
{ {
"internalType": "uint256", "internalType": "uint256",
@ -302,12 +290,10 @@
"type": "uint256" "type": "uint256"
} }
], ],
"payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": true,
"inputs": [], "inputs": [],
"name": "nextStreamId", "name": "nextStreamId",
"outputs": [ "outputs": [
@ -317,12 +303,10 @@
"type": "uint256" "type": "uint256"
} }
], ],
"payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": true,
"inputs": [], "inputs": [],
"name": "torn", "name": "torn",
"outputs": [ "outputs": [
@ -332,12 +316,10 @@
"type": "address" "type": "address"
} }
], ],
"payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": true,
"inputs": [], "inputs": [],
"name": "tornadoGovernance", "name": "tornadoGovernance",
"outputs": [ "outputs": [
@ -347,22 +329,15 @@
"type": "address" "type": "address"
} }
], ],
"payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": false,
"inputs": [ "inputs": [
{ {
"internalType": "uint256", "internalType": "uint256",
"name": "streamId", "name": "streamId",
"type": "uint256" "type": "uint256"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
} }
], ],
"name": "withdrawFromStream", "name": "withdrawFromStream",
@ -373,12 +348,10 @@
"type": "bool" "type": "bool"
} }
], ],
"payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{ {
"constant": false,
"inputs": [], "inputs": [],
"name": "withdrawFunds", "name": "withdrawFunds",
"outputs": [ "outputs": [
@ -388,7 +361,6 @@
"type": "bool" "type": "bool"
} }
], ],
"payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
} }

@ -2,8 +2,6 @@ const { ethers, network } = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const stakingAddr = "0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29"; const stakingAddr = "0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29";
const gasCompensationAddr = "0xFA4C1f3f7D5dd7c12a9Adb82Cd7dDA542E3d59ef";
const userVaultAddr = "0x2F50508a8a3D323B91336FA3eA6ae50E55f32185";
const governanceAddr = "0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce"; const governanceAddr = "0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce";
const tornAddr = "0x77777FeDdddFfC19Ff86DB637967013e6C6A116C"; const tornAddr = "0x77777FeDdddFfC19Ff86DB637967013e6C6A116C";